From abcfe2ccbd7a8d18c170090f27394142c418a3b1 Mon Sep 17 00:00:00 2001 From: violacathrine Date: Thu, 29 May 2025 10:23:56 +0200 Subject: [PATCH 1/9] implemented routes, filtering --- data.json => data/data.json | 0 package.json | 1 + server.js | 64 +++++++++++++++++++++++++++++-------- todo.txt | 27 ++++++++++++++++ 4 files changed, 78 insertions(+), 14 deletions(-) rename data.json => data/data.json (100%) create mode 100644 todo.txt diff --git a/data.json b/data/data.json similarity index 100% rename from data.json rename to data/data.json diff --git a/package.json b/package.json index bf25bb6..00addae 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@babel/preset-env": "^7.16.11", "cors": "^2.8.5", "express": "^4.17.3", + "express-list-endpoints": "^7.1.1", "nodemon": "^3.0.1" } } diff --git a/server.js b/server.js index f47771b..3179755 100644 --- a/server.js +++ b/server.js @@ -1,22 +1,58 @@ -import cors from "cors" -import express from "express" +import express from "express"; +import cors from "cors"; +import listEndpoints from "express-list-endpoints"; -// Defines the port the app will run on. Defaults to 8080, but can be overridden -// when starting the server. Example command to overwrite PORT env variable value: -// PORT=9000 npm start -const port = process.env.PORT || 8080 -const app = express() +import data from "./data/data.json"; + +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 +// GET / -> API-documentation app.get("/", (req, res) => { - res.send("Hello Technigo!") -}) + const endpoints = listEndpoints(app); + res.json({ + message: "Welcome to the Happy Thoughts API", + endpoints: endpoints, + }); +}); + +// endpoint for getting all thoughts +app.get("/thoughts", (req, res) => { + const { message, minHearts } = req.query; + let filteredThoughts = data; + + if (message) { + filteredThoughts = filteredThoughts.filter( + (thought) => thought.message.toLowerCase().includes(message.toLowerCase()) + ); + } + + if (minHearts) { + filteredThoughts = filteredThoughts.filter((thought) => + thought.hearts >= Number (minHearts) + ); + } + + res.json(filteredThoughts); +}); + +// endpoint for gettin one thought +app.get("/thoughts/:id", (req, res) => { + const thought = data.find((thought) => thought._id === req.params.id); + + // tiny error handling if we get an id that doesnt exist in our data + if (!thought) { + 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}`) -}) + console.log(`Server running on http://localhost:${port}`); +}); diff --git a/todo.txt b/todo.txt new file mode 100644 index 0000000..c174e7f --- /dev/null +++ b/todo.txt @@ -0,0 +1,27 @@ +### ✅ Vecka 1 – Grundläggande API med Express + +- [x] API:t har minst tre olika routes (endpoints). +- [x] Route `GET /` returnerar API-dokumentation (t.ex. med Express List Endpoints). +- [x] Minst en route returnerar en lista med flera objekt (array). +- [x] Minst en route returnerar ett enskilt objekt (single element). +- [x] API:t följer RESTful-principer. +- [x] Koden är städad och följer principer för clean code. + +#### 🧠 Stretch goals + +- [x] Routes som returnerar ett enskilt objekt hanterar om objektet inte finns och ger användbar information i svaret. +- [ ] Implementera pagination med `.slice()` för stora datamängder, och använd query-parametrar som `?page=2`. +- [ ] Lägg till kategori eller taggar för tankar (t.ex. "Food thoughts", "Work thoughts"). +- [ ] Lägg till sortering och filtrering i endpointen som returnerar alla tankar. Exempel: + - [ ] Sortera på datum eller antal likes. + - [x] Filtrera på tankar med fler än X hjärtan. + - [ ] Filtrera på tankar nyare än ett visst datum. + - [ ] Filtrera på specifika kategorier (om du har implementerat dem). + +#### 🛠 Tips + +- [ ] Testa dina endpoints med Postman. +- [ ] Gör frekventa commits till GitHub. +- [ ] Kom ihåg skillnaden mellan: + - **Path params**: t.ex. `/thoughts/:id` + - **Query params**: t.ex. `/thoughts?category=work` From c644ae6db3a9f14bb3a0d11b65c3a9031d970855 Mon Sep 17 00:00:00 2001 From: violacathrine Date: Fri, 30 May 2025 08:11:47 +0200 Subject: [PATCH 2/9] pagination --- server.js | 31 +++++++++++++++++++++++++++---- todo.txt | 4 ++-- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/server.js b/server.js index 3179755..398c86c 100644 --- a/server.js +++ b/server.js @@ -23,20 +23,43 @@ app.get("/", (req, res) => { // endpoint for getting all thoughts app.get("/thoughts", (req, res) => { const { message, minHearts } = req.query; + const page = Number(req.query.page) || 1; + const limit = Number(req.query.limit) || 10; + + let filteredThoughts = data; if (message) { - filteredThoughts = filteredThoughts.filter( - (thought) => thought.message.toLowerCase().includes(message.toLowerCase()) + filteredThoughts = filteredThoughts.filter((thought) => + thought.message.toLowerCase().includes(message.toLowerCase()) ); } if (minHearts) { - filteredThoughts = filteredThoughts.filter((thought) => - thought.hearts >= Number (minHearts) + filteredThoughts = filteredThoughts.filter( + (thought) => thought.hearts >= Number(minHearts) ); } + // error message if no thoughts where found to a specific filter + if (filteredThoughts.length === 0) { + return res.status(404).json({ + message: "No thoughts found matching your filters.", + }); + } + + // pagination + const startIndex = (page - 1) * limit; + const endIndex = startIndex + limit; + const paginatedThoughts = filteredThoughts.slice(startIndex, endIndex); + + res.json({ + page: Number(page), + limit: Number(limit), + total: filteredThoughts.length, + results: paginatedThoughts, + }); + res.json(filteredThoughts); }); diff --git a/todo.txt b/todo.txt index c174e7f..3b4103d 100644 --- a/todo.txt +++ b/todo.txt @@ -5,12 +5,12 @@ - [x] Minst en route returnerar en lista med flera objekt (array). - [x] Minst en route returnerar ett enskilt objekt (single element). - [x] API:t följer RESTful-principer. -- [x] Koden är städad och följer principer för clean code. +- [] Koden är städad och följer principer för clean code. #### 🧠 Stretch goals - [x] Routes som returnerar ett enskilt objekt hanterar om objektet inte finns och ger användbar information i svaret. -- [ ] Implementera pagination med `.slice()` för stora datamängder, och använd query-parametrar som `?page=2`. +- [x] Implementera pagination med `.slice()` för stora datamängder, och använd query-parametrar som `?page=2`. - [ ] Lägg till kategori eller taggar för tankar (t.ex. "Food thoughts", "Work thoughts"). - [ ] Lägg till sortering och filtrering i endpointen som returnerar alla tankar. Exempel: - [ ] Sortera på datum eller antal likes. From bc41ee78d231e193f1e8efe6470dec9dc8ead390 Mon Sep 17 00:00:00 2001 From: violacathrine Date: Fri, 30 May 2025 09:29:27 +0200 Subject: [PATCH 3/9] sort by amount of likes --- server.js | 11 +++++++++-- todo.txt | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/server.js b/server.js index 398c86c..d8a3077 100644 --- a/server.js +++ b/server.js @@ -22,11 +22,10 @@ app.get("/", (req, res) => { // endpoint for getting all thoughts app.get("/thoughts", (req, res) => { - const { message, minHearts } = req.query; + const { message, minHearts, sort } = req.query; const page = Number(req.query.page) || 1; const limit = Number(req.query.limit) || 10; - let filteredThoughts = data; if (message) { @@ -48,6 +47,14 @@ app.get("/thoughts", (req, res) => { }); } + // sort by hearts: ascending if "hearts", descending if "-hearts" + + if (sort === "most-liked") { + filteredThoughts.sort((a, b) => Number(b.hearts) - Number(a.hearts)); + } else if (sort === "least-liked") { + filteredThoughts.sort((a, b) => Number(a.hearts) - Number(b.hearts)); + } + // pagination const startIndex = (page - 1) * limit; const endIndex = startIndex + limit; diff --git a/todo.txt b/todo.txt index 3b4103d..63fb8c3 100644 --- a/todo.txt +++ b/todo.txt @@ -13,7 +13,7 @@ - [x] Implementera pagination med `.slice()` för stora datamängder, och använd query-parametrar som `?page=2`. - [ ] Lägg till kategori eller taggar för tankar (t.ex. "Food thoughts", "Work thoughts"). - [ ] Lägg till sortering och filtrering i endpointen som returnerar alla tankar. Exempel: - - [ ] Sortera på datum eller antal likes. + - [ x] Sortera på datum eller antal likes. - [x] Filtrera på tankar med fler än X hjärtan. - [ ] Filtrera på tankar nyare än ett visst datum. - [ ] Filtrera på specifika kategorier (om du har implementerat dem). From f5b2b818d5df07e951e63577f84c3ceebc9be610 Mon Sep 17 00:00:00 2001 From: violacathrine Date: Wed, 11 Jun 2025 13:53:58 +0200 Subject: [PATCH 4/9] deploy backend to render --- controllers/thoughtsController.js | 103 ++++++++++++++++++++++++++++++ db.js | 14 ++++ models/Thought.js | 24 +++++++ package.json | 2 + routes/thoughtsRoutes.js | 18 ++++++ server.js | 78 ++++------------------ todo.txt | 2 +- 7 files changed, 173 insertions(+), 68 deletions(-) create mode 100644 controllers/thoughtsController.js create mode 100644 db.js create mode 100644 models/Thought.js create mode 100644 routes/thoughtsRoutes.js diff --git a/controllers/thoughtsController.js b/controllers/thoughtsController.js new file mode 100644 index 0000000..fab37ea --- /dev/null +++ b/controllers/thoughtsController.js @@ -0,0 +1,103 @@ +import { Thought } from "../models/Thought.js"; + +export const getThoughts = async (req, res) => { + const { message, minHearts, sort, page = 1, limit = 10 } = req.query; + try { + let query = {}; + + if (message) { + query.message = { $regex: new RegExp(message, "i") }; + } + + if (minHearts) { + query.hearts = { $gte: Number(minHearts) }; + } + + let thoughtsQuery = Thought.find(query); + + if (sort === "most-liked") { + thoughtsQuery = thoughtsQuery.sort({ hearts: -1 }); + } else if (sort === "least-liked") { + thoughtsQuery = thoughtsQuery.sort({ hearts: 1 }); + } + + const total = await Thought.countDocuments(query); + const results = await thoughtsQuery + .skip((page - 1) * limit) + .limit(Number(limit)); + + if (results.length === 0) { + return res.status(404).json({ message: "No thoughts found." }); + } + + res.json({ page: Number(page), limit: Number(limit), total, results }); + } catch (error) { + res.status(500).json({ error: "Server error", details: error }); + } +}; + +export const getThoughtById = async (req, res) => { + try { + const thought = await Thought.findById(req.params.id); + if (!thought) { + return res.status(404).json({ error: "Thought not found" }); + } + res.json(thought); + } catch (error) { + res.status(400).json({ error: "Invalid ID", details: error }); + } +}; + +// Create a new thought +export const createThought = async (req, res) => { + try { + const { message, category } = req.body; + + const newThought = new Thought({ message, category }); + + await newThought.save(); + + res.status(201).json(newThought); + } catch (error) { + res.status(400).json({ error: "Could not create thought", details: error }); + } +}; + +// Update an existing thought +export const updateThought = async (req, res) => { + const { id } = req.params; + const { message, hearts, category } = req.body; + + try { + const updatedThought = await Thought.findByIdAndUpdate( + id, + { message, hearts, category }, + { new: true, runValidators: true } // returnera det uppdaterade dokumentet + ); + + if (!updatedThought) { + return res.status(404).json({ error: "Thought not found" }); + } + + res.json(updatedThought); + } catch (error) { + res.status(400).json({ error: "Invalid update", details: error }); + } +}; + +// Delete a thought +export const deleteThought = async (req, res) => { + const { id } = req.params; + + try { + const deletedThought = await Thought.findByIdAndDelete(id); + + if (!deletedThought) { + return res.status(404).json({ error: "Thought not found" }); + } + + res.json({ message: "Thought deleted successfully", deletedThought }); + } catch (error) { + res.status(400).json({ error: "Invalid ID", details: error }); + } +}; diff --git a/db.js b/db.js new file mode 100644 index 0000000..dac3980 --- /dev/null +++ b/db.js @@ -0,0 +1,14 @@ +// db.js +import mongoose from "mongoose"; + +const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/happy-thoughts"; + +export const connectToDatabase = async () => { + try { + await mongoose.connect(mongoUrl); + console.log("Connected to MongoDB"); + } catch (error) { + console.error("Could not connect to MongoDB:", error); + process.exit(1); // Avsluta appen om anslutning misslyckas + } +}; diff --git a/models/Thought.js b/models/Thought.js new file mode 100644 index 0000000..4d18b06 --- /dev/null +++ b/models/Thought.js @@ -0,0 +1,24 @@ +import mongoose from "mongoose"; + +const ThoughtSchema = new mongoose.Schema({ + message: { + type: String, + required: true, + minlength: 5, + maxlength: 140, + }, + hearts: { + type: Number, + default: 0, + }, + createdAt: { + type: Date, + default: Date.now, + }, + category: { + type: String, + enum: ["Food", "Work", "Life", "Other"], + }, +}); + +export const Thought = mongoose.model("Thought", ThoughtSchema); diff --git a/package.json b/package.json index 00addae..d320bcb 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,10 @@ "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", "cors": "^2.8.5", + "dotenv": "^16.5.0", "express": "^4.17.3", "express-list-endpoints": "^7.1.1", + "mongoose": "^8.15.1", "nodemon": "^3.0.1" } } diff --git a/routes/thoughtsRoutes.js b/routes/thoughtsRoutes.js new file mode 100644 index 0000000..42c7eac --- /dev/null +++ b/routes/thoughtsRoutes.js @@ -0,0 +1,18 @@ +import express from "express"; +import { + getThoughts, + getThoughtById, + createThought, + updateThought, + deleteThought, +} from "../controllers/thoughtsController.js"; + +const router = express.Router(); + +router.get("/", getThoughts); +router.get("/:id", getThoughtById); +router.post("/", createThought); +router.patch("/:id", updateThought); +router.delete("/:id", deleteThought); + +export default router; diff --git a/server.js b/server.js index d8a3077..f93dd78 100644 --- a/server.js +++ b/server.js @@ -1,88 +1,32 @@ +import dotenv from "dotenv"; +dotenv.config(); // Load environment variables from .env file + import express from "express"; import cors from "cors"; import listEndpoints from "express-list-endpoints"; -import data from "./data/data.json"; +import { connectToDatabase } from "./db.js"; // import connection +import thoughtsRoutes from "./routes/thoughtsRoutes.js"; 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()); -// GET / -> API-documentation app.get("/", (req, res) => { const endpoints = listEndpoints(app); res.json({ message: "Welcome to the Happy Thoughts API", - endpoints: endpoints, + endpoints, }); }); -// endpoint for getting all thoughts -app.get("/thoughts", (req, res) => { - const { message, minHearts, sort } = req.query; - const page = Number(req.query.page) || 1; - const limit = Number(req.query.limit) || 10; - - let filteredThoughts = data; - - if (message) { - filteredThoughts = filteredThoughts.filter((thought) => - thought.message.toLowerCase().includes(message.toLowerCase()) - ); - } - - if (minHearts) { - filteredThoughts = filteredThoughts.filter( - (thought) => thought.hearts >= Number(minHearts) - ); - } - - // error message if no thoughts where found to a specific filter - if (filteredThoughts.length === 0) { - return res.status(404).json({ - message: "No thoughts found matching your filters.", - }); - } - - // sort by hearts: ascending if "hearts", descending if "-hearts" - - if (sort === "most-liked") { - filteredThoughts.sort((a, b) => Number(b.hearts) - Number(a.hearts)); - } else if (sort === "least-liked") { - filteredThoughts.sort((a, b) => Number(a.hearts) - Number(b.hearts)); - } - - // pagination - const startIndex = (page - 1) * limit; - const endIndex = startIndex + limit; - const paginatedThoughts = filteredThoughts.slice(startIndex, endIndex); +app.use("/thoughts", thoughtsRoutes); - res.json({ - page: Number(page), - limit: Number(limit), - total: filteredThoughts.length, - results: paginatedThoughts, +// connect to mongoDB then start server +connectToDatabase().then(() => { + app.listen(port, () => { + console.log(`Server running on http://localhost:${port}`); }); - - res.json(filteredThoughts); -}); - -// endpoint for gettin one thought -app.get("/thoughts/:id", (req, res) => { - const thought = data.find((thought) => thought._id === req.params.id); - - // tiny error handling if we get an id that doesnt exist in our data - if (!thought) { - 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}`); }); diff --git a/todo.txt b/todo.txt index 63fb8c3..78a25ec 100644 --- a/todo.txt +++ b/todo.txt @@ -13,7 +13,7 @@ - [x] Implementera pagination med `.slice()` för stora datamängder, och använd query-parametrar som `?page=2`. - [ ] Lägg till kategori eller taggar för tankar (t.ex. "Food thoughts", "Work thoughts"). - [ ] Lägg till sortering och filtrering i endpointen som returnerar alla tankar. Exempel: - - [ x] Sortera på datum eller antal likes. + - [/] Sortera på datum eller antal likes. - [x] Filtrera på tankar med fler än X hjärtan. - [ ] Filtrera på tankar nyare än ett visst datum. - [ ] Filtrera på specifika kategorier (om du har implementerat dem). From c35eb62c44242a6426f586e05d3d6b7be92d5bb5 Mon Sep 17 00:00:00 2001 From: violacathrine Date: Wed, 6 Aug 2025 09:11:19 +0200 Subject: [PATCH 5/9] Ready for deployment --- controllers/authController.js | 73 ++++++++++++++++++ controllers/thoughtsController.js | 60 +++++++++++---- data/data.json | 121 ------------------------------ middleware/auth.js | 21 ++++++ models/User.js | 24 ++++++ package.json | 3 + routes/authRoutes.js | 24 ++++++ routes/thoughtsRoutes.js | 56 +++++++++++++- server.js | 4 + 9 files changed, 249 insertions(+), 137 deletions(-) create mode 100644 controllers/authController.js delete mode 100644 data/data.json create mode 100644 middleware/auth.js create mode 100644 models/User.js create mode 100644 routes/authRoutes.js diff --git a/controllers/authController.js b/controllers/authController.js new file mode 100644 index 0000000..47690ce --- /dev/null +++ b/controllers/authController.js @@ -0,0 +1,73 @@ +import { validationResult } from "express-validator"; +import bcrypt from "bcrypt"; +import jwt from "jsonwebtoken"; +import { User } from "../models/User.js"; + +const JWT_SECRET = process.env.JWT_SECRET || "supersecret"; + +// User registration +export const register = async (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + + const { email, password } = req.body; + try { + // Check if user already exists + const existingUser = await User.findOne({ email }); + if (existingUser) { + return res + .status(409) + .json({ message: "That email address already exists" }); + } + + // Hash password + const hashedPassword = await bcrypt.hash(password, 10); + + // Create and save user + const user = await User.create({ email, password: hashedPassword }); + + // Create JWT token + const token = jwt.sign( + { userId: user._id, email: user.email }, + JWT_SECRET, + { expiresIn: "2h" } + ); + + res.status(201).json({ token, user: { id: user._id, email: user.email } }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}; + +// User login +export const login = async (req, res) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + + const { email, password } = req.body; + try { + const user = await User.findOne({ email }); + if (!user) { + return res.status(401).json({ message: "Invalid credentials" }); + } + + const match = await bcrypt.compare(password, user.password); + if (!match) { + return res.status(401).json({ message: "Invalid credentials" }); + } + + const token = jwt.sign( + { userId: user._id, email: user.email }, + JWT_SECRET, + { expiresIn: "2h" } + ); + + res.status(200).json({ token, user: { id: user._id, email: user.email } }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}; diff --git a/controllers/thoughtsController.js b/controllers/thoughtsController.js index fab37ea..51b0065 100644 --- a/controllers/thoughtsController.js +++ b/controllers/thoughtsController.js @@ -1,20 +1,19 @@ import { Thought } from "../models/Thought.js"; +import { validationResult } from "express-validator"; +// Get all thoughts with optional filters and pagination export const getThoughts = async (req, res) => { const { message, minHearts, sort, page = 1, limit = 10 } = req.query; try { - let query = {}; - + const query = {}; if (message) { query.message = { $regex: new RegExp(message, "i") }; } - if (minHearts) { query.hearts = { $gte: Number(minHearts) }; } let thoughtsQuery = Thought.find(query); - if (sort === "most-liked") { thoughtsQuery = thoughtsQuery.sort({ hearts: -1 }); } else if (sort === "least-liked") { @@ -36,6 +35,7 @@ export const getThoughts = async (req, res) => { } }; +// Get a single thought by ID export const getThoughtById = async (req, res) => { try { const thought = await Thought.findById(req.params.id); @@ -50,13 +50,16 @@ export const getThoughtById = async (req, res) => { // Create a new thought export const createThought = async (req, res) => { + // Valideringssteg + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + try { const { message, category } = req.body; - const newThought = new Thought({ message, category }); - await newThought.save(); - res.status(201).json(newThought); } catch (error) { res.status(400).json({ error: "Could not create thought", details: error }); @@ -65,6 +68,12 @@ export const createThought = async (req, res) => { // Update an existing thought export const updateThought = async (req, res) => { + // Valideringssteg + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + const { id } = req.params; const { message, hearts, category } = req.body; @@ -72,7 +81,7 @@ export const updateThought = async (req, res) => { const updatedThought = await Thought.findByIdAndUpdate( id, { message, hearts, category }, - { new: true, runValidators: true } // returnera det uppdaterade dokumentet + { new: true, runValidators: true } ); if (!updatedThought) { @@ -87,17 +96,42 @@ export const updateThought = async (req, res) => { // Delete a thought export const deleteThought = async (req, res) => { - const { id } = req.params; - try { - const deletedThought = await Thought.findByIdAndDelete(id); - + const deletedThought = await Thought.findByIdAndDelete(req.params.id); if (!deletedThought) { return res.status(404).json({ error: "Thought not found" }); } - res.json({ message: "Thought deleted successfully", deletedThought }); } catch (error) { res.status(400).json({ error: "Invalid ID", details: error }); } }; + +// Like a thought +export const likeThought = async (req, res) => { + try { + const updatedThought = await Thought.findByIdAndUpdate( + req.params.id, + { $inc: { hearts: 1 } }, + { new: true } + ); + res.json(updatedThought); + } catch (error) { + res.status(400).json({ error: "Failed to like thought", details: error }); + } +}; + +// Unlike a thought +export const unlikeThought = async (req, res) => { + try { + const thought = await Thought.findById(req.params.id); + if (!thought) { + return res.status(404).json({ error: "Thought not found" }); + } + thought.hearts = Math.max(thought.hearts - 1, 0); + await thought.save(); + res.json(thought); + } catch (error) { + res.status(400).json({ error: "Failed to unlike", details: error }); + } +}; diff --git a/data/data.json b/data/data.json deleted file mode 100644 index a2c844f..0000000 --- a/data/data.json +++ /dev/null @@ -1,121 +0,0 @@ -[ - { - "_id": "682bab8c12155b00101732ce", - "message": "Berlin baby", - "hearts": 37, - "createdAt": "2025-05-19T22:07:08.999Z", - "__v": 0 - }, - { - "_id": "682e53cc4fddf50010bbe739", - "message": "My family!", - "hearts": 0, - "createdAt": "2025-05-22T22:29:32.232Z", - "__v": 0 - }, - { - "_id": "682e4f844fddf50010bbe738", - "message": "The smell of coffee in the morning....", - "hearts": 23, - "createdAt": "2025-05-22T22:11:16.075Z", - "__v": 0 - }, - { - "_id": "682e48bf4fddf50010bbe737", - "message": "Newly washed bedlinen, kids that sleeps through the night.. FINGERS CROSSED 🤞🏼\n", - "hearts": 6, - "createdAt": "2025-05-21T21:42:23.862Z", - "__v": 0 - }, - { - "_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 - }, - { - "_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 - }, - { - "_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 - }, - { - "_id": "682cebbe17487d0010a298b5", - "message": "Tacos and tequila🌮🍹", - "hearts": 2, - "createdAt": "2025-05-19T20:53:18.899Z", - "__v": 0 - }, - { - "_id": "682ceb5617487d0010a298b4", - "message": "Netflix and late night ice-cream🍦", - "hearts": 1, - "createdAt": "2025-05-18T20:51:34.494Z", - "__v": 0 - }, - { - "_id": "682c99ba3bff2d0010f5d44e", - "message": "Summer is coming...", - "hearts": 2, - "createdAt": "2025-05-20T15:03:22.379Z", - "__v": 0 - }, - { - "_id": "682c706c951f7a0017130024", - "message": "Exercise? I thought you said extra fries! 🍟😂", - "hearts": 14, - "createdAt": "2025-05-20T12:07:08.185Z", - "__v": 0 - }, - { - "_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": "682c6f0e951f7a0017130022", - "message": "Cute monkeys🐒", - "hearts": 2, - "createdAt": "2025-05-20T12:01:18.308Z", - "__v": 0 - }, - { - "_id": "682c6e65951f7a0017130021", - "message": "The weather is nice!", - "hearts": 0, - "createdAt": "2025-05-20T11:58:29.662Z", - "__v": 0 - }, - { - "_id": "682bfdb4270ca300105af221", - "message": "good vibes and good things", - "hearts": 3, - "createdAt": "2025-05-20T03:57:40.322Z", - "__v": 0 - }, - { - "_id": "682bab8c12155b00101732ce", - "message": "Berlin baby", - "hearts": 37, - "createdAt": "2025-05-19T22:07:08.999Z", - "__v": 0 - } -] \ No newline at end of file diff --git a/middleware/auth.js b/middleware/auth.js new file mode 100644 index 0000000..6f3168f --- /dev/null +++ b/middleware/auth.js @@ -0,0 +1,21 @@ +// middleware/auth.js +import jwt from "jsonwebtoken"; + +const JWT_SECRET = process.env.JWT_SECRET || "supersecret"; + +export const verifyToken = (req, res, next) => { + const auth = req.headers.authorization; + if (!auth || !auth.startsWith("Bearer ")) { + return res + .status(401) + .json({ message: "Missing or invalid Authorization header" }); + } + const token = auth.split(" ")[1]; + try { + const payload = jwt.verify(token, JWT_SECRET); + req.user = { id: payload.userId, email: payload.email }; + next(); + } catch (err) { + return res.status(401).json({ message: "Invalid or expired token" }); + } +}; diff --git a/models/User.js b/models/User.js new file mode 100644 index 0000000..ec306f4 --- /dev/null +++ b/models/User.js @@ -0,0 +1,24 @@ +import mongoose from "mongoose"; + +const UserSchema = new mongoose.Schema( + { + email: { + type: String, + required: [true, "Email is required"], + unique: true, + lowercase: true, + trim: true, + match: [/.+@.+\..+/, "Please provide a valid email address"], + }, + password: { + type: String, + required: [true, "Password is required"], + minlength: [8, "Password must be at least 8 characters long"], + }, + }, + { + timestamps: true, + } +); + +export const User = mongoose.model("User", UserSchema); diff --git a/package.json b/package.json index d320bcb..99e8dc5 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,13 @@ "@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.5.0", "express": "^4.17.3", "express-list-endpoints": "^7.1.1", + "express-validator": "^7.2.1", + "jsonwebtoken": "^9.0.2", "mongoose": "^8.15.1", "nodemon": "^3.0.1" } diff --git a/routes/authRoutes.js b/routes/authRoutes.js new file mode 100644 index 0000000..b286d72 --- /dev/null +++ b/routes/authRoutes.js @@ -0,0 +1,24 @@ +import express from "express"; +import { body } from "express-validator"; +import { register, login } from "../controllers/authController.js"; + +const router = express.Router(); + +router.post( + "/register", + [ + body("email").isEmail().withMessage("Must be a valid email"), + body("password") + .isLength({ min: 8 }) + .withMessage("Password must be at least 8 characters"), + ], + register +); + +router.post( + "/login", + [body("email").isEmail(), body("password").exists()], + login +); + +export default router; diff --git a/routes/thoughtsRoutes.js b/routes/thoughtsRoutes.js index 42c7eac..337f909 100644 --- a/routes/thoughtsRoutes.js +++ b/routes/thoughtsRoutes.js @@ -1,18 +1,68 @@ import express from "express"; +import { body } from "express-validator"; +import { verifyToken } from "../middleware/auth.js"; import { getThoughts, getThoughtById, createThought, updateThought, deleteThought, + likeThought, + unlikeThought, } from "../controllers/thoughtsController.js"; const router = express.Router(); +// --- PUBLIKA ROUTES --- router.get("/", getThoughts); router.get("/:id", getThoughtById); -router.post("/", createThought); -router.patch("/:id", updateThought); -router.delete("/:id", deleteThought); + +// --- SKYDDADE ROUTES --- +// Skapa tanke +router.post( + "/", + verifyToken, + [ + body("message") + .isString() + .isLength({ min: 5, max: 140 }) + .withMessage("Meddelandet måste vara mellan 5 och 140 tecken."), + // Ta bort kategori om du inte vill ha det: + body("category") + .optional() + .isIn(["Food", "Work", "Life", "Other"]) + .withMessage("Ogiltig kategori."), + ], + createThought +); + +// Uppdatera tanke +router.patch( + "/:id", + verifyToken, + [ + body("message") + .optional() + .isString() + .isLength({ min: 5, max: 140 }) + .withMessage("Meddelandet (om satt) måste vara 5–140 tecken."), + body("hearts") + .optional() + .isInt({ min: 0 }) + .withMessage("Hearts måste vara ett heltal ≥ 0."), + body("category") + .optional() + .isIn(["Food", "Work", "Life", "Other"]) + .withMessage("Ogiltig kategori."), + ], + updateThought +); + +// Radera tanke +router.delete("/:id", verifyToken, deleteThought); + +// Gilla/ogilla tanke +router.patch("/:id/like", verifyToken, likeThought); +router.patch("/:id/unlike", verifyToken, unlikeThought); export default router; diff --git a/server.js b/server.js index f93dd78..83f093f 100644 --- a/server.js +++ b/server.js @@ -6,6 +6,7 @@ import cors from "cors"; import listEndpoints from "express-list-endpoints"; import { connectToDatabase } from "./db.js"; // import connection +import authRoutes from "./routes/authRoutes.js"; import thoughtsRoutes from "./routes/thoughtsRoutes.js"; const port = process.env.PORT || 8080; @@ -14,6 +15,9 @@ const app = express(); app.use(cors()); app.use(express.json()); +// Public-API för autentisering +app.use("/auth", authRoutes); + app.get("/", (req, res) => { const endpoints = listEndpoints(app); res.json({ From 832976194a00848da5a58968fc655a864a87bd9e Mon Sep 17 00:00:00 2001 From: violacathrine Date: Wed, 6 Aug 2025 11:23:50 +0200 Subject: [PATCH 6/9] fixes --- .vscode/settings.json | 3 ++ controllers/thoughtsController.js | 58 ++++++++++++++++++------------- models/Thought.js | 5 +-- routes/thoughtsRoutes.js | 27 +++++--------- 4 files changed, 47 insertions(+), 46 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d3cb2ac --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "postman.settings.dotenv-detection-notification-visibility": false +} diff --git a/controllers/thoughtsController.js b/controllers/thoughtsController.js index 51b0065..899f3d2 100644 --- a/controllers/thoughtsController.js +++ b/controllers/thoughtsController.js @@ -25,10 +25,7 @@ export const getThoughts = async (req, res) => { .skip((page - 1) * limit) .limit(Number(limit)); - if (results.length === 0) { - return res.status(404).json({ message: "No thoughts found." }); - } - + // Always return 200 with results array (possibly empty) res.json({ page: Number(page), limit: Number(limit), total, results }); } catch (error) { res.status(500).json({ error: "Server error", details: error }); @@ -50,15 +47,14 @@ export const getThoughtById = async (req, res) => { // Create a new thought export const createThought = async (req, res) => { - // Valideringssteg const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } try { - const { message, category } = req.body; - const newThought = new Thought({ message, category }); + const { message } = req.body; + const newThought = new Thought({ message, user: req.user.id }); await newThought.save(); res.status(201).json(newThought); } catch (error) { @@ -66,42 +62,56 @@ export const createThought = async (req, res) => { } }; -// Update an existing thought +// UPDATE A THOUGHT export const updateThought = async (req, res) => { - // Valideringssteg const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } const { id } = req.params; - const { message, hearts, category } = req.body; - + const { message } = req.body; try { - const updatedThought = await Thought.findByIdAndUpdate( - id, - { message, hearts, category }, - { new: true, runValidators: true } - ); - - if (!updatedThought) { + // Get the thought by ID and check ownership + const thought = await Thought.findById(id); + if (!thought) { return res.status(404).json({ error: "Thought not found" }); } - res.json(updatedThought); + // Ensure the user is the owner of the thought + if (thought.user.toString() !== req.user.id) { + return res + .status(403) + .json({ error: "You can only update your own thoughts" }); + } + + // Update the thought message + thought.message = message; + await thought.save(); + res.json(thought); } catch (error) { res.status(400).json({ error: "Invalid update", details: error }); } }; -// Delete a thought +// DELETE A THOUGHT export const deleteThought = async (req, res) => { try { - const deletedThought = await Thought.findByIdAndDelete(req.params.id); - if (!deletedThought) { + const thought = await Thought.findById(req.params.id); + if (!thought) { return res.status(404).json({ error: "Thought not found" }); } - res.json({ message: "Thought deleted successfully", deletedThought }); + + // Ensure the user is the owner of the thought + if (thought.user.toString() !== req.user.id) { + return res + .status(403) + .json({ error: "You can only delete your own thoughts" }); + } + + // Delete the thought + await thought.deleteOne(); + res.json({ message: "Thought deleted successfully" }); } catch (error) { res.status(400).json({ error: "Invalid ID", details: error }); } @@ -132,6 +142,6 @@ export const unlikeThought = async (req, res) => { await thought.save(); res.json(thought); } catch (error) { - res.status(400).json({ error: "Failed to unlike", details: error }); + res.status(400).json({ error: "Failed to unlike thought", details: error }); } }; diff --git a/models/Thought.js b/models/Thought.js index 4d18b06..bc5d198 100644 --- a/models/Thought.js +++ b/models/Thought.js @@ -15,10 +15,7 @@ const ThoughtSchema = new mongoose.Schema({ type: Date, default: Date.now, }, - category: { - type: String, - enum: ["Food", "Work", "Life", "Other"], - }, + user: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }, }); export const Thought = mongoose.model("Thought", ThoughtSchema); diff --git a/routes/thoughtsRoutes.js b/routes/thoughtsRoutes.js index 337f909..0837ea0 100644 --- a/routes/thoughtsRoutes.js +++ b/routes/thoughtsRoutes.js @@ -13,12 +13,12 @@ import { const router = express.Router(); -// --- PUBLIKA ROUTES --- +// --- PUBLIC ROUTES --- router.get("/", getThoughts); router.get("/:id", getThoughtById); -// --- SKYDDADE ROUTES --- -// Skapa tanke +// --- SECURED ROUTES --- +// Create thought router.post( "/", verifyToken, @@ -26,17 +26,12 @@ router.post( body("message") .isString() .isLength({ min: 5, max: 140 }) - .withMessage("Meddelandet måste vara mellan 5 och 140 tecken."), - // Ta bort kategori om du inte vill ha det: - body("category") - .optional() - .isIn(["Food", "Work", "Life", "Other"]) - .withMessage("Ogiltig kategori."), + .withMessage("Message must be 5–140 characters."), ], createThought ); -// Uppdatera tanke +// Update a thought router.patch( "/:id", verifyToken, @@ -45,23 +40,19 @@ router.patch( .optional() .isString() .isLength({ min: 5, max: 140 }) - .withMessage("Meddelandet (om satt) måste vara 5–140 tecken."), + .withMessage("Message (if set) must be 5–140 characters."), body("hearts") .optional() .isInt({ min: 0 }) - .withMessage("Hearts måste vara ett heltal ≥ 0."), - body("category") - .optional() - .isIn(["Food", "Work", "Life", "Other"]) - .withMessage("Ogiltig kategori."), + .withMessage("Hearts must be a whole number ≥ 0."), ], updateThought ); -// Radera tanke +// Delete a thought router.delete("/:id", verifyToken, deleteThought); -// Gilla/ogilla tanke +// Like/Unlike a thought router.patch("/:id/like", verifyToken, likeThought); router.patch("/:id/unlike", verifyToken, unlikeThought); From 57d7e315d17c417ee63f96e43f0c8f3b334b533c Mon Sep 17 00:00:00 2001 From: violacathrine Date: Thu, 7 Aug 2025 09:24:23 +0200 Subject: [PATCH 7/9] test --- controllers/thoughtsController.js | 83 ++++++++++++++----------------- models/Thought.js | 6 ++- 2 files changed, 41 insertions(+), 48 deletions(-) diff --git a/controllers/thoughtsController.js b/controllers/thoughtsController.js index 899f3d2..db2b68a 100644 --- a/controllers/thoughtsController.js +++ b/controllers/thoughtsController.js @@ -1,60 +1,59 @@ import { Thought } from "../models/Thought.js"; import { validationResult } from "express-validator"; -// Get all thoughts with optional filters and pagination +// GET /thoughts export const getThoughts = async (req, res) => { const { message, minHearts, sort, page = 1, limit = 10 } = req.query; try { const query = {}; - if (message) { - query.message = { $regex: new RegExp(message, "i") }; - } - if (minHearts) { - query.hearts = { $gte: Number(minHearts) }; - } + if (message) query.message = { $regex: new RegExp(message, "i") }; + if (minHearts) query.hearts = { $gte: Number(minHearts) }; - let thoughtsQuery = Thought.find(query); - if (sort === "most-liked") { + // inkludera user‐fältet så frontend ser ägaren + let thoughtsQuery = Thought.find(query).select( + "message hearts createdAt user" + ); + if (sort === "most-liked") thoughtsQuery = thoughtsQuery.sort({ hearts: -1 }); - } else if (sort === "least-liked") { + else if (sort === "least-liked") thoughtsQuery = thoughtsQuery.sort({ hearts: 1 }); - } const total = await Thought.countDocuments(query); const results = await thoughtsQuery .skip((page - 1) * limit) .limit(Number(limit)); - // Always return 200 with results array (possibly empty) res.json({ page: Number(page), limit: Number(limit), total, results }); } catch (error) { res.status(500).json({ error: "Server error", details: error }); } }; -// Get a single thought by ID +// GET /thoughts/:id export const getThoughtById = async (req, res) => { try { - const thought = await Thought.findById(req.params.id); - if (!thought) { - return res.status(404).json({ error: "Thought not found" }); - } + const thought = await Thought.findById(req.params.id).select( + "message hearts createdAt user" + ); + if (!thought) return res.status(404).json({ error: "Thought not found" }); res.json(thought); } catch (error) { res.status(400).json({ error: "Invalid ID", details: error }); } }; -// Create a new thought +// POST /thoughts export const createThought = async (req, res) => { const errors = validationResult(req); - if (!errors.isEmpty()) { + if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() }); - } try { const { message } = req.body; - const newThought = new Thought({ message, user: req.user.id }); + const newThought = new Thought({ + message, + user: req.user.userId, // använd alltid userId från JWT + }); await newThought.save(); res.status(201).json(newThought); } catch (error) { @@ -62,30 +61,25 @@ export const createThought = async (req, res) => { } }; -// UPDATE A THOUGHT +// PATCH /thoughts/:id export const updateThought = async (req, res) => { const errors = validationResult(req); - if (!errors.isEmpty()) { + if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() }); - } - const { id } = req.params; - const { message } = req.body; try { - // Get the thought by ID and check ownership + const { id } = req.params; + const { message } = req.body; const thought = await Thought.findById(id); - if (!thought) { - return res.status(404).json({ error: "Thought not found" }); - } + if (!thought) return res.status(404).json({ error: "Thought not found" }); - // Ensure the user is the owner of the thought - if (thought.user.toString() !== req.user.id) { + // ägarskapskontroll + if (!thought.user.equals(req.user.userId)) { return res .status(403) .json({ error: "You can only update your own thoughts" }); } - // Update the thought message thought.message = message; await thought.save(); res.json(thought); @@ -94,22 +88,19 @@ export const updateThought = async (req, res) => { } }; -// DELETE A THOUGHT +// DELETE /thoughts/:id export const deleteThought = async (req, res) => { try { const thought = await Thought.findById(req.params.id); - if (!thought) { - return res.status(404).json({ error: "Thought not found" }); - } + if (!thought) return res.status(404).json({ error: "Thought not found" }); - // Ensure the user is the owner of the thought - if (thought.user.toString() !== req.user.id) { + // ägarskapskontroll + if (!thought.user.equals(req.user.userId)) { return res .status(403) .json({ error: "You can only delete your own thoughts" }); } - // Delete the thought await thought.deleteOne(); res.json({ message: "Thought deleted successfully" }); } catch (error) { @@ -117,27 +108,25 @@ export const deleteThought = async (req, res) => { } }; -// Like a thought +// PATCH /thoughts/:id/like export const likeThought = async (req, res) => { try { - const updatedThought = await Thought.findByIdAndUpdate( + const updated = await Thought.findByIdAndUpdate( req.params.id, { $inc: { hearts: 1 } }, { new: true } ); - res.json(updatedThought); + res.json(updated); } catch (error) { res.status(400).json({ error: "Failed to like thought", details: error }); } }; -// Unlike a thought +// PATCH /thoughts/:id/unlike export const unlikeThought = async (req, res) => { try { const thought = await Thought.findById(req.params.id); - if (!thought) { - return res.status(404).json({ error: "Thought not found" }); - } + if (!thought) return res.status(404).json({ error: "Thought not found" }); thought.hearts = Math.max(thought.hearts - 1, 0); await thought.save(); res.json(thought); diff --git a/models/Thought.js b/models/Thought.js index bc5d198..8f7c3d7 100644 --- a/models/Thought.js +++ b/models/Thought.js @@ -15,7 +15,11 @@ const ThoughtSchema = new mongoose.Schema({ type: Date, default: Date.now, }, - user: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }, + user: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true + }, }); export const Thought = mongoose.model("Thought", ThoughtSchema); From cd58301e940a06bd613f676679352eaf9e9df372 Mon Sep 17 00:00:00 2001 From: violacathrine Date: Tue, 2 Sep 2025 08:42:40 +0200 Subject: [PATCH 8/9] ready for deploy --- controllers/thoughtsController.js | 8 +++++--- middleware/auth.js | 15 +++++++++++++++ models/Thought.js | 2 +- routes/thoughtsRoutes.js | 17 +++++++++-------- 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/controllers/thoughtsController.js b/controllers/thoughtsController.js index db2b68a..6e884eb 100644 --- a/controllers/thoughtsController.js +++ b/controllers/thoughtsController.js @@ -52,7 +52,8 @@ export const createThought = async (req, res) => { const { message } = req.body; const newThought = new Thought({ message, - user: req.user.userId, // använd alltid userId från JWT + // If user is logged in (req.user exists), save their ID, otherwise null for anonymous + user: req.user?.id || null, }); await newThought.save(); res.status(201).json(newThought); @@ -74,7 +75,7 @@ export const updateThought = async (req, res) => { if (!thought) return res.status(404).json({ error: "Thought not found" }); // ägarskapskontroll - if (!thought.user.equals(req.user.userId)) { + if (!thought.user.equals(req.user.id)) { return res .status(403) .json({ error: "You can only update your own thoughts" }); @@ -95,7 +96,7 @@ export const deleteThought = async (req, res) => { if (!thought) return res.status(404).json({ error: "Thought not found" }); // ägarskapskontroll - if (!thought.user.equals(req.user.userId)) { + if (!thought.user.equals(req.user.id)) { return res .status(403) .json({ error: "You can only delete your own thoughts" }); @@ -116,6 +117,7 @@ export const likeThought = async (req, res) => { { $inc: { hearts: 1 } }, { new: true } ); + if (!updated) return res.status(404).json({ error: "Thought not found" }); res.json(updated); } catch (error) { res.status(400).json({ error: "Failed to like thought", details: error }); diff --git a/middleware/auth.js b/middleware/auth.js index 6f3168f..348322f 100644 --- a/middleware/auth.js +++ b/middleware/auth.js @@ -19,3 +19,18 @@ export const verifyToken = (req, res, next) => { return res.status(401).json({ message: "Invalid or expired token" }); } }; + +// Optional auth - checks token if provided but doesn't require it +export const optionalAuth = (req, res, next) => { + const auth = req.headers.authorization; + if (auth && auth.startsWith("Bearer ")) { + const token = auth.split(" ")[1]; + try { + const payload = jwt.verify(token, JWT_SECRET); + req.user = { id: payload.userId, email: payload.email }; + } catch (err) { + // Invalid token, but we don't fail - just continue as anonymous + } + } + next(); +}; diff --git a/models/Thought.js b/models/Thought.js index 8f7c3d7..a70eba8 100644 --- a/models/Thought.js +++ b/models/Thought.js @@ -18,7 +18,7 @@ const ThoughtSchema = new mongoose.Schema({ user: { type: mongoose.Schema.Types.ObjectId, ref: "User", - required: true + required: false // Allow anonymous posts }, }); diff --git a/routes/thoughtsRoutes.js b/routes/thoughtsRoutes.js index 0837ea0..02c1a83 100644 --- a/routes/thoughtsRoutes.js +++ b/routes/thoughtsRoutes.js @@ -1,6 +1,6 @@ import express from "express"; import { body } from "express-validator"; -import { verifyToken } from "../middleware/auth.js"; +import { verifyToken, optionalAuth } from "../middleware/auth.js"; import { getThoughts, getThoughtById, @@ -17,11 +17,10 @@ const router = express.Router(); router.get("/", getThoughts); router.get("/:id", getThoughtById); -// --- SECURED ROUTES --- -// Create thought +// Create thought (public - anyone can post, but we check for optional auth) router.post( "/", - verifyToken, + optionalAuth, [ body("message") .isString() @@ -31,6 +30,12 @@ router.post( createThought ); +// Like/Unlike a thought (public - anyone can like) +router.patch("/:id/like", optionalAuth, likeThought); +router.patch("/:id/unlike", optionalAuth, unlikeThought); + +// --- SECURED ROUTES --- + // Update a thought router.patch( "/:id", @@ -52,8 +57,4 @@ router.patch( // Delete a thought router.delete("/:id", verifyToken, deleteThought); -// Like/Unlike a thought -router.patch("/:id/like", verifyToken, likeThought); -router.patch("/:id/unlike", verifyToken, unlikeThought); - export default router; From 58ca881837b27527ad26badebac520c267cb30df Mon Sep 17 00:00:00 2001 From: violacathrine Date: Tue, 2 Sep 2025 09:00:56 +0200 Subject: [PATCH 9/9] readme --- README.md | 61 ++++++++++++++++++++++++++++++++++++---- pull_request_template.md | 5 +++- todo.txt | 27 ------------------ 3 files changed, 59 insertions(+), 34 deletions(-) delete mode 100644 todo.txt diff --git a/README.md b/README.md index 0f9f073..e4c84d1 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,60 @@ -# 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. +Backend API for sharing happy thoughts. Users can post thoughts, like others' thoughts, and manage their own content. -## Getting started +## Live Demo -Install dependencies with `npm install`, then start the server by running `npm run dev` +**API:** [https://js-project-api-cathi.onrender.com](https://js-project-api-cathi.onrender.com) -## View it live +## Tech Stack -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. +- Node.js & Express +- MongoDB with Mongoose +- JWT authentication +- bcrypt for passwords + +## API Endpoints + +### Authentication +- `POST /auth/register` - Register user +- `POST /auth/login` - Login user + +### Thoughts +- `GET /thoughts` - Get all thoughts +- `POST /thoughts` - Create thought +- `PATCH /thoughts/:id` - Update thought (own only) +- `DELETE /thoughts/:id` - Delete thought (own only) +- `PATCH /thoughts/:id/like` - Like thought +- `PATCH /thoughts/:id/unlike` - Unlike thought + +## Installation + +1. Clone repo and install dependencies: +```bash +npm install +``` + +2. Create `.env` file: +``` +MONGO_URL=mongodb://localhost/happy-thoughts +JWT_SECRET=your-secret-key +PORT=8080 +``` + +3. Start development server: +```bash +npm run dev +``` + +## Features + +- ✅ Anonymous and authenticated posting +- ✅ User authentication with JWT +- ✅ CRUD operations for thoughts +- ✅ Like/unlike functionality +- ✅ Input validation and error handling +- ✅ Pagination and filtering + +## Deployment + +Deployed on Render with MongoDB Atlas database. diff --git a/pull_request_template.md b/pull_request_template.md index fb9fdc3..e4e16a4 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -1 +1,4 @@ -Please include your Render link here. \ No newline at end of file +https://js-project-api-cathi.onrender.com + +https://happythoughtsbyc.netlify.app/ + diff --git a/todo.txt b/todo.txt deleted file mode 100644 index 78a25ec..0000000 --- a/todo.txt +++ /dev/null @@ -1,27 +0,0 @@ -### ✅ Vecka 1 – Grundläggande API med Express - -- [x] API:t har minst tre olika routes (endpoints). -- [x] Route `GET /` returnerar API-dokumentation (t.ex. med Express List Endpoints). -- [x] Minst en route returnerar en lista med flera objekt (array). -- [x] Minst en route returnerar ett enskilt objekt (single element). -- [x] API:t följer RESTful-principer. -- [] Koden är städad och följer principer för clean code. - -#### 🧠 Stretch goals - -- [x] Routes som returnerar ett enskilt objekt hanterar om objektet inte finns och ger användbar information i svaret. -- [x] Implementera pagination med `.slice()` för stora datamängder, och använd query-parametrar som `?page=2`. -- [ ] Lägg till kategori eller taggar för tankar (t.ex. "Food thoughts", "Work thoughts"). -- [ ] Lägg till sortering och filtrering i endpointen som returnerar alla tankar. Exempel: - - [/] Sortera på datum eller antal likes. - - [x] Filtrera på tankar med fler än X hjärtan. - - [ ] Filtrera på tankar nyare än ett visst datum. - - [ ] Filtrera på specifika kategorier (om du har implementerat dem). - -#### 🛠 Tips - -- [ ] Testa dina endpoints med Postman. -- [ ] Gör frekventa commits till GitHub. -- [ ] Kom ihåg skillnaden mellan: - - **Path params**: t.ex. `/thoughts/:id` - - **Query params**: t.ex. `/thoughts?category=work`