From 3580eb2d5139a7379ec63a82e6cf00fd97ccf18c Mon Sep 17 00:00:00 2001 From: AgnesSj01 Date: Tue, 20 Jan 2026 09:28:35 +0100 Subject: [PATCH 1/2] Add Happy Thoughts API with sorting, filtering and paginaton --- package.json | 2 +- server.js | 80 +++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 71 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index bf25bb6..6cbc67d 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", "cors": "^2.8.5", - "express": "^4.17.3", + "express": "^4.22.1", "nodemon": "^3.0.1" } } diff --git a/server.js b/server.js index f47771b..f48dcb7 100644 --- a/server.js +++ b/server.js @@ -1,22 +1,82 @@ -import cors from "cors" -import express from "express" +import cors from "cors"; +import express from "express"; +import thoughtsData from "./data.json"; // Defines the port the app will run on. Defaults to 8080, but can be overridden // when starting the server. Example command to overwrite PORT env variable value: // PORT=9000 npm start -const port = process.env.PORT || 8080 -const app = express() +const port = process.env.PORT || 8080; +const app = express(); // Add middlewares to enable cors and json body parsing -app.use(cors()) -app.use(express.json()) +app.use(cors()); +app.use(express.json()); // Start defining your routes here + +//Documentation app.get("/", (req, res) => { - res.send("Hello Technigo!") -}) + res.json({ + message: "Welcome to Happy Thoughts API", + endpoints: [ + { method: "GET", path: "/", description: "API documentation" }, + { method: "GET", path: "/thoughts", description: "Get all thoughts" }, + { + method: "GET", + path: "/thoughts/:id", + description: "Get a specific thought by ID", + }, + ], + }); +}); + +//All Thoughts (optional sorting with ?sort=hearts) +app.get("/thoughts", (req, res) => { + const sort = req.query.sort; + const minHearts = req.query.minHearts; + let results = [...thoughtsData]; //copy the array + + //Paginering + const page = Number(req.query.page) || 1; + const limit = Number(req.query.limit) || results.length; + const startIndex = (page - 1) * limit; + const endIndex = startIndex + limit; + + //Sort + if (sort === "hearts") { + results.sort((a, b) => b.hearts - a.hearts); + } + + //filtering on least X hearts + if (minHearts) { + results = results.filter((t) => t.hearts >= Number(minHearts)); + } + + //Paginering + results = results.slice(startIndex, endIndex); + + //Send it back as json + res.json(results); +}); + +app.get("/thoughts/random", (req, res) => { + const random = Math.floor(Math.random() * thoughtsData.length); + res.json(thoughtsData[random]); +}); + +//Get thought from specific id +app.get("/thoughts/:id", (req, res) => { + const id = req.params.id; + const thought = thoughtsData.find((t) => t._id === id); + if (thought) { + res.json(thought); + } else { + //If the id dont exist this error message shows + res.status(404).json({ error: "thought not found" }); + } +}); // Start the server app.listen(port, () => { - console.log(`Server running on http://localhost:${port}`) -}) + console.log(`Server running on http://localhost:${port}`); +}); From 9a395ef0c52e8ff77a0422ee9cc57bad3fed4e0c Mon Sep 17 00:00:00 2001 From: AgnesSj01 Date: Tue, 3 Feb 2026 08:23:17 +0100 Subject: [PATCH 2/2] Add authentication --- middleware/auth.js | 13 +++ models/User.js | 28 ++++++ package.json | 4 + routes/userRoutes.js | 51 +++++++++++ server.js | 209 +++++++++++++++++++++++++++++++++---------- 5 files changed, 260 insertions(+), 45 deletions(-) create mode 100644 middleware/auth.js create mode 100644 models/User.js create mode 100644 routes/userRoutes.js diff --git a/middleware/auth.js b/middleware/auth.js new file mode 100644 index 0000000..e26b512 --- /dev/null +++ b/middleware/auth.js @@ -0,0 +1,13 @@ +import User from "../models/User.js"; + +const authenticateUser = async (req, res, next) => { + const user = await User.findOne({ accessToken: req.header("Authorization") }); + if (user) { + req.user = user; + next(); + } else { + res.status(401).json({ error: "Authentication failed" }); + } +}; + +export default authenticateUser; diff --git a/models/User.js b/models/User.js new file mode 100644 index 0000000..897fcb2 --- /dev/null +++ b/models/User.js @@ -0,0 +1,28 @@ +import mongoose from "mongoose"; +import bcrypt from "bcryptjs"; +import crypto from "crypto"; + +const userSchema = new mongoose.Schema({ + name: { + type: String, + unique: true, + required: true, + }, + email: { + type: String, + unique: true, + required: true, + }, + password: { + type: String, + required: true, + }, + accessToken: { + type: String, + default: () => crypto.randomBytes(128).toString("hex"), + }, +}); + +const User = mongoose.model("User", userSchema); + +export default User; diff --git a/package.json b/package.json index 6cbc67d..2e7d8f5 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,12 @@ "@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.22.1", + "jsonwebtoken": "^9.0.3", + "mongoose": "^9.1.5", "nodemon": "^3.0.1" } } diff --git a/routes/userRoutes.js b/routes/userRoutes.js new file mode 100644 index 0000000..2cbf070 --- /dev/null +++ b/routes/userRoutes.js @@ -0,0 +1,51 @@ +import express from "express"; +import jwt from "jsonwebtoken"; +import User from "../models/User.js"; +import bcrypt from "bcryptjs"; + +const router = express.Router(); + +// Register new user +router.post("/users", async (req, res) => { + try { + // Get data from request body + const { name, email, password } = req.body; + + // Does the email already exist? + const existingUser = await User.findOne({ email }); + if (existingUser) { + // if the email already exists, return error messages + return res.status(400).json({ error: "That email already exists" }); + } + + // Create a new user with a hashed password + const user = new User({ name, email, password: bcrypt.hashSync(password) }); + + await user.save(); + + // Return user ID and token on successful registration + res.status(201).json({ id: user._id, accessToken: user.accessToken }); + } catch (err) { + res + .status(400) + .json({ message: "Could not create user", errors: err.errors }); + } +}); + +// When user Log in, send email and password, receive accessToken. +router.post("/sessions", async (req, res) => { + try { + const user = await User.findOne({ email: req.body.email }); + + // Check if the user exists AND if the password is correct + if (user && bcrypt.compareSync(req.body.password, user.password)) { + res.json({ userId: user._id, accessToken: user.accessToken }); + } else { + res.status(401).json({ message: "Invalid email or password" }); + } + } catch (err) { + res.status(500).json({ error: "Login failed" }); + } +}); + +export default router; diff --git a/server.js b/server.js index f48dcb7..8584040 100644 --- a/server.js +++ b/server.js @@ -1,82 +1,201 @@ -import cors from "cors"; -import express from "express"; -import thoughtsData from "./data.json"; +import cors from "cors"; // Allows requests from other domains (frontend) +import express from "express"; // Web server framework +import mongoose from "mongoose"; // Database connection to MongoDB +import authenticateUser from "./middleware/auth.js"; // Middleware to protect routes +import userRoutes from "./routes/userRoutes.js"; // Routes for register/login -// 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 +// Load environment variables from the .env file +import dotenv from "dotenv"; +dotenv.config(); + +//DATABASE CONNECTION +const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/HappyThoughts"; +mongoose.connect(mongoUrl); + +// Log when we have connected to the database +mongoose.connection.on("connected", () => { + console.log(" Connected to MongoDB"); +}); + +// Log if something goes wrong with the database connection +mongoose.connection.on("error", (err) => { + console.log("MongoDB connection error:", err); +}); +mongoose.Promise = Promise; + +// THOUGHT-MODELL +// Defines what a "thought" looks like in the database +const Thought = mongoose.model("Thought", { + message: { + type: String, + required: true, + minlength: 3, + maxlength: 140, + }, + hearts: { + type: Number, + default: 0, + }, + createdAt: { + type: Date, + default: () => new Date(), + }, +}); + +// SERVER-SETUP const port = process.env.PORT || 8080; const app = express(); -// Add middlewares to enable cors and json body parsing +// Middleware app.use(cors()); app.use(express.json()); -// Start defining your routes here - -//Documentation +// ROUTES +// API-dokumentation app.get("/", (req, res) => { res.json({ message: "Welcome to Happy Thoughts API", endpoints: [ { method: "GET", path: "/", description: "API documentation" }, { method: "GET", path: "/thoughts", description: "Get all thoughts" }, + { + method: "GET", + path: "/thoughts/random", + description: "Get a random thought", + }, { method: "GET", path: "/thoughts/:id", description: "Get a specific thought by ID", }, + { + method: "POST", + path: "/thoughts", + description: "Create a new thought (requires authentication)", + }, + { + method: "DELETE", + path: "/thoughts/:id", + description: "Delete a thought (requires authentication)", + }, + { + method: "PATCH", + path: "/thoughts/:id", + description: "Add a heart to a thought (requires authentication)", + }, + { + method: "PATCH", + path: "/thoughts/:id/update", + description: "Update message (requires authentication)", + }, ], }); }); -//All Thoughts (optional sorting with ?sort=hearts) -app.get("/thoughts", (req, res) => { - const sort = req.query.sort; - const minHearts = req.query.minHearts; - let results = [...thoughtsData]; //copy the array - - //Paginering - const page = Number(req.query.page) || 1; - const limit = Number(req.query.limit) || results.length; - const startIndex = (page - 1) * limit; - const endIndex = startIndex + limit; - - //Sort - if (sort === "hearts") { - results.sort((a, b) => b.hearts - a.hearts); +// Get all thoughts +app.get("/thoughts", async (req, res) => { + try { + const results = await Thought.find(); + res.json(results); + } catch (err) { + res.status(500).json({ error: "Could not fetch thoughts" }); + } +}); + +// Get a random thought +app.get("/thoughts/random", async (req, res) => { + try { + const thoughts = await Thought.find(); + if (thoughts.length === 0) { + return res.status(404).json({ error: "No thoughts found" }); + } + const randomIndex = Math.floor(Math.random() * thoughts.length); + res.json(thoughts[randomIndex]); + } catch (err) { + res.status(500).json({ error: "Could not get random thought" }); } +}); + +// Get a specific thought by ID +app.get("/thoughts/:id", async (req, res) => { + try { + const thought = await Thought.findById(req.params.id); + if (thought) { + res.json(thought); + } else { + res.status(404).json({ error: "Thought do not exist" }); + } + } catch (error) { + res.status(400).json({ error: "Invalid thought ID" }); + } +}); - //filtering on least X hearts - if (minHearts) { - results = results.filter((t) => t.hearts >= Number(minHearts)); +// Create a new thought – PROTECTED (requires login) +// authenticateUser runs FIRST and checks that the user has a valid token +app.post("/thoughts", authenticateUser, async (req, res) => { + try { + const newThought = new Thought({ + message: req.body.message, + hearts: 0, + }); + const savedThought = await newThought.save(); + res.status(201).json(savedThought); + } catch (err) { + res.status(400).json({ error: err.message }); } +}); - //Paginering - results = results.slice(startIndex, endIndex); +// Delete thought – PROTECTED (requires login) +app.delete("/thoughts/:id", authenticateUser, async (req, res) => { + try { + const deleted = await Thought.findByIdAndDelete(req.params.id); - //Send it back as json - res.json(results); + if (!deleted) { + return res.status(404).json({ error: "Thought not found" }); + } + res.status(200).json({ message: "Deleted", id: req.params.id }); + } catch (err) { + res.status(400).json({ error: "Invalid ID" }); + } }); -app.get("/thoughts/random", (req, res) => { - const random = Math.floor(Math.random() * thoughtsData.length); - res.json(thoughtsData[random]); +// Add a heart to a thought – PROTECTED (requires login) +app.patch("/thoughts/:id", authenticateUser, async (req, res) => { + try { + const updatedThought = await Thought.findByIdAndUpdate( + req.params.id, + { $inc: { hearts: 1 } }, + { new: true }, + ); + if (!updatedThought) { + return res.status(404).json({ error: "Thought not found" }); + } + res.status(200).json(updatedThought); + } catch { + res.status(400).json({ error: "Unable to add heart to thought" }); + } }); -//Get thought from specific id -app.get("/thoughts/:id", (req, res) => { - const id = req.params.id; - const thought = thoughtsData.find((t) => t._id === id); - if (thought) { - res.json(thought); - } else { - //If the id dont exist this error message shows - res.status(404).json({ error: "thought not found" }); +// User updates a thought – PROTECTED (requires login) +app.patch("/thoughts/:id/update", authenticateUser, async (req, res) => { + try { + const updatedThought = await Thought.findByIdAndUpdate( + req.params.id, + { message: req.body.message }, + { new: true }, + ); + if (!updatedThought) { + return res.status(404).json({ error: "Thought not found" }); + } + res.status(200).json(updatedThought); + } catch { + res.status(400).json({ error: "Unable to update message" }); } }); -// Start the server +app.use(userRoutes); + +// START SERVERN app.listen(port, () => { console.log(`Server running on http://localhost:${port}`); });