From 9de05aa655bb4431f596d590e704983b9c1d1de7 Mon Sep 17 00:00:00 2001 From: Arta Kjato Date: Wed, 21 Jan 2026 22:42:07 +0100 Subject: [PATCH 01/21] add single and collection of results routes --- server.js | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/server.js b/server.js index f47771b..a9e76af 100644 --- a/server.js +++ b/server.js @@ -1,13 +1,18 @@ import cors from "cors" import express from "express" +import { readFile } from 'fs/promises'; +import thoughtsData from './data.json' with { type: "json" }; +//import fs from 'fs'; +import { get } from "http"; -// 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 port = process.env.PORT || 8080; const app = express() -// Add middlewares to enable cors and json body parsing +async function getThoughts() { + const data = await readFile("./data.json"); + return JSON.parse(data); +} + app.use(cors()) app.use(express.json()) @@ -16,6 +21,19 @@ app.get("/", (req, res) => { res.send("Hello Technigo!") }) +app.get("/api/thoughts", async(req, res) => { + const thoughts = await getThoughts(); + res.json(thoughts); +}) + +app.get("/api/thoughts/:id", async(req, res) => { + const thoughts = await getThoughts(); + const id = req.params.id; + const thought = thoughts.find(thought => thought._id === id); + res.json(thought); +}) + + // Start the server app.listen(port, () => { console.log(`Server running on http://localhost:${port}`) From 911cb93bab1529d4f4be8acf06c34e51f51d49d4 Mon Sep 17 00:00:00 2001 From: Arta Kjato Date: Thu, 22 Jan 2026 21:54:36 +0100 Subject: [PATCH 02/21] add documentation of API w Express --- package.json | 1 + server.js | 58 ++++++++++++++++++++++++++-------------------------- 2 files changed, 30 insertions(+), 29 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 a9e76af..3aee0e8 100644 --- a/server.js +++ b/server.js @@ -1,40 +1,40 @@ -import cors from "cors" -import express from "express" -import { readFile } from 'fs/promises'; -import thoughtsData from './data.json' with { type: "json" }; -//import fs from 'fs'; -import { get } from "http"; +import cors from "cors"; +import express from "express"; +import expressListEndpoints from "express-list-endpoints"; +import data from "./data.json"; const port = process.env.PORT || 8080; -const app = express() +const app = express(); -async function getThoughts() { - const data = await readFile("./data.json"); - return JSON.parse(data); -} - -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!") -}) - -app.get("/api/thoughts", async(req, res) => { - const thoughts = await getThoughts(); - res.json(thoughts); -}) - -app.get("/api/thoughts/:id", async(req, res) => { - const thoughts = await getThoughts(); - const id = req.params.id; - const thought = thoughts.find(thought => thought._id === id); + res.send(expressListEndpoints(app)); // List all available endpoints +}); + + +app.get("/api/thoughts", (req, res) => { + if (req.query.minHearts) { + const minHearts = parseInt(req.query.minHearts); + const filteredThoughts = data.filter(thought => thought.hearts >= minHearts); + return res.json(filteredThoughts); + } else { + res.send("Unknown query parameter"); + } + res.json(data); + +}); + +app.get("/api/thoughts/:id", (req, res) => { + const id = req.params.id; + const thought = data.find((thought) => thought._id === id); res.json(thought); -}) +}); // Start the server app.listen(port, () => { - console.log(`Server running on http://localhost:${port}`) -}) + console.log(`Server running on http://localhost:${port}`); +}); From c0b1f29b170a66b42970613140e3ad36bc5d38c2 Mon Sep 17 00:00:00 2001 From: Arta Kjato Date: Thu, 22 Jan 2026 22:18:02 +0100 Subject: [PATCH 03/21] add additional routes --- server.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/server.js b/server.js index 3aee0e8..33a4bbf 100644 --- a/server.js +++ b/server.js @@ -14,17 +14,15 @@ app.get("/", (req, res) => { res.send(expressListEndpoints(app)); // List all available endpoints }); - app.get("/api/thoughts", (req, res) => { if (req.query.minHearts) { const minHearts = parseInt(req.query.minHearts); - const filteredThoughts = data.filter(thought => thought.hearts >= minHearts); + const filteredThoughts = data.filter( + (thought) => thought.hearts >= minHearts, + ); return res.json(filteredThoughts); - } else { - res.send("Unknown query parameter"); } res.json(data); - }); app.get("/api/thoughts/:id", (req, res) => { @@ -33,7 +31,6 @@ app.get("/api/thoughts/:id", (req, res) => { res.json(thought); }); - // Start the server app.listen(port, () => { console.log(`Server running on http://localhost:${port}`); From d37dabce93d1b7fc2da77048663e5586e8610213 Mon Sep 17 00:00:00 2001 From: Arta Kjato Date: Fri, 23 Jan 2026 12:10:19 +0100 Subject: [PATCH 04/21] will add a post method to the server.js file --- server.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/server.js b/server.js index 33a4bbf..1283b9d 100644 --- a/server.js +++ b/server.js @@ -31,6 +31,20 @@ app.get("/api/thoughts/:id", (req, res) => { res.json(thought); }); +app.post("/api/thoughts", (req, res) => { + const body = req.body; + const newThought = { + _id: String(data.length + 1), + message: body.message, + hearts: 0, + createdAt: new Date().toISOString(), + "__v": 0 + }; + data.push(newThought); + res.status(201).json(newThought); +}) +; + // Start the server app.listen(port, () => { console.log(`Server running on http://localhost:${port}`); From 912f579199684ce8c4aea88090c994b277aa2cf5 Mon Sep 17 00:00:00 2001 From: Arta Kjato Date: Fri, 23 Jan 2026 12:57:40 +0100 Subject: [PATCH 05/21] expansion on endpoints to provide more information to users about the API. --- server.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/server.js b/server.js index 1283b9d..fe4c5d8 100644 --- a/server.js +++ b/server.js @@ -9,10 +9,13 @@ const app = express(); app.use(cors()); app.use(express.json()); -// Start defining your routes here -app.get("/", (req, res) => { - res.send(expressListEndpoints(app)); // List all available endpoints -}); +app.get("/", (req, res) => { //listing all available endpoints + const endpoints = expressListEndpoints(app); + res.json({ + message: "Welcome to the Thoughts API!", + endpoints: endpoints, + }); + }); app.get("/api/thoughts", (req, res) => { if (req.query.minHearts) { From 9a54edbd5d3d41a668d6150e14e591921447980d Mon Sep 17 00:00:00 2001 From: Arta Kjato Date: Wed, 28 Jan 2026 22:47:30 +0100 Subject: [PATCH 06/21] connect to MongoDB database --- package.json | 1 + server.js | 33 ++++++++++++++++++++++++++++----- 2 files changed, 29 insertions(+), 5 deletions(-) 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/server.js b/server.js index fe4c5d8..6f8c7fb 100644 --- a/server.js +++ b/server.js @@ -2,6 +2,26 @@ import cors from "cors"; import express from "express"; import expressListEndpoints from "express-list-endpoints"; import data from "./data.json"; +import mongoose from 'mongoose'; + +const mongoDB = "mongodb://localhost:27017/happythoughts"; +main().catch((err) => console.log(err)); +async function main() { + console.log("Connecting to MongoDB..."); + await mongoose.connect(mongoDB); +} + +const Schema = mongoose.Schema; + +const HappyThoughtsSchema = new Schema({ + // _id: String, + message: String, + hearts: Number, + createdAt: String, + __v: Number +}); + +const HappyThoughts = mongoose.model("HappyThoughts", HappyThoughtsSchema); const port = process.env.PORT || 8080; const app = express(); @@ -17,15 +37,18 @@ app.get("/", (req, res) => { //listing all available endpoints }); }); -app.get("/api/thoughts", (req, res) => { +app.get("/api/thoughts", async (req, res) => { if (req.query.minHearts) { const minHearts = parseInt(req.query.minHearts); - const filteredThoughts = data.filter( - (thought) => thought.hearts >= minHearts, - ); + const filteredThoughts = await HappyThoughts.find({ hearts: { $gte: minHearts } }); //greater than or equal to return res.json(filteredThoughts); } - res.json(data); + try { + const thoughts = await HappyThoughts.find(); + res.json(thoughts); + } catch (err) { + res.status(500).json({ error: err.message }); + } }); app.get("/api/thoughts/:id", (req, res) => { From 66d2f5dd97aba9dedd60875c326fc29dd6533701 Mon Sep 17 00:00:00 2001 From: Arta Kjato Date: Thu, 29 Jan 2026 19:17:44 +0100 Subject: [PATCH 07/21] fetch thoughts by id --- server.js | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/server.js b/server.js index 6f8c7fb..573daf4 100644 --- a/server.js +++ b/server.js @@ -2,7 +2,7 @@ import cors from "cors"; import express from "express"; import expressListEndpoints from "express-list-endpoints"; import data from "./data.json"; -import mongoose from 'mongoose'; +import mongoose from "mongoose"; const mongoDB = "mongodb://localhost:27017/happythoughts"; main().catch((err) => console.log(err)); @@ -14,11 +14,11 @@ async function main() { const Schema = mongoose.Schema; const HappyThoughtsSchema = new Schema({ - // _id: String, + _id: String, message: String, hearts: Number, createdAt: String, - __v: Number + __v: Number, }); const HappyThoughts = mongoose.model("HappyThoughts", HappyThoughtsSchema); @@ -29,21 +29,24 @@ const app = express(); app.use(cors()); app.use(express.json()); -app.get("/", (req, res) => { //listing all available endpoints +app.get("/", (req, res) => { + //listing all available endpoints const endpoints = expressListEndpoints(app); res.json({ message: "Welcome to the Thoughts API!", - endpoints: endpoints, - }); + endpoints: endpoints, }); +}); app.get("/api/thoughts", async (req, res) => { if (req.query.minHearts) { const minHearts = parseInt(req.query.minHearts); - const filteredThoughts = await HappyThoughts.find({ hearts: { $gte: minHearts } }); //greater than or equal to + const filteredThoughts = await HappyThoughts.find({ + hearts: { $gte: minHearts }, + }); //greater than or equal to return res.json(filteredThoughts); } - try { + try { const thoughts = await HappyThoughts.find(); res.json(thoughts); } catch (err) { @@ -51,10 +54,20 @@ app.get("/api/thoughts", async (req, res) => { } }); -app.get("/api/thoughts/:id", (req, res) => { +app.get("/api/thoughts/:id", async (req, res) => { const id = req.params.id; - const thought = data.find((thought) => thought._id === id); - res.json(thought); + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ error: "Invalid id" }); + } + try { + const thought = await HappyThoughts.findById(id); + if (!thought) { + return res.status(404).json({ error: "Thought not found" }); + } + return res.json(thought); + } catch (err) { + return res.status(500).json({ error: err.message }); + } }); app.post("/api/thoughts", (req, res) => { @@ -64,12 +77,11 @@ app.post("/api/thoughts", (req, res) => { message: body.message, hearts: 0, createdAt: new Date().toISOString(), - "__v": 0 + __v: 0, }; data.push(newThought); res.status(201).json(newThought); -}) -; +}); // Start the server app.listen(port, () => { From 77d7adb8e6da81dc10c474af7d3d1b4326e2a230 Mon Sep 17 00:00:00 2001 From: Arta Kjato Date: Thu, 29 Jan 2026 19:23:14 +0100 Subject: [PATCH 08/21] create new thought in database --- server.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server.js b/server.js index 573daf4..b861295 100644 --- a/server.js +++ b/server.js @@ -70,16 +70,17 @@ app.get("/api/thoughts/:id", async (req, res) => { } }); -app.post("/api/thoughts", (req, res) => { +app.post("/api/thoughts", async (req, res) => { const body = req.body; const newThought = { - _id: String(data.length + 1), + _id: new mongoose.Types.ObjectId().toString(), message: body.message, hearts: 0, createdAt: new Date().toISOString(), __v: 0, }; - data.push(newThought); + await HappyThoughts.create(newThought); + // data.push(newThought); res.status(201).json(newThought); }); From d3e45dde5b5bd8f8d79a9eb69e4c36e61b35801a Mon Sep 17 00:00:00 2001 From: Arta Kjato Date: Thu, 29 Jan 2026 19:38:03 +0100 Subject: [PATCH 09/21] update and delete thought --- server.js | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/server.js b/server.js index b861295..598e5ee 100644 --- a/server.js +++ b/server.js @@ -57,7 +57,7 @@ app.get("/api/thoughts", async (req, res) => { app.get("/api/thoughts/:id", async (req, res) => { const id = req.params.id; if (!mongoose.Types.ObjectId.isValid(id)) { - return res.status(400).json({ error: "Invalid id" }); + return res.status(400).json({ error: "Invalid id" }); // will check if the id is valid } try { const thought = await HappyThoughts.findById(id); @@ -72,6 +72,11 @@ app.get("/api/thoughts/:id", async (req, res) => { app.post("/api/thoughts", async (req, res) => { const body = req.body; + if (!body.message || body.message.length < 5 || body.message.length > 140) { + return res.status(400).json({ + error: "Message is required and must be between 5 and 140 characters", // will validate the message length or if it's empty + }); + } const newThought = { _id: new mongoose.Types.ObjectId().toString(), message: body.message, @@ -84,6 +89,48 @@ app.post("/api/thoughts", async (req, res) => { res.status(201).json(newThought); }); +app.put("/api/thoughts/:id", async (req, res) => { + const id = req.params.id; + const { message } = req.body; + if (!message || message.length < 5 || message.length > 140) { + return res.status(400).json({ + error: "Message is required and must be between 5 and 140 characters", + }); + } + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ error: "Invalid id" }); + } + try { + const thought = await HappyThoughts.findByIdAndUpdate( + id, + { $set: { message: message } }, + { new: true } + ); + if (!thought) { + return res.status(404).json({ error: "Thought not found" }); + } + return res.json(thought); + } catch (err) { + return res.status(500).json({ error: err.message }); + } +}); + +app.delete("/api/thoughts/:id", async (req, res) => { + const id = req.params.id; + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ error: "Invalid id" }); + } + try { + const thought = await HappyThoughts.findByIdAndDelete(id); + if (!thought) { + return res.status(404).json({ error: "Thought not found" }); + } + return res.json({ message: "Thought deleted successfully" }); + } catch (err) { + return res.status(500).json({ error: err.message }); + } +}); + // Start the server app.listen(port, () => { console.log(`Server running on http://localhost:${port}`); From 034db780c3f8a8e9876cdae8a8b4ffce0ec9994e Mon Sep 17 00:00:00 2001 From: Arta Kjato Date: Fri, 30 Jan 2026 11:04:05 +0100 Subject: [PATCH 10/21] implemented cluster db connection string --- server.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server.js b/server.js index 598e5ee..ae03bca 100644 --- a/server.js +++ b/server.js @@ -4,7 +4,9 @@ import expressListEndpoints from "express-list-endpoints"; import data from "./data.json"; import mongoose from "mongoose"; -const mongoDB = "mongodb://localhost:27017/happythoughts"; +//const mongoDB = "mongodb://localhost:27017/happythoughts"; +const password = process.env.PASSWORD; +const mongoDB = process.env.MONGO_URL || `mongodb+srv://artakjato:${password}@clusterhappythoughts.fhtetam.mongodb.net/?appName=ClusterHappyThoughts`; main().catch((err) => console.log(err)); async function main() { console.log("Connecting to MongoDB..."); From ce9d9d42096d17228f6aeda5a10164df0d2620af Mon Sep 17 00:00:00 2001 From: Arta Kjato Date: Fri, 30 Jan 2026 12:29:12 +0100 Subject: [PATCH 11/21] add like API --- package.json | 1 + server.js | 29 +++++++++++++++++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 214a34f..e3f8894 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@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", "mongoose": "^9.1.5", diff --git a/server.js b/server.js index ae03bca..a8cac91 100644 --- a/server.js +++ b/server.js @@ -1,12 +1,14 @@ import cors from "cors"; import express from "express"; import expressListEndpoints from "express-list-endpoints"; -import data from "./data.json"; import mongoose from "mongoose"; +import "dotenv/config"; //const mongoDB = "mongodb://localhost:27017/happythoughts"; -const password = process.env.PASSWORD; -const mongoDB = process.env.MONGO_URL || `mongodb+srv://artakjato:${password}@clusterhappythoughts.fhtetam.mongodb.net/?appName=ClusterHappyThoughts`; +const password = process.env.PASSWORD; +const mongoDB = + process.env.MONGO_URL || + `mongodb+srv://artakjato:${password}@clusterhappythoughts.fhtetam.mongodb.net/?appName=ClusterHappyThoughts`; main().catch((err) => console.log(err)); async function main() { console.log("Connecting to MongoDB..."); @@ -106,7 +108,7 @@ app.put("/api/thoughts/:id", async (req, res) => { const thought = await HappyThoughts.findByIdAndUpdate( id, { $set: { message: message } }, - { new: true } + { new: true }, ); if (!thought) { return res.status(404).json({ error: "Thought not found" }); @@ -133,6 +135,25 @@ app.delete("/api/thoughts/:id", async (req, res) => { } }); +app.post("/api/thoughts/:id/like", async (req, res) => { + const id = req.params.id; + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ error: "Invalid id" }); + } + try { + const thought = await HappyThoughts.findByIdAndUpdate( + id, + { $inc: { hearts: 1 } } + ); + if (!thought) { + return res.status(404).json({ error: "Thought not found" }); + } + return res.json(thought); + } catch (err) { + return res.status(500).json({ error: err.message }); + } +}); + // Start the server app.listen(port, () => { console.log(`Server running on http://localhost:${port}`); From 16a499a06534b03813d8a2ac7cb09b7a92cd5dfe Mon Sep 17 00:00:00 2001 From: Arta Kjato Date: Thu, 5 Feb 2026 10:50:43 +0100 Subject: [PATCH 12/21] create user route --- routes/userRoutes.js | 74 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 routes/userRoutes.js diff --git a/routes/userRoutes.js b/routes/userRoutes.js new file mode 100644 index 0000000..cb8ab6a --- /dev/null +++ b/routes/userRoutes.js @@ -0,0 +1,74 @@ +import express, { response } from 'express'; +import bcrypt from 'bcryptjs'; +import { User } from '../models/User.js'; + +const router = express.Router(); + +router.post('/signup', async (req, res) => { + try { + const { email, password } = req.body; + + const existingUser = await User.findOne({ email: email.toLowerCase() }); + if (existingUser) { + return res.status(400).json({ + success: false, + error: 'Email already in use' + }); + } + + const salt = bcrypt.genSalt(); + const hashedPassword = await bcrypt.hash(password, salt); + const user = new User({ email, password: hashedPassword }); + await user.save(); + + return res.status(201).json({ + success: true, + message: 'User created successfully', + response: { + email: user.email, + id: user._id, + accessToken: user.accessToken, + }, + }); +} catch (error) { + res.status(400).json({ + success: false, + message: 'Could not create user', + response: error, + }); +} +}); + +router.post('/login', async (req, res) => { + try { + const { email, password } = req.body; + + const user = await User.findOne({ email: email.toLowerCase() }); + + if (user && bcrypt.compareSync(password, user.password)) { + res.json({ + success: true, + message: 'Login successful', + response: { + email: user.email, + id: user._id, + accessToken: user.accessToken, + }, + }); + } else { + res.status(401).json({ + success: false, + message: 'Invalid email or password', + response: null, + }); + } + } catch (error) { + res.status(500).json({ + success: false, + message: 'Login failed', + response: error, + }); + } +}); + +export default router; From 73cdec0a430ed84eedce66414b806e8123d451e2 Mon Sep 17 00:00:00 2001 From: Arta Kjato Date: Thu, 5 Feb 2026 11:04:52 +0100 Subject: [PATCH 13/21] create user model --- models/User.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 models/User.js diff --git a/models/User.js b/models/User.js new file mode 100644 index 0000000..0dd904d --- /dev/null +++ b/models/User.js @@ -0,0 +1,20 @@ +import mongoose, { Schema } from "mongoose"; +import crypto from "crypto"; + +const UserSchema = new Schema({ + email: { + type: String, + required: true, + unique: 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 From 7f86f557c455c175997d45a33c0a8c3dce1389c2 Mon Sep 17 00:00:00 2001 From: Arta Kjato Date: Thu, 5 Feb 2026 11:14:27 +0100 Subject: [PATCH 14/21] separate happy thoughts in routes and models files --- models/happyThought.js | 13 ++++ routes/happyThoughtRoutes.js | 119 +++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 models/happyThought.js create mode 100644 routes/happyThoughtRoutes.js diff --git a/models/happyThought.js b/models/happyThought.js new file mode 100644 index 0000000..55f21cb --- /dev/null +++ b/models/happyThought.js @@ -0,0 +1,13 @@ +import mongoose, { Schema } from "mongoose"; + +const Schema = mongoose.Schema; + +const HappyThoughtsSchema = new Schema({ + _id: String, + message: String, + hearts: Number, + createdAt: String, + __v: Number, +}); + +export const HappyThoughts = mongoose.model("HappyThoughts", HappyThoughtsSchema); diff --git a/routes/happyThoughtRoutes.js b/routes/happyThoughtRoutes.js new file mode 100644 index 0000000..3d7219d --- /dev/null +++ b/routes/happyThoughtRoutes.js @@ -0,0 +1,119 @@ +import express from "express"; +import { happyThought } from "../models/happyThought.js"; +import { authenticateUser } from "../middleware/authenticateUser.js"; + +const router = express.Router(); + +app.get("/api/thoughts", async (req, res) => { + if (req.query.minHearts) { + const minHearts = parseInt(req.query.minHearts); + const filteredThoughts = await HappyThoughts.find({ + hearts: { $gte: minHearts }, + }); //greater than or equal to + return res.json(filteredThoughts); + } + try { + const thoughts = await HappyThoughts.find(); + res.json(thoughts); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +app.get("/api/thoughts/:id", async (req, res) => { + const id = req.params.id; + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ error: "Invalid id" }); // will check if the id is valid + } + try { + const thought = await HappyThoughts.findById(id); + if (!thought) { + return res.status(404).json({ error: "Thought not found" }); + } + return res.json(thought); + } catch (err) { + return res.status(500).json({ error: err.message }); + } +}); + +app.post("/api/thoughts", async (req, res) => { + const body = req.body; + if (!body.message || body.message.length < 5 || body.message.length > 140) { + return res.status(400).json({ + error: "Message is required and must be between 5 and 140 characters", // will validate the message length or if it's empty + }); + } + const newThought = { + _id: new mongoose.Types.ObjectId().toString(), + message: body.message, + hearts: 0, + createdAt: new Date().toISOString(), + __v: 0, + }; + await HappyThoughts.create(newThought); + // data.push(newThought); + res.status(201).json(newThought); +}); + +app.put("/api/thoughts/:id", async (req, res) => { + const id = req.params.id; + const { message } = req.body; + if (!message || message.length < 5 || message.length > 140) { + return res.status(400).json({ + error: "Message is required and must be between 5 and 140 characters", + }); + } + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ error: "Invalid id" }); + } + try { + const thought = await HappyThoughts.findByIdAndUpdate( + id, + { $set: { message: message } }, + { new: true }, + ); + if (!thought) { + return res.status(404).json({ error: "Thought not found" }); + } + return res.json(thought); + } catch (err) { + return res.status(500).json({ error: err.message }); + } +}); + +app.delete("/api/thoughts/:id", async (req, res) => { + const id = req.params.id; + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ error: "Invalid id" }); + } + try { + const thought = await HappyThoughts.findByIdAndDelete(id); + if (!thought) { + return res.status(404).json({ error: "Thought not found" }); + } + return res.json({ message: "Thought deleted successfully" }); + } catch (err) { + return res.status(500).json({ error: err.message }); + } +}); + +app.post("/api/thoughts/:id/like", async (req, res) => { + const id = req.params.id; + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ error: "Invalid id" }); + } + try { + const thought = await HappyThoughts.findByIdAndUpdate( + id, + { $inc: { hearts: 1 } } + ); + if (!thought) { + return res.status(404).json({ error: "Thought not found" }); + } + return res.json(thought); + } catch (err) { + return res.status(500).json({ error: err.message }); + } +}); + +export default router; \ No newline at end of file From 34970e501e09bf896bf663310d78fdcc2b26f67d Mon Sep 17 00:00:00 2001 From: Arta Kjato Date: Thu, 5 Feb 2026 11:42:17 +0100 Subject: [PATCH 15/21] debug and update userRoutes --- routes/userRoutes.js | 56 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/routes/userRoutes.js b/routes/userRoutes.js index cb8ab6a..f36dfa9 100644 --- a/routes/userRoutes.js +++ b/routes/userRoutes.js @@ -8,17 +8,34 @@ router.post('/signup', async (req, res) => { try { const { email, password } = req.body; - const existingUser = await User.findOne({ email: email.toLowerCase() }); + if (!email || !password) { + return res.status(400).json({ + success: false, + error: 'Email and password are required', + }); + } + + if (password.length < 8) { + return res.status(400).json({ + success: false, + error: 'Password must be at least 8 characters long', + }); + } + + const normalizedEmail = email.toLowerCase().trim(); + + const existingUser = await User.findOne({ email: normalizedEmail }); if (existingUser) { - return res.status(400).json({ + return res.status(409).json({ success: false, error: 'Email already in use' }); } - const salt = bcrypt.genSalt(); + const salt = await bcrypt.genSalt(); const hashedPassword = await bcrypt.hash(password, salt); - const user = new User({ email, password: hashedPassword }); + + const user = new User({ email: normalizedEmail, password: hashedPassword }); await user.save(); return res.status(201).json({ @@ -31,10 +48,10 @@ router.post('/signup', async (req, res) => { }, }); } catch (error) { - res.status(400).json({ + return res.status(400).json({ success: false, - message: 'Could not create user', - response: error, + message: 'Sorry, I could not create a user', + response: error.message, }); } }); @@ -43,10 +60,21 @@ router.post('/login', async (req, res) => { try { const { email, password } = req.body; - const user = await User.findOne({ email: email.toLowerCase() }); + if (!email || !password) { + return res.status(400).json({ + success: false, + error: 'Email and password are required', + }); + } + + const normalizedEmail = email.toLowerCase().trim(); + + const user = await User.findOne({ email: normalizedEmail }); + + const passwordMatch = user ? await bcrypt.compare(password, user.password) : false; - if (user && bcrypt.compareSync(password, user.password)) { - res.json({ + if (user && passwordMatch) { + return res.json({ success: true, message: 'Login successful', response: { @@ -55,11 +83,13 @@ router.post('/login', async (req, res) => { accessToken: user.accessToken, }, }); - } else { - res.status(401).json({ + } + + else { + return res.status(401).json({ success: false, message: 'Invalid email or password', - response: null, + info: 'https://http.dog/401', }); } } catch (error) { From b5f71b75ba9628c448a47c39d812b24c697a814c Mon Sep 17 00:00:00 2001 From: Arta Kjato Date: Thu, 5 Feb 2026 13:14:03 +0100 Subject: [PATCH 16/21] update backend and clear some bugs --- middleware/authMiddleware.js | 30 ++++++++ models/User.js | 11 ++- models/happyThought.js | 30 +++++--- routes/happyThoughtRoutes.js | 95 +++++++++++++++---------- server.js | 131 ++--------------------------------- 5 files changed, 123 insertions(+), 174 deletions(-) create mode 100644 middleware/authMiddleware.js diff --git a/middleware/authMiddleware.js b/middleware/authMiddleware.js new file mode 100644 index 0000000..e797ffa --- /dev/null +++ b/middleware/authMiddleware.js @@ -0,0 +1,30 @@ +import { User } from "../models/User.js"; + +export const authenticateUser = async (req, res, next) => { + try { + const authHeader = req.header("Authorization"); + + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return res.status(401).json({ + message: "Authentication missing or invalid.", + loggedOut: true, + }); + } + + const token = authHeader.replace("Bearer ", "").trim(); + const user = await User.findOne({ accessToken: token }); + + if (!user) { + return res.status(401).json({ + message: "Authentication missing or invalid.", + loggedOut: true, + }); + } + req.user = user; + return next(); + } catch (err) { + return res.status(500).json({ + message: "Internal server error", + error: err.message }); + } +}; \ No newline at end of file diff --git a/models/User.js b/models/User.js index 0dd904d..7291ac6 100644 --- a/models/User.js +++ b/models/User.js @@ -1,20 +1,25 @@ -import mongoose, { Schema } from "mongoose"; +import mongoose from "mongoose"; import crypto from "crypto"; -const UserSchema = new Schema({ +const UserSchema = new mongoose.Schema({ email: { type: String, required: true, unique: true, + lowercase: true, + trim:true, }, password: { type: String, required: true, + minlength: 6, }, accessToken: { type: String, default: () => crypto.randomBytes(128).toString("hex"), }, -}); +}, +{ timestamps: true } +); export const User = mongoose.model("User", UserSchema); \ No newline at end of file diff --git a/models/happyThought.js b/models/happyThought.js index 55f21cb..9fd5906 100644 --- a/models/happyThought.js +++ b/models/happyThought.js @@ -1,13 +1,25 @@ -import mongoose, { Schema } from "mongoose"; +import mongoose from "mongoose"; -const Schema = mongoose.Schema; - -const HappyThoughtsSchema = new Schema({ - _id: String, - message: String, - hearts: Number, - createdAt: String, - __v: Number, +const HappyThoughtsSchema = new mongoose.Schema({ + message:{ + type: String, + required: true, + minlength: 5, + maxlength: 140, + }, + hearts:{ + type: Number, + default: 0, + }, + createdAt:{ + type: Date, + default: Date.now, + }, + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + }, }); export const HappyThoughts = mongoose.model("HappyThoughts", HappyThoughtsSchema); diff --git a/routes/happyThoughtRoutes.js b/routes/happyThoughtRoutes.js index 3d7219d..5c85177 100644 --- a/routes/happyThoughtRoutes.js +++ b/routes/happyThoughtRoutes.js @@ -1,27 +1,31 @@ import express from "express"; -import { happyThought } from "../models/happyThought.js"; +import mongoose from "mongoose"; +import { HappyThoughts } from "../models/happyThought.js"; import { authenticateUser } from "../middleware/authenticateUser.js"; const router = express.Router(); -app.get("/api/thoughts", async (req, res) => { +router.get("/", async (req, res) => { + try { + const query = {}; + if (req.query.minHearts) { - const minHearts = parseInt(req.query.minHearts); - const filteredThoughts = await HappyThoughts.find({ - hearts: { $gte: minHearts }, - }); //greater than or equal to - return res.json(filteredThoughts); + const minHearts = Number(req.query.minHearts); + if (Number.isNaN(minHearts)) { + return res.status(400).json({ error: "minHearts must be a number" }); + } + query.hearts = { $gte: minHearts }; } - try { - const thoughts = await HappyThoughts.find(); - res.json(thoughts); + + const thoughts = await HappyThoughts.find(query).sort({ createdAt: -1 });//greater than or equal to + return res.json(thoughts); } catch (err) { res.status(500).json({ error: err.message }); } }); -app.get("/api/thoughts/:id", async (req, res) => { - const id = req.params.id; +router.get("/:id", async (req, res) => { + const { id } = req.params; if (!mongoose.Types.ObjectId.isValid(id)) { return res.status(400).json({ error: "Invalid id" }); // will check if the id is valid } @@ -36,80 +40,95 @@ app.get("/api/thoughts/:id", async (req, res) => { } }); -app.post("/api/thoughts", async (req, res) => { - const body = req.body; - if (!body.message || body.message.length < 5 || body.message.length > 140) { +router.post("/", authenticateUser, async (req, res) => { + const { message } = req.body; + if (!message || message.length < 5 || message.length > 140) { return res.status(400).json({ error: "Message is required and must be between 5 and 140 characters", // will validate the message length or if it's empty }); + } try { + const newThought = await HappyThoughts.create({ + message, + userId: req.user.id, + }); + return res.status(201).json(newThought); + } catch (err) { + return res.status(500).json({ error: err.message }); } - const newThought = { - _id: new mongoose.Types.ObjectId().toString(), - message: body.message, - hearts: 0, - createdAt: new Date().toISOString(), - __v: 0, - }; - await HappyThoughts.create(newThought); - // data.push(newThought); - res.status(201).json(newThought); }); -app.put("/api/thoughts/:id", async (req, res) => { - const id = req.params.id; +router.put("/:id", authenticateUser,async (req, res) => { + const { id } = req.params; const { message } = req.body; + if (!message || message.length < 5 || message.length > 140) { return res.status(400).json({ error: "Message is required and must be between 5 and 140 characters", }); } + if (!mongoose.Types.ObjectId.isValid(id)) { return res.status(400).json({ error: "Invalid id" }); } + try { - const thought = await HappyThoughts.findByIdAndUpdate( - id, - { $set: { message: message } }, - { new: true }, - ); + const thought = await HappyThoughts.findById(id) if (!thought) { return res.status(404).json({ error: "Thought not found" }); } + if (thought.userId.toString() !== req.user.id) { + return res.status(403).json({ error: "Unauthorized to update this thought" }); + } + + thought.message = message; + await thought.save(); + return res.json(thought); } catch (err) { return res.status(500).json({ error: err.message }); } }); -app.delete("/api/thoughts/:id", async (req, res) => { - const id = req.params.id; +router.delete("/:id", authenticateUser, async (req, res) => { + const { id } = req.params; + if (!mongoose.Types.ObjectId.isValid(id)) { return res.status(400).json({ error: "Invalid id" }); } + try { - const thought = await HappyThoughts.findByIdAndDelete(id); + const thought = await HappyThoughts.findById(id); if (!thought) { return res.status(404).json({ error: "Thought not found" }); } + if (thought.userId.toString() !== req.user.id) { + return res.status(403).json({ error: "Unauthorized to delete this thought" }); + } + await HappyThoughts.findByIdAndDelete(id); return res.json({ message: "Thought deleted successfully" }); } catch (err) { return res.status(500).json({ error: err.message }); } }); -app.post("/api/thoughts/:id/like", async (req, res) => { - const id = req.params.id; +router.post("/:id/like", async (req, res) => { + const { id } = req.params; + if (!mongoose.Types.ObjectId.isValid(id)) { return res.status(400).json({ error: "Invalid id" }); } + try { const thought = await HappyThoughts.findByIdAndUpdate( id, - { $inc: { hearts: 1 } } + { $inc: { hearts: 1 } }, + { new: true } ); + if (!thought) { return res.status(404).json({ error: "Thought not found" }); } + return res.json(thought); } catch (err) { return res.status(500).json({ error: err.message }); diff --git a/server.js b/server.js index a8cac91..3e7f133 100644 --- a/server.js +++ b/server.js @@ -4,6 +4,9 @@ import expressListEndpoints from "express-list-endpoints"; import mongoose from "mongoose"; import "dotenv/config"; +import happyThoughtRoutes from "./routes/happyThoughtRoutes.js"; +import userRoutes from "./routes/userRoutes.js"; + //const mongoDB = "mongodb://localhost:27017/happythoughts"; const password = process.env.PASSWORD; const mongoDB = @@ -15,144 +18,24 @@ async function main() { await mongoose.connect(mongoDB); } -const Schema = mongoose.Schema; - -const HappyThoughtsSchema = new Schema({ - _id: String, - message: String, - hearts: Number, - createdAt: String, - __v: Number, -}); - -const HappyThoughts = mongoose.model("HappyThoughts", HappyThoughtsSchema); - const port = process.env.PORT || 8080; const app = express(); app.use(cors()); app.use(express.json()); + app.get("/", (req, res) => { //listing all available endpoints const endpoints = expressListEndpoints(app); res.json({ - message: "Welcome to the Thoughts API!", + message: "Welcome to the Happy Thoughts API!", endpoints: endpoints, }); }); -app.get("/api/thoughts", async (req, res) => { - if (req.query.minHearts) { - const minHearts = parseInt(req.query.minHearts); - const filteredThoughts = await HappyThoughts.find({ - hearts: { $gte: minHearts }, - }); //greater than or equal to - return res.json(filteredThoughts); - } - try { - const thoughts = await HappyThoughts.find(); - res.json(thoughts); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}); - -app.get("/api/thoughts/:id", async (req, res) => { - const id = req.params.id; - if (!mongoose.Types.ObjectId.isValid(id)) { - return res.status(400).json({ error: "Invalid id" }); // will check if the id is valid - } - try { - const thought = await HappyThoughts.findById(id); - if (!thought) { - return res.status(404).json({ error: "Thought not found" }); - } - return res.json(thought); - } catch (err) { - return res.status(500).json({ error: err.message }); - } -}); - -app.post("/api/thoughts", async (req, res) => { - const body = req.body; - if (!body.message || body.message.length < 5 || body.message.length > 140) { - return res.status(400).json({ - error: "Message is required and must be between 5 and 140 characters", // will validate the message length or if it's empty - }); - } - const newThought = { - _id: new mongoose.Types.ObjectId().toString(), - message: body.message, - hearts: 0, - createdAt: new Date().toISOString(), - __v: 0, - }; - await HappyThoughts.create(newThought); - // data.push(newThought); - res.status(201).json(newThought); -}); - -app.put("/api/thoughts/:id", async (req, res) => { - const id = req.params.id; - const { message } = req.body; - if (!message || message.length < 5 || message.length > 140) { - return res.status(400).json({ - error: "Message is required and must be between 5 and 140 characters", - }); - } - if (!mongoose.Types.ObjectId.isValid(id)) { - return res.status(400).json({ error: "Invalid id" }); - } - try { - const thought = await HappyThoughts.findByIdAndUpdate( - id, - { $set: { message: message } }, - { new: true }, - ); - if (!thought) { - return res.status(404).json({ error: "Thought not found" }); - } - return res.json(thought); - } catch (err) { - return res.status(500).json({ error: err.message }); - } -}); - -app.delete("/api/thoughts/:id", async (req, res) => { - const id = req.params.id; - if (!mongoose.Types.ObjectId.isValid(id)) { - return res.status(400).json({ error: "Invalid id" }); - } - try { - const thought = await HappyThoughts.findByIdAndDelete(id); - if (!thought) { - return res.status(404).json({ error: "Thought not found" }); - } - return res.json({ message: "Thought deleted successfully" }); - } catch (err) { - return res.status(500).json({ error: err.message }); - } -}); - -app.post("/api/thoughts/:id/like", async (req, res) => { - const id = req.params.id; - if (!mongoose.Types.ObjectId.isValid(id)) { - return res.status(400).json({ error: "Invalid id" }); - } - try { - const thought = await HappyThoughts.findByIdAndUpdate( - id, - { $inc: { hearts: 1 } } - ); - if (!thought) { - return res.status(404).json({ error: "Thought not found" }); - } - return res.json(thought); - } catch (err) { - return res.status(500).json({ error: err.message }); - } -}); +app.use("/api/thoughts", happyThoughtRoutes); +app.use("/api/users", userRoutes); // Start the server app.listen(port, () => { From dbbfeff8dacd0e1baeb819082955160d1c77c0ca Mon Sep 17 00:00:00 2001 From: Arta Kjato Date: Thu, 5 Feb 2026 15:33:26 +0100 Subject: [PATCH 17/21] fix a bug --- routes/happyThoughtRoutes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/happyThoughtRoutes.js b/routes/happyThoughtRoutes.js index 5c85177..3bd31c6 100644 --- a/routes/happyThoughtRoutes.js +++ b/routes/happyThoughtRoutes.js @@ -1,7 +1,7 @@ import express from "express"; import mongoose from "mongoose"; import { HappyThoughts } from "../models/happyThought.js"; -import { authenticateUser } from "../middleware/authenticateUser.js"; +import { authenticateUser } from "../middleware/authMiddle.js"; const router = express.Router(); From c87d7d49864521f3d77f6532f099735f420dc700 Mon Sep 17 00:00:00 2001 From: Arta Kjato Date: Thu, 5 Feb 2026 15:41:57 +0100 Subject: [PATCH 18/21] fix auth middleware import path --- routes/happyThoughtRoutes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/happyThoughtRoutes.js b/routes/happyThoughtRoutes.js index 3bd31c6..7a027dd 100644 --- a/routes/happyThoughtRoutes.js +++ b/routes/happyThoughtRoutes.js @@ -1,7 +1,7 @@ import express from "express"; import mongoose from "mongoose"; import { HappyThoughts } from "../models/happyThought.js"; -import { authenticateUser } from "../middleware/authMiddle.js"; +import { authenticateUser } from "../middleware/authMiddleware.js"; const router = express.Router(); From b09d86163f6146304e7b9c8e91ca8053b10008bc Mon Sep 17 00:00:00 2001 From: Arta Kjato Date: Thu, 5 Feb 2026 15:46:33 +0100 Subject: [PATCH 19/21] install bcrypt --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index e3f8894..5a68e4f 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", + "bcryptjs": "^3.0.3", "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^4.17.3", From 0c7564a25bcef0d72646682e897e3dc89df6b733 Mon Sep 17 00:00:00 2001 From: Arta Kjato Date: Thu, 5 Feb 2026 22:38:49 +0100 Subject: [PATCH 20/21] do some housekeeping ad update the README file --- README.md | 95 +++++++++++++++++++++++++++++++++--- middleware/authMiddleware.js | 11 +++-- models/User.js | 39 +++++++-------- models/happyThought.js | 11 +++-- routes/happyThoughtRoutes.js | 41 +++++++++------- routes/userRoutes.js | 88 ++++++++++++++++----------------- server.js | 2 - 7 files changed, 189 insertions(+), 98 deletions(-) diff --git a/README.md b/README.md index 0f9f073..cb971ae 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,94 @@ -# 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 is a RESTful API built with Node.js, Express, and MongoDB for a β€œHappy Thoughts” application. +Users can sign up, log in, post happy thoughts, like them, and manage their own posts. -## Getting started +The API is deployed on Render and connected to MongoDB Atlas. -Install dependencies with `npm install`, then start the server by running `npm run dev` +πŸš€ Features -## View it live + User authentication (signup & login) + + Secure password hashing + + Create, read, update, and delete thoughts + + Like thoughts + + Ownership protection (users can only edit/delete their own posts) + + API documentation endpoint + + Deployed online + +πŸ› οΈ Tech Stack + + Node.js + + Express.js + + MongoDB + Mongoose + + bcryptjs (password hashing) + + dotenv (environment variables) + + express-list-endpoints + +CORS + +🧠 Data Models +User Model + + email (unique) + + password (hashed) + + accessToken + +HappyThought Model + + message (5–140 chars) + + hearts (number) + + createdAt + + userId (reference to User) + +πŸ”’ Security + + Passwords are hashed using bcrypt + + Access tokens are stored securely + + Protected routes require authentication + + Users can only edit/delete their own posts + +❗ Error Handling + + The API returns meaningful status codes: + + 400 Bad Request + + 401 Unauthorized + + 403 Forbidden + + 404 Not Found + + 500 Server Error + +This project was built to practice: + +REST APIs + +Authentication + +MongoDB + +Full-stack integration + +Deployment -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. diff --git a/middleware/authMiddleware.js b/middleware/authMiddleware.js index e797ffa..321fab3 100644 --- a/middleware/authMiddleware.js +++ b/middleware/authMiddleware.js @@ -5,7 +5,7 @@ export const authenticateUser = async (req, res, next) => { const authHeader = req.header("Authorization"); if (!authHeader || !authHeader.startsWith("Bearer ")) { - return res.status(401).json({ + return res.status(401).json({ message: "Authentication missing or invalid.", loggedOut: true, }); @@ -23,8 +23,9 @@ export const authenticateUser = async (req, res, next) => { req.user = user; return next(); } catch (err) { - return res.status(500).json({ - message: "Internal server error", - error: err.message }); + return res.status(500).json({ + message: "Internal server error", + error: err.message, + }); } -}; \ No newline at end of file +}; diff --git a/models/User.js b/models/User.js index 7291ac6..f4bcad3 100644 --- a/models/User.js +++ b/models/User.js @@ -1,25 +1,26 @@ import mongoose from "mongoose"; import crypto from "crypto"; -const UserSchema = new mongoose.Schema({ - email: { - type: String, - required: true, - unique: true, - lowercase: true, - trim:true, +const UserSchema = new mongoose.Schema( + { + email: { + type: String, + required: true, + unique: true, + lowercase: true, + trim: true, + }, + password: { + type: String, + required: true, + minlength: 6, + }, + accessToken: { + type: String, + default: () => crypto.randomBytes(128).toString("hex"), + }, }, - password: { - type: String, - required: true, - minlength: 6, - }, - accessToken: { - type: String, - default: () => crypto.randomBytes(128).toString("hex"), - }, -}, -{ timestamps: true } + { timestamps: true }, ); -export const User = mongoose.model("User", UserSchema); \ No newline at end of file +export const User = mongoose.model("User", UserSchema); diff --git a/models/happyThought.js b/models/happyThought.js index 9fd5906..51f292e 100644 --- a/models/happyThought.js +++ b/models/happyThought.js @@ -1,17 +1,17 @@ import mongoose from "mongoose"; const HappyThoughtsSchema = new mongoose.Schema({ - message:{ + message: { type: String, required: true, minlength: 5, maxlength: 140, }, - hearts:{ + hearts: { type: Number, default: 0, }, - createdAt:{ + createdAt: { type: Date, default: Date.now, }, @@ -22,4 +22,7 @@ const HappyThoughtsSchema = new mongoose.Schema({ }, }); -export const HappyThoughts = mongoose.model("HappyThoughts", HappyThoughtsSchema); +export const HappyThoughts = mongoose.model( + "HappyThoughts", + HappyThoughtsSchema, +); diff --git a/routes/happyThoughtRoutes.js b/routes/happyThoughtRoutes.js index 7a027dd..3a5f313 100644 --- a/routes/happyThoughtRoutes.js +++ b/routes/happyThoughtRoutes.js @@ -9,15 +9,15 @@ router.get("/", async (req, res) => { try { const query = {}; - if (req.query.minHearts) { - const minHearts = Number(req.query.minHearts); - if (Number.isNaN(minHearts)) { - return res.status(400).json({ error: "minHearts must be a number" }); - } - query.hearts = { $gte: minHearts }; - } + if (req.query.minHearts) { + const minHearts = Number(req.query.minHearts); + if (Number.isNaN(minHearts)) { + return res.status(400).json({ error: "minHearts must be a number" }); + } + query.hearts = { $gte: minHearts }; + } - const thoughts = await HappyThoughts.find(query).sort({ createdAt: -1 });//greater than or equal to + const thoughts = await HappyThoughts.find(query).sort({ createdAt: -1 }); //greater than or equal to return res.json(thoughts); } catch (err) { res.status(500).json({ error: err.message }); @@ -46,10 +46,11 @@ router.post("/", authenticateUser, async (req, res) => { return res.status(400).json({ error: "Message is required and must be between 5 and 140 characters", // will validate the message length or if it's empty }); - } try { - const newThought = await HappyThoughts.create({ - message, - userId: req.user.id, + } + try { + const newThought = await HappyThoughts.create({ + message, + userId: req.user.id, }); return res.status(201).json(newThought); } catch (err) { @@ -57,7 +58,7 @@ router.post("/", authenticateUser, async (req, res) => { } }); -router.put("/:id", authenticateUser,async (req, res) => { +router.put("/:id", authenticateUser, async (req, res) => { const { id } = req.params; const { message } = req.body; @@ -72,12 +73,14 @@ router.put("/:id", authenticateUser,async (req, res) => { } try { - const thought = await HappyThoughts.findById(id) + const thought = await HappyThoughts.findById(id); if (!thought) { return res.status(404).json({ error: "Thought not found" }); } if (thought.userId.toString() !== req.user.id) { - return res.status(403).json({ error: "Unauthorized to update this thought" }); + return res + .status(403) + .json({ error: "Unauthorized to update this thought" }); } thought.message = message; @@ -102,7 +105,9 @@ router.delete("/:id", authenticateUser, async (req, res) => { return res.status(404).json({ error: "Thought not found" }); } if (thought.userId.toString() !== req.user.id) { - return res.status(403).json({ error: "Unauthorized to delete this thought" }); + return res + .status(403) + .json({ error: "Unauthorized to delete this thought" }); } await HappyThoughts.findByIdAndDelete(id); return res.json({ message: "Thought deleted successfully" }); @@ -122,7 +127,7 @@ router.post("/:id/like", async (req, res) => { const thought = await HappyThoughts.findByIdAndUpdate( id, { $inc: { hearts: 1 } }, - { new: true } + { new: true }, ); if (!thought) { @@ -135,4 +140,4 @@ router.post("/:id/like", async (req, res) => { } }); -export default router; \ No newline at end of file +export default router; diff --git a/routes/userRoutes.js b/routes/userRoutes.js index f36dfa9..9d6d912 100644 --- a/routes/userRoutes.js +++ b/routes/userRoutes.js @@ -1,69 +1,69 @@ -import express, { response } from 'express'; -import bcrypt from 'bcryptjs'; -import { User } from '../models/User.js'; +import express, { response } from "express"; +import bcrypt from "bcryptjs"; +import { User } from "../models/User.js"; const router = express.Router(); -router.post('/signup', async (req, res) => { +router.post("/signup", async (req, res) => { try { const { email, password } = req.body; if (!email || !password) { return res.status(400).json({ success: false, - error: 'Email and password are required', + error: "Email and password are required", }); } if (password.length < 8) { return res.status(400).json({ success: false, - error: 'Password must be at least 8 characters long', + error: "Password must be at least 8 characters long", }); } - const normalizedEmail = email.toLowerCase().trim(); + const normalizedEmail = email.toLowerCase().trim(); + + const existingUser = await User.findOne({ email: normalizedEmail }); + if (existingUser) { + return res.status(409).json({ + success: false, + error: "Email already in use", + }); + } - const existingUser = await User.findOne({ email: normalizedEmail }); - if (existingUser) { - return res.status(409).json({ + const salt = await bcrypt.genSalt(); + const hashedPassword = await bcrypt.hash(password, salt); + + const user = new User({ email: normalizedEmail, password: hashedPassword }); + await user.save(); + + return res.status(201).json({ + success: true, + message: "User created successfully", + response: { + email: user.email, + id: user._id, + accessToken: user.accessToken, + }, + }); + } catch (error) { + return res.status(400).json({ success: false, - error: 'Email already in use' + message: "Sorry, I could not create a user", + response: error.message, }); } - - const salt = await bcrypt.genSalt(); - const hashedPassword = await bcrypt.hash(password, salt); - - const user = new User({ email: normalizedEmail, password: hashedPassword }); - await user.save(); - - return res.status(201).json({ - success: true, - message: 'User created successfully', - response: { - email: user.email, - id: user._id, - accessToken: user.accessToken, - }, - }); -} catch (error) { - return res.status(400).json({ - success: false, - message: 'Sorry, I could not create a user', - response: error.message, - }); -} }); -router.post('/login', async (req, res) => { +router.post("/login", async (req, res) => { try { const { email, password } = req.body; if (!email || !password) { return res.status(400).json({ success: false, - error: 'Email and password are required', + error: "Email and password are required", }); } @@ -71,31 +71,31 @@ router.post('/login', async (req, res) => { const user = await User.findOne({ email: normalizedEmail }); - const passwordMatch = user ? await bcrypt.compare(password, user.password) : false; + const passwordMatch = user + ? await bcrypt.compare(password, user.password) + : false; if (user && passwordMatch) { return res.json({ success: true, - message: 'Login successful', + message: "Login successful", response: { email: user.email, id: user._id, accessToken: user.accessToken, }, }); - } - - else { + } else { return res.status(401).json({ success: false, - message: 'Invalid email or password', - info: 'https://http.dog/401', + message: "Invalid email or password", + info: "https://http.dog/401", }); } } catch (error) { res.status(500).json({ success: false, - message: 'Login failed', + message: "Login failed", response: error, }); } diff --git a/server.js b/server.js index 3e7f133..d04c9c9 100644 --- a/server.js +++ b/server.js @@ -7,7 +7,6 @@ import "dotenv/config"; import happyThoughtRoutes from "./routes/happyThoughtRoutes.js"; import userRoutes from "./routes/userRoutes.js"; -//const mongoDB = "mongodb://localhost:27017/happythoughts"; const password = process.env.PASSWORD; const mongoDB = process.env.MONGO_URL || @@ -24,7 +23,6 @@ const app = express(); app.use(cors()); app.use(express.json()); - app.get("/", (req, res) => { //listing all available endpoints const endpoints = expressListEndpoints(app); From ad9f2f6311b0a176b73e736dd3a4d6e48286230a Mon Sep 17 00:00:00 2001 From: Arta Kjato Date: Thu, 5 Feb 2026 22:40:44 +0100 Subject: [PATCH 21/21] tiny update to README --- README.md | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/README.md b/README.md index cb971ae..cb3c23c 100644 --- a/README.md +++ b/README.md @@ -8,31 +8,19 @@ The API is deployed on Render and connected to MongoDB Atlas. πŸš€ Features User authentication (signup & login) - Secure password hashing - Create, read, update, and delete thoughts - Like thoughts - Ownership protection (users can only edit/delete their own posts) - API documentation endpoint - Deployed online πŸ› οΈ Tech Stack - Node.js - Express.js - MongoDB + Mongoose - bcryptjs (password hashing) - dotenv (environment variables) - express-list-endpoints CORS @@ -41,54 +29,37 @@ CORS User Model email (unique) - password (hashed) - accessToken HappyThought Model message (5–140 chars) - hearts (number) - createdAt - userId (reference to User) πŸ”’ Security Passwords are hashed using bcrypt - Access tokens are stored securely - Protected routes require authentication - Users can only edit/delete their own posts ❗ Error Handling The API returns meaningful status codes: - 400 Bad Request - 401 Unauthorized - 403 Forbidden - 404 Not Found - 500 Server Error This project was built to practice: REST APIs - Authentication - MongoDB - Full-stack integration - Deployment