From 13843042ff8a5a46ae322586bf2348ea09ce1e16 Mon Sep 17 00:00:00 2001 From: "Leon.Ekelund" Date: Wed, 4 Feb 2026 00:34:56 +0100 Subject: [PATCH 1/4] Add thoughts endpoints and API documentation --- data.json | 10 ++++----- models/Thought.js | 20 +++++++++++++++++ package.json | 4 ++++ server.js | 57 +++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 84 insertions(+), 7 deletions(-) create mode 100644 models/Thought.js diff --git a/data.json b/data.json index a2c844f..62b0fd7 100644 --- a/data.json +++ b/data.json @@ -1,5 +1,5 @@ [ - { + { "_id": "682bab8c12155b00101732ce", "message": "Berlin baby", "hearts": 37, @@ -7,7 +7,7 @@ "__v": 0 }, { - "_id": "682e53cc4fddf50010bbe739", + "_id": "682e53cc4fddf50010bbe739", "message": "My family!", "hearts": 0, "createdAt": "2025-05-22T22:29:32.232Z", @@ -25,7 +25,7 @@ "message": "Newly washed bedlinen, kids that sleeps through the night.. FINGERS CROSSED 🤞🏼\n", "hearts": 6, "createdAt": "2025-05-21T21:42:23.862Z", - "__v": 0 + "__v": 0 }, { "_id": "682e45804fddf50010bbe736", @@ -53,7 +53,7 @@ "message": "A god joke: \nWhy did the scarecrow win an award?\nBecause he was outstanding in his field!", "hearts": 12, "createdAt": "2025-05-20T20:54:51.082Z", - "__v": 0 + "__v": 0 }, { "_id": "682cebbe17487d0010a298b5", @@ -74,7 +74,7 @@ "message": "Summer is coming...", "hearts": 2, "createdAt": "2025-05-20T15:03:22.379Z", - "__v": 0 + "__v": 0 }, { "_id": "682c706c951f7a0017130024", diff --git a/models/Thought.js b/models/Thought.js new file mode 100644 index 0000000..39367c0 --- /dev/null +++ b/models/Thought.js @@ -0,0 +1,20 @@ +import mongoose from "mongoose" + +const ThoughtSchema = new mongoose.Schema({ + message: { + type: String, + required: [true, "Message is required"], + minlength: 5, + maxlength: 140 + }, + hearts: { + type: Number, + default: 0 + }, + createdAt: { + type: Date, + default: () => new Date() + } +}) + +export const Thought = mongoose.model("Thought", ThoughtSchema) diff --git a/package.json b/package.json index bf25bb6..c9d5530 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,11 @@ "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", "cors": "^2.8.5", + "dotenv": "^16.3.1", "express": "^4.17.3", + "express-list-endpoints": "^7.1.1", + "mongodb": "^7.0.0", + "mongoose": "^9.1.5", "nodemon": "^3.0.1" } } diff --git a/server.js b/server.js index f47771b..ff6fa39 100644 --- a/server.js +++ b/server.js @@ -1,5 +1,16 @@ import cors from "cors" import express from "express" +import mongoose from "mongoose" +import { Thought } from "./models/Thought" +import listEndpoints from "express-list-endpoints" + +//remove later +import dotenv from "dotenv" +dotenv.config() + +const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/happy-thoughts" +mongoose.connect(mongoUrl) +mongoose.Promise = Promise // Defines the port the app will run on. Defaults to 8080, but can be overridden // when starting the server. Example command to overwrite PORT env variable value: @@ -11,9 +22,51 @@ const app = express() app.use(cors()) app.use(express.json()) -// Start defining your routes here +// root - shows all available endpoints app.get("/", (req, res) => { - res.send("Hello Technigo!") + res.json(listEndpoints(app)) +}) + +// get all thoughts +app.get("/thoughts", async (req, res) => { + const thoughts = await Thought.find() + res.json(thoughts) +}) + +// create a new thought +app.post("/thoughts", async (req, res) => { + const { message } = req.body + const thought = await Thought.create({ message }) + res.status(201).json(thought) +}) + +// get single thought +app.get("/thoughts/:id", async (req, res) => { + const { id } = req.params + const thought = await Thought.findById(id) + res.json(thought) +}) + +// update a thought +app.put("/thoughts/:id", async (req, res) => { + const { id } = req.params + const { message } = req.body + const thought = await Thought.findByIdAndUpdate(id, { message }, { new: true }) + res.json(thought) +}) + +// delete a thought +app.delete("/thoughts/:id", async (req, res) => { + const { id } = req.params + await Thought.findByIdAndDelete(id) + res.status(204).send() +}) + +// like a thought +app.post("/thoughts/:id/like", async (req, res) => { + const { id } = req.params + const thought = await Thought.findByIdAndUpdate(id, { $inc: { hearts: 1 } }, { new: true }) + res.json(thought) }) // Start the server From 37b0cc1f77ff854180ca6fc24b5f2597f89be65d Mon Sep 17 00:00:00 2001 From: "Leon.Ekelund" Date: Thu, 5 Feb 2026 10:39:23 +0100 Subject: [PATCH 2/4] add error handling --- server.js | 72 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 18 deletions(-) diff --git a/server.js b/server.js index ff6fa39..b8ebe1b 100644 --- a/server.js +++ b/server.js @@ -29,44 +29,80 @@ app.get("/", (req, res) => { // get all thoughts app.get("/thoughts", async (req, res) => { - const thoughts = await Thought.find() - res.json(thoughts) + try { + const thoughts = await Thought.find() + res.json(thoughts) + } catch (error) { + res.status(500).json({ error: "Could not fetch thoughts" }) + } }) // create a new thought app.post("/thoughts", async (req, res) => { - const { message } = req.body - const thought = await Thought.create({ message }) - res.status(201).json(thought) + try { + const { message } = req.body + const thought = await Thought.create({ message }) + res.status(201).json(thought) + } catch (error) { + res.status(400).json({ error: error.message }) + } }) // get single thought app.get("/thoughts/:id", async (req, res) => { - const { id } = req.params - const thought = await Thought.findById(id) - res.json(thought) + try { + const { id } = req.params + const thought = await Thought.findById(id) + if (!thought) { + return res.status(404).json({ error: "Thought not found" }) + } + res.json(thought) + } catch (error) { + res.status(400).json({ error: "Invalid ID format" }) + } }) // update a thought app.put("/thoughts/:id", async (req, res) => { - const { id } = req.params - const { message } = req.body - const thought = await Thought.findByIdAndUpdate(id, { message }, { new: true }) - res.json(thought) + try { + const { id } = req.params + const { message } = req.body + const thought = await Thought.findByIdAndUpdate(id, { message }, { new: true }) + if (!thought) { + return res.status(404).json({ error: "Thought not found" }) + } + res.json(thought) + } catch (error) { + res.status(400).json({ error: error.message }) + } }) // delete a thought app.delete("/thoughts/:id", async (req, res) => { - const { id } = req.params - await Thought.findByIdAndDelete(id) - res.status(204).send() + try { + const { id } = req.params + const thought = await Thought.findByIdAndDelete(id) + if (!thought) { + return res.status(404).json({ error: "Thought not found" }) + } + res.status(204).send() + } catch (error) { + res.status(400).json({ error: "Invalid ID format" }) + } }) // like a thought app.post("/thoughts/:id/like", async (req, res) => { - const { id } = req.params - const thought = await Thought.findByIdAndUpdate(id, { $inc: { hearts: 1 } }, { new: true }) - res.json(thought) + try { + const { id } = req.params + const thought = await Thought.findByIdAndUpdate(id, { $inc: { hearts: 1 } }, { new: true }) + if (!thought) { + return res.status(404).json({ error: "Thought not found" }) + } + res.json(thought) + } catch (error) { + res.status(400).json({ error: "Invalid ID format" }) + } }) // Start the server From d32c13027bcfb6d8a055943160c38c0be0257d69 Mon Sep 17 00:00:00 2001 From: "Leon.Ekelund" Date: Sun, 8 Feb 2026 22:11:33 +0100 Subject: [PATCH 3/4] Add login and authentication logic --- middleware/auth.js | 22 +++++++++++ models/Thought.js | 5 +++ models/User.js | 32 +++++++++++++++ package.json | 2 + server.js | 97 +++++++++++++++++++++++++++++++++++++--------- 5 files changed, 139 insertions(+), 19 deletions(-) create mode 100644 middleware/auth.js create mode 100644 models/User.js diff --git a/middleware/auth.js b/middleware/auth.js new file mode 100644 index 0000000..93045ec --- /dev/null +++ b/middleware/auth.js @@ -0,0 +1,22 @@ +import jwt from "jsonwebtoken" +import { User } from "../models/User" + +export const authenticateUser = async (req, res, next) => { + try { + const token = req.header("Authorization")?.replace("Bearer ", "") + if (!token) { + return res.status(401).json({ error: "Access denied. No token provided." }) + } + + const decoded = jwt.verify(token, process.env.JWT_SECRET) + const user = await User.findById(decoded.id) + if (!user) { + return res.status(401).json({ error: "Invalid token." }) + } + + req.user = user + next() + } catch (error) { + res.status(401).json({ error: "Invalid token." }) + } +} diff --git a/models/Thought.js b/models/Thought.js index 39367c0..dff054d 100644 --- a/models/Thought.js +++ b/models/Thought.js @@ -11,6 +11,11 @@ const ThoughtSchema = new mongoose.Schema({ type: Number, default: 0 }, + user: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true + }, createdAt: { type: Date, default: () => new Date() diff --git a/models/User.js b/models/User.js new file mode 100644 index 0000000..03ee705 --- /dev/null +++ b/models/User.js @@ -0,0 +1,32 @@ +import mongoose from "mongoose" +import bcrypt from "bcrypt" + +const UserSchema = new mongoose.Schema({ + username: { + type: String, + required: [true, "Username is required"], + unique: true, + minlength: 3, + maxlength: 20 + }, + email: { + type: String, + required: [true, "Email is required"], + unique: true, + match: [/^\S+@\S+\.\S+$/, "Please enter a valid email"] + }, + password: { + type: String, + required: [true, "Password is required"], + minlength: 6 + } +}, { timestamps: true }) + +// Hash password before saving +UserSchema.pre("save", async function () { + if (this.isModified("password")) { + this.password = await bcrypt.hash(this.password, 10) + } +}) + +export const User = mongoose.model("User", UserSchema) diff --git a/package.json b/package.json index c9d5530..c248736 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,12 @@ "@babel/core": "^7.17.9", "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", + "bcrypt": "^6.0.0", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.17.3", "express-list-endpoints": "^7.1.1", + "jsonwebtoken": "^9.0.3", "mongodb": "^7.0.0", "mongoose": "^9.1.5", "nodemon": "^3.0.1" diff --git a/server.js b/server.js index b8ebe1b..2fb1b32 100644 --- a/server.js +++ b/server.js @@ -1,10 +1,13 @@ import cors from "cors" import express from "express" import mongoose from "mongoose" +import bcrypt from "bcrypt" +import jwt from "jsonwebtoken" import { Thought } from "./models/Thought" +import { User } from "./models/User" +import { authenticateUser } from "./middleware/auth" import listEndpoints from "express-list-endpoints" -//remove later import dotenv from "dotenv" dotenv.config() @@ -12,13 +15,9 @@ const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/happy-thoughts" mongoose.connect(mongoUrl) mongoose.Promise = Promise -// Defines the port the app will run on. Defaults to 8080, but can be overridden -// when starting the server. Example command to overwrite PORT env variable value: -// PORT=9000 npm start const port = process.env.PORT || 8080 const app = express() -// Add middlewares to enable cors and json body parsing app.use(cors()) app.use(express.json()) @@ -27,22 +26,72 @@ app.get("/", (req, res) => { res.json(listEndpoints(app)) }) +// ---- AUTH ROUTES ---- + +// register +app.post("/register", async (req, res) => { + try { + const { username, email, password } = req.body + const user = await User.create({ username, email, password }) + const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, { expiresIn: "7d" }) + res.status(201).json({ + id: user._id, + username: user.username, + email: user.email, + token + }) + } catch (error) { + if (error.code === 11000) { + res.status(400).json({ error: "Username or email already exists" }) + } else { + res.status(400).json({ error: error.message }) + } + } +}) + +// login +app.post("/login", async (req, res) => { + try { + const { email, password } = req.body + const user = await User.findOne({ email }) + if (!user) { + return res.status(401).json({ error: "Invalid email or password" }) + } + const isMatch = await bcrypt.compare(password, user.password) + if (!isMatch) { + return res.status(401).json({ error: "Invalid email or password" }) + } + const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, { expiresIn: "7d" }) + res.json({ + id: user._id, + username: user.username, + email: user.email, + token + }) + } catch (error) { + res.status(500).json({ error: "Something went wrong" }) + } +}) + +// ---- THOUGHT ROUTES ---- + // get all thoughts app.get("/thoughts", async (req, res) => { try { - const thoughts = await Thought.find() + const thoughts = await Thought.find().populate("user", "username").sort({ createdAt: -1 }) res.json(thoughts) } catch (error) { res.status(500).json({ error: "Could not fetch thoughts" }) } }) -// create a new thought -app.post("/thoughts", async (req, res) => { +// create a new thought (authenticated) +app.post("/thoughts", authenticateUser, async (req, res) => { try { const { message } = req.body - const thought = await Thought.create({ message }) - res.status(201).json(thought) + const thought = await Thought.create({ message, user: req.user._id }) + const populatedThought = await thought.populate("user", "username") + res.status(201).json(populatedThought) } catch (error) { res.status(400).json({ error: error.message }) } @@ -52,7 +101,7 @@ app.post("/thoughts", async (req, res) => { app.get("/thoughts/:id", async (req, res) => { try { const { id } = req.params - const thought = await Thought.findById(id) + const thought = await Thought.findById(id).populate("user", "username") if (!thought) { return res.status(404).json({ error: "Thought not found" }) } @@ -62,29 +111,39 @@ app.get("/thoughts/:id", async (req, res) => { } }) -// update a thought -app.put("/thoughts/:id", async (req, res) => { +// update a thought (authenticated, only creator) +app.put("/thoughts/:id", authenticateUser, async (req, res) => { try { const { id } = req.params const { message } = req.body - const thought = await Thought.findByIdAndUpdate(id, { message }, { new: true }) + const thought = await Thought.findById(id) if (!thought) { return res.status(404).json({ error: "Thought not found" }) } - res.json(thought) + if (thought.user.toString() !== req.user._id.toString()) { + return res.status(403).json({ error: "You can only edit your own thoughts" }) + } + thought.message = message + await thought.save() + const updatedThought = await thought.populate("user", "username") + res.json(updatedThought) } catch (error) { res.status(400).json({ error: error.message }) } }) -// delete a thought -app.delete("/thoughts/:id", async (req, res) => { +// delete a thought (authenticated, only creator) +app.delete("/thoughts/:id", authenticateUser, async (req, res) => { try { const { id } = req.params - const thought = await Thought.findByIdAndDelete(id) + const thought = await Thought.findById(id) if (!thought) { return res.status(404).json({ error: "Thought not found" }) } + if (thought.user.toString() !== req.user._id.toString()) { + return res.status(403).json({ error: "You can only delete your own thoughts" }) + } + await thought.deleteOne() res.status(204).send() } catch (error) { res.status(400).json({ error: "Invalid ID format" }) @@ -95,7 +154,7 @@ app.delete("/thoughts/:id", async (req, res) => { app.post("/thoughts/:id/like", async (req, res) => { try { const { id } = req.params - const thought = await Thought.findByIdAndUpdate(id, { $inc: { hearts: 1 } }, { new: true }) + const thought = await Thought.findByIdAndUpdate(id, { $inc: { hearts: 1 } }, { new: true }).populate("user", "username") if (!thought) { return res.status(404).json({ error: "Thought not found" }) } From 9f4e7e6161a22eb0ad5d943f2d547a1f9f4120ca Mon Sep 17 00:00:00 2001 From: Leon Ekelund <102791497+LeonEkelund@users.noreply.github.com> Date: Mon, 9 Feb 2026 00:46:24 +0100 Subject: [PATCH 4/4] Update README to include only project links Removed project description and getting started instructions from README. --- README.md | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 0f9f073..1f7f61a 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,4 @@ -# Project API - -This project includes the packages and babel setup for an express server, and is just meant to make things a little simpler to get up and running with. - -## Getting started - -Install dependencies with `npm install`, then start the server by running `npm run dev` - -## View it live - -Every project should be deployed somewhere. Be sure to include the link to the deployed project so that the viewer can click around and see what it's all about. +Link to frontend: +https://leonhappythoughts.netlify.app/ +Link to API: +https://happythoughtsapi.onrender.com