-
Notifications
You must be signed in to change notification settings - Fork 52
Happy Thoughts API - Gabriella #34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
e064949
79bb3c7
afb32df
cdc7562
3bfd934
efebf1b
0a9313f
46d9635
f2f1574
ac9537b
65a3e43
9069a0b
f798f15
bab940d
445b9ab
aa1bb14
fc5cf29
aab1e65
6423923
c955e24
706771e
9b9c367
3fdebaa
c5ab35c
7238ae6
b6e0887
eb91d5e
fbc419b
d44b870
5bdd556
b60925d
8339ebc
990688a
b2f0f30
891c8b1
4dfbb95
a30870d
40d9670
f54a00f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| "liveServer.settings.port": 5501 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,35 @@ | ||
| # 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 repository contains the backend API for Happy Thoughts, built with Node.js, Express, and MongoDB. The API handles authentication, authorization, data validation, and all CRUD operations for thoughts and users. | ||
|
|
||
| ## Getting started | ||
| The API is fully RESTful and deployed to Render. | ||
|
|
||
| Install dependencies with `npm install`, then start the server by running `npm run dev` | ||
| ## Live Site: https://happysharing.netlify.app/ | ||
|
|
||
| ## 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. | ||
| ## Features | ||
|
|
||
| - User authentication (sign up & login) | ||
| - Password hashing with bcrypt | ||
| - Token-based authorization | ||
| - Create, read, update & delete thoughts | ||
| - Allow anonymous posting | ||
| - Like thoughts (authenticated & anonymous) | ||
| - Track which users liked which thoughts | ||
| - Fetch thoughts liked by the logged-in user | ||
| - Filtering & sorting thoughts: By date and number of likes | ||
| - Input validation & error handling | ||
| - Secure routes for authenticated actions only | ||
|
|
||
| --- | ||
|
|
||
| ## Tech Stack | ||
|
|
||
| - Node.js | ||
| - Express | ||
| - MongoDB | ||
| - Mongoose | ||
| - bcrypt | ||
| - RESTful API design | ||
| - Render (deployment) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import User from "../models/User"; | ||
|
|
||
|
|
||
| // Global middleware for authentication - To attach req.user everywhere | ||
| // I.e. if there is an accessToken in request header, find the matching user of it and attach it to every request | ||
| export const optionalAuth = async (req, res, next) => { | ||
| try { | ||
| const accessToken = req.headers.authorization; | ||
|
|
||
| if (!accessToken) { | ||
| return next(); | ||
| } | ||
|
|
||
| const matchingUser = await User.findOne({ accessToken: accessToken }); | ||
|
|
||
| if (matchingUser) { | ||
| req.user = matchingUser | ||
| } | ||
|
|
||
| next(); | ||
|
|
||
| } catch(error) { | ||
| console.error("Optional auth error:", error) | ||
| next(); | ||
| } | ||
| }; | ||
|
|
||
|
|
||
| // To be used in routes that should only be accessed by authorized users | ||
| export const authenticateUser = (req, res, next) => { | ||
|
|
||
| if (!req.user) { | ||
| return res.status(401).json({ loggedOut: true }); | ||
| } | ||
| next(); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import mongoose from "mongoose"; | ||
|
|
||
| const ThoughtSchema = new mongoose.Schema({ | ||
| message: { | ||
| type: String, | ||
| required: true, | ||
| minlength: 1, | ||
| maxlength: 140 | ||
| }, | ||
| hearts: [ | ||
| { | ||
| userId: { type: mongoose.Schema.Types.ObjectId, default: null } | ||
| } | ||
| ], | ||
| createdAt: { | ||
| type: Date, | ||
| default: Date.now | ||
| }, | ||
| editToken: { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. smart med både userId och editToken för persistence i samma schema:) |
||
| type: String, | ||
| default: () => crypto.randomUUID() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Du använder crypto.randomUUID men importerar aldrig crypto i den här filen. Fungerar det ändå kanske? |
||
| }, | ||
| // For logged-in users: | ||
| userId: { | ||
| type: mongoose.Schema.Types.ObjectId, | ||
| default: null | ||
| } | ||
| }); | ||
|
|
||
| export default mongoose.model("Thought", ThoughtSchema); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import mongoose from "mongoose"; | ||
| import crypto from "crypto"; | ||
|
|
||
| const UserSchema = new mongoose.Schema({ | ||
| name: { | ||
| type: String, | ||
| required: true | ||
| }, | ||
| email: { | ||
| type: String, | ||
| unique: true, | ||
| required: true | ||
| }, | ||
| password: { | ||
| type: String, | ||
| required: true | ||
| }, | ||
| accessToken : { | ||
| type: String, | ||
| default: () => crypto.randomBytes(128).toString("hex") | ||
| } | ||
| }); | ||
|
|
||
| export default mongoose.model("User", UserSchema); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,198 @@ | ||
| import express from "express"; | ||
| import mongoose from "mongoose"; | ||
| import Thought from "../models/Thought"; | ||
| import { authenticateUser } from "../middlewares/authMiddleware"; | ||
| import dotenv from "dotenv"; | ||
| dotenv.config(); | ||
|
|
||
| // Endpoint is /thoughts | ||
| const router = express.Router(); | ||
|
|
||
|
|
||
| // All thoughts | ||
| router.get("/", async (req, res) => { | ||
|
|
||
| try { | ||
| const { minLikes, sortBy, order } = req.query; | ||
| const sortingOrder = order === "asc" ? 1 : -1; | ||
|
|
||
| // Variable for telling MongoDB how to prepare the data | ||
| const filterAndSort = []; | ||
|
|
||
| // Compute the like count from the hearts array, to use in the filtering | ||
| filterAndSort.push({ | ||
| $addFields: { | ||
| likesCount: { $size: { $ifNull: ["$hearts", []] } } // Handle empty/null hearts | ||
| } | ||
| }); | ||
|
|
||
| /* --- Functionality for filtering --- */ | ||
| if (minLikes) { | ||
| filterAndSort.push({ | ||
| $match: { likesCount: { $gte: Number(minLikes) } } //gte = Greater than or equals to | ||
| }); | ||
| } | ||
|
|
||
| /* --- Functionality for sorting --- */ | ||
| const sortCriteria = {}; | ||
| if (sortBy === "date") { | ||
| sortCriteria.createdAt = sortingOrder; | ||
| } else if (sortBy === "likes") { | ||
| sortCriteria.likesCount = sortingOrder; | ||
| sortCriteria.createdAt = -1; // Secondary sort by date | ||
| } else { | ||
| sortCriteria.createdAt = -1; // Default sorting | ||
| } | ||
|
|
||
| filterAndSort.push({ $sort: sortCriteria }); | ||
|
|
||
| /// Remove editToken to prevent it being exposed to users | ||
| filterAndSort.push({ | ||
| $project: { editToken: 0 } | ||
| }); | ||
|
|
||
| /* --- Execute filter and sorting --- */ | ||
| const thoughts = await Thought.aggregate(filterAndSort); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Var tvungen att kolla upp "aggregate" aldrig sett förut😅 tack för att jag fick lära mig nåt nytt:) verkar dock kunna bli problem vid equals eftersom aggregate inte returnerar mongoose dokument utan vanlig js-objekt och equals hör ihop med mongoose dokument om jag fattade rätt. Nåt att kolla på kanske. |
||
|
|
||
| const result = thoughts.map((thought) => { | ||
| const isCreator = req.user && thought.userId?.equals(req.user._id); | ||
| delete thought.userId; // Remove userId (after isCreator is computed) to prevent it from being exposed on front-end | ||
| return { | ||
| ...thought, | ||
| isCreator | ||
| }; | ||
| }); | ||
| res.json(result); | ||
| } catch (error) { | ||
| console.error("GET /thoughts error:", error); | ||
| res.status(500).json({ message: "Failed to fetch thoughts", error: error.message }); | ||
| } | ||
| }); | ||
|
|
||
|
|
||
| // Post a thought | ||
| router.post("/", async (req, res) => { | ||
| try { | ||
| const message = req.body.message; | ||
|
|
||
| // Use mongoose model to create a database entry | ||
| const newThought = new Thought({ | ||
| message, | ||
| userId: req.user ? req.user._id : null | ||
| }); | ||
|
|
||
| const savedThought = await newThought.save(); | ||
|
|
||
| res.status(201).json(savedThought); | ||
| } catch(error) { | ||
| res.status(400).json({ | ||
| message: "Failed to save thought to database", | ||
| error: error.message | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
|
|
||
| // Delete a thought | ||
| router.delete("/id/:id", async (req, res) => { | ||
| const id = req.params.id; | ||
|
|
||
| // Error handling for invalid id input | ||
| if (!mongoose.Types.ObjectId.isValid(id)) { | ||
| return res.status(400).json({ error: `Invalid id: ${id}` }); | ||
| } | ||
|
|
||
| try { | ||
| const deletedThought = await Thought.findByIdAndDelete(id); | ||
|
|
||
| // Error handling for no ID match | ||
| if(!deletedThought) { | ||
| return res.status(404).json({ error: `Thought with id ${id} not found` }); | ||
| } | ||
|
|
||
| res.json(deletedThought); | ||
|
|
||
| } catch(error) { | ||
| res.status(500).json({error: error.message}); | ||
| } | ||
| }); | ||
|
|
||
|
|
||
| // Update the like count of a thought | ||
| router.patch("/id/:id/like", async (req, res) => { | ||
| try { | ||
| const { id } = req.params; | ||
|
|
||
| if (!mongoose.Types.ObjectId.isValid(id)) { | ||
| return res.status(400).json({ error: `Invalid id: ${id}` }); | ||
| } | ||
|
|
||
| const updatedThought = await Thought.findByIdAndUpdate( | ||
| id, | ||
| { $push: { hearts: { userId: req.user ? req.user._id : null } } }, //Ensures the updated heart count gets returned, and that schema validation also is performed | ||
| { new: true, runValidators: true } | ||
| ); | ||
|
|
||
| // Error handling for no ID match | ||
| if(!updatedThought) { | ||
| return res.status(404).json({ error: `Thought with id ${id} not found` }); | ||
| } | ||
|
|
||
| res.json(updatedThought); | ||
|
|
||
| } catch(error) { | ||
| res.status(500).json({ error: error.message }); | ||
| } | ||
| }); | ||
|
|
||
|
|
||
| // Update the message of a thought | ||
| router.patch("/id/:id/message", async (req, res) => { | ||
| const { id } = req.params; | ||
| const { message } = req.body; | ||
|
|
||
| // Error handling for invalid id input | ||
| if (!mongoose.Types.ObjectId.isValid(id)) { | ||
| return res.status(400).json({ error: `Invalid id: ${id}` }); | ||
| } | ||
|
|
||
| try { | ||
| const updatedThought = await Thought.findByIdAndUpdate( | ||
| id, | ||
| { message }, | ||
| { new: true, runValidators: true} //Ensures the updated message gets returned, and that schema validation also is performed on the new message | ||
| ); | ||
|
|
||
| // Error handling for no ID match | ||
| if(!updatedThought) { | ||
| return res.status(404).json({error: `Thought with id ${id} not found`}); | ||
| } | ||
|
|
||
| res.json(updatedThought); | ||
|
|
||
| } catch(err) { | ||
| res.status(500).json({error: err.message}); | ||
| } | ||
| }); | ||
|
|
||
|
|
||
| /* --- Authenticated only routes ---*/ | ||
|
|
||
|
|
||
| // Liked thoughts | ||
| router.get("/liked", authenticateUser, async (req, res) => { | ||
| try { | ||
| const likedThoughts = await Thought | ||
| .find({ "hearts.userId": req.user._id }) | ||
| .sort({ createdAt: -1 }); | ||
|
|
||
| res.json(likedThoughts); | ||
|
|
||
| } catch (error) { | ||
| console.error("GET /thoughts error:", error); | ||
| res.status(500).json({ message: "Failed to fetch liked thoughts", error: error.message }); | ||
| } | ||
| }); | ||
|
|
||
|
|
||
| export default router; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Jag älskar hur du har gjort det här tvåstegsmönstret med auth och optionalAuth – det är väldigt kreativt, och det verkar fungera precis så som du vill att det ska.
Men om du skulle vilja använda Bearer, kanske det kan inkluderas genom att först definiera en const authHeader och en const accessToken.