diff --git a/data.json b/data.json index a2c844f..be839a2 100644 --- a/data.json +++ b/data.json @@ -1,118 +1,101 @@ [ - { - "_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 + "__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 + "__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 + "__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", diff --git a/middleware/authMiddleware.js b/middleware/authMiddleware.js new file mode 100644 index 0000000..5f2c352 --- /dev/null +++ b/middleware/authMiddleware.js @@ -0,0 +1,46 @@ +import { User } from '../models/User.js' + + +export const authenticateUser = async (req, res, next) => { + try { + const accessToken = req.header("Authorization") + const user = await User.findOne({ accessToken: accessToken }) + if (user) { + req.user = user + next() + } else { + res.status(401).json({ + message: "Authentication missing or invalid.", + loggedOut: true + }) + } + } catch (error) { + res.status(500).json({ + message: "Internal server error", + error: error.message + }); + } +} + +export const authenticateUserLike = async (req, res, next) => { + try { + const accessToken = req.header("Authorization") + if (accessToken) { + const user = await User.findOne({ accessToken: accessToken }) + if (user) { + req.user = user + } else { + return res.status(401).json({ + message: "Authentication missing or invalid.", + loggedOut: true + }) + } + } + next() + } catch (error) { + res.status(500).json({ + message: "Internal server error", + error: error.message + }) + } +} \ No newline at end of file diff --git a/models/Like.js b/models/Like.js new file mode 100644 index 0000000..3d35ca1 --- /dev/null +++ b/models/Like.js @@ -0,0 +1,24 @@ +import mongoose from "mongoose" + +const likeSchema = new mongoose.Schema({ + user: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true + }, + thought: { + type: mongoose.Schema.Types.ObjectId, + ref: "Thought", + required: true + }, + createdAt: { + type: Date, + default: Date.now + } +}) + +// will make sure a user can only like a thought one time +// +likeSchema.index({ user: 1, thought: 1 }, { unique: true }) + +export const Like = mongoose.model("Like", likeSchema) \ No newline at end of file diff --git a/models/Thought.js b/models/Thought.js new file mode 100644 index 0000000..ef50fa2 --- /dev/null +++ b/models/Thought.js @@ -0,0 +1,27 @@ +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 + }, + user: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true + }, + +}) + +export const Thought = mongoose.model("Thought", thoughtSchema) \ No newline at end of file diff --git a/models/User.js b/models/User.js new file mode 100644 index 0000000..bc1c205 --- /dev/null +++ b/models/User.js @@ -0,0 +1,28 @@ +import mongoose from "mongoose" +import crypto from "crypto" + + +// Create a schema for the users +const userSchema = new mongoose.Schema({ + name: { + type: String, + required: true, + + }, + email: { + type: String, + required: true, + unique: true + }, + password: { + type: String, + required: true + }, // Passwords should be hashed in a real application + accessToken: { + type: String, + default: crypto.randomBytes(128).toString("hex") + } // Generate a random access token + +}) + +export const User = mongoose.model("User", userSchema) diff --git a/package.json b/package.json index bf25bb6..eba631c 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,13 @@ "@babel/core": "^7.17.9", "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", + "bcrypt-nodejs": "^0.0.3", + "bcryptjs": "^3.0.2", "cors": "^2.8.5", + "dotenv": "^16.5.0", "express": "^4.17.3", - "nodemon": "^3.0.1" + "express-list-endpoints": "^7.1.1", + "mongoose": "^8.15.1", + "nodemon": "^3.1.10" } } diff --git a/routes/thoughtsRoutes.js b/routes/thoughtsRoutes.js new file mode 100644 index 0000000..a3d7b15 --- /dev/null +++ b/routes/thoughtsRoutes.js @@ -0,0 +1,314 @@ +import express from 'express' +import mongoose from 'mongoose' +import { Thought } from '../models/Thought.js' +import { authenticateUser, authenticateUserLike } from '../middleware/authMiddleware.js' +import { Like } from '../models/Like.js' + +const router = express.Router() +// Get all thoughts + + +//endpoint actually "/thoughts" +router.get("/", async (req, res) => { + const { likes, minLikes } = req.query + + const query = {} + + //Make sure there is error handelning when parameter is not a number + + if (likes !== undefined) { + const numLikes = +likes + if (isNaN(numLikes)) { + return res.status(400).json({ error: "Query parameter 'likes' must be a number." }) + } + query.hearts = numLikes + } + + if (minLikes !== undefined) { + const numMinLikes = +minLikes + if (isNaN(numMinLikes)) { + return res.status(400).json({ error: "Query parameter 'likes' must be a number." }) + } + query.hearts = { $gte: numMinLikes } + } + + try { + const filteredThoughts = await Thought.find(query) + + if (filteredThoughts.length === 0) { + return res.status(404).json({ + success: false, + response: [], + message: "No thougth found for that query. Please try another one" + }) + } + res.status(200).json({ + success: true, + response: filteredThoughts + }) + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: "Server error! Failed to fetch thoughts." + }) + } +}) + +// endpoint for getting liked thoughts actually "/thoughts/liked" +router.get("/liked", authenticateUser, async (req, res) => { + const { minLikes } = req.query + const userId = req.user._id + + try { + // 1. Find all likes by this user + const likes = await Like.find({ user: userId }).select('thought') + // 2. Extract thought IDs + const thoughtIds = likes.map(like => like.thought) + + // 3. Build query to find those thoughts + const query = { _id: { $in: thoughtIds } } + if (minLikes !== undefined) { + const min = parseInt(minLikes) + if (isNaN(min)) { + return res.status(400).json({ + success: false, + message: "Query parameter 'minLikes' must be a number.", + }) + } + query.hearts = { $gte: min } + } + + // 4. Find and return the liked thoughts + const likedThoughts = await Thought.find(query) + res.status(200).json({ + success: true, + response: likedThoughts, + }) + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: "Could not get liked thoughts.", + }) + } +}) + +// endpoint for getting one thought actually "/thoughts/:id" +router.get("/:id", async (req, res) => { + const { id } = req.params + + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ + success: false, + response: [], + message: "Invalid id" + }) + } + + try { + const thought = await Thought.findById(id) + + if (!thought) { + return res.status(404).json({ + success: false, + response: [], + message: "No thougth found" + }) + } + + res.status(200).json({ + success: true, + response: thought + }) + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: "Server error! Failed to fetch thoughts." + }) + } +}) + + + + + +// endpoint for creating a thought actually "/thoughts" +router.post("/", authenticateUser, async (req, res) => { + const { message } = req.body + + try { + const newThought = await new Thought({ + message, + user: req.user._id + }).save() + + + res.status(201).json({ + success: true, + response: newThought, + message: "Thought was successfully created" + }) + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: "Couldn't create thought" + }) + } +}) + + + +//delete a thought endpoint actually "/thoughts/:id" +router.delete("/:id", authenticateUser, async (req, res) => { + const { id } = req.params + + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ + success: false, + response: [], + message: "Could not delete! Invalid id" + }) + } + + try { + //Check if this message really belong to the user thats logged in + const thought = await Thought.findByIdAndDelete({ + _id: id, + user: req.user._id + }) + + if (!thought) { + return res.status(404).json({ + success: false, + response: [], + message: "No thougth found" + }) + } + + res.status(200).json({ + success: true, + response: thought, + message: "Was successfully deleted" + }) + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: "Server error! Failed to fetch thoughts." + }) + } +}) + + +// endpoint for liking a thought actually "/thoughts/:id/like" +router.patch("/:id/like", authenticateUserLike, async (req, res) => { + const { id } = req.params + + try { + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ + success: false, + response: [], + message: "Invalid id" + }) + } + + // Check if the user has already liked this thought + if (req.user) { + const existingLike = await Like.findOne({ + user: req.user._id, + thought: id + }) + + if (existingLike) { + return res.status(400).json({ + success: false, + response: null, + message: "You have already liked this thought" + }) + } else { + // Create a new like if the user hasn't liked this thought yet + await new Like({ + user: req.user._id, + thought: id + }).save() + } + } + + const thought = await Thought.findByIdAndUpdate( + id, + { $inc: { hearts: 1 } }, + { new: true, runValidators: true } + ) + + if (!thought) { + return res.status(404).json({ + success: false, + response: null, + message: "Thought not found" + }) + } + + + + res.status(200).json({ + success: true, + response: thought, + message: "Updated" + }) + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: "Server Error! Failed to update Likes", + + }) + } +}) + + + +//for updating a thought message (actuallt "/thoughts/:id/edit") +router.patch("/:id/edit", authenticateUser, async (req, res) => { + const { id } = req.params + const { message: newMessage } = req.body + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ + success: false, + response: [], + message: "Invalid id" + }) + } + + try { + const thought = await Thought.findByIdAndUpdate({ + _id: id, + user: req.user._id + }, { message: newMessage }, { new: true, runValidators: true }) + + if (!thought) { + return res.status(404).json({ + success: false, + response: null, + message: "Thought not found" + }) + } + res.status(200).json({ + success: true, + response: thought, + message: "Updated" + }) + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: "Server Error! Failed to update Message", + + }) + } +}) + +export default router \ No newline at end of file diff --git a/routes/userRoutes.js b/routes/userRoutes.js new file mode 100644 index 0000000..37c5576 --- /dev/null +++ b/routes/userRoutes.js @@ -0,0 +1,64 @@ + +import express from 'express' +import bcrypt from 'bcryptjs'// Use bcryptjs for compatibility? +import { User } from '../models/User.js' + + +const router = express.Router() + +//Create a new user (registration endpoint actually "/users/signup") +router.post("/signup", async (req, res) => { + try { + const { name, password } = req.body + //make sure email is lowercase + const email = req.body.email.trim().toLowerCase() + const salt = bcrypt.genSaltSync() + const user = new User({ + name, + email, + password: bcrypt.hashSync(password, salt) // Hash the password before saving + }) + //await to not send response before database finished saving + await user.save() + + res.status(201).json({ + success: true, + message: "User created successfully", + id: user._id, + accessToken: user.accessToken + + }) + + } catch (error) { + res.status(400).json({ + success: false, + response: error, + message: "Failed to create user. Please check your input." + }) + } +}) + +//Login endpoint actually "/users/login" +router.post("/login", async (req, res) => { + //make sure email is lowercase + const email = req.body.email.trim().toLowerCase() + const user = await User.findOne({ email }) + + if (user && bcrypt.compareSync(req.body.password, user.password)) { + res.status(200).json({ + success: true, + accessToken: user.accessToken, + userId: user._id, + message: "Login successful", + }) + } else { + //Login failed + res.status(401).json({ + success: false, + message: "User not found or password is incorrect", + }); + } +}) + + +export default router \ No newline at end of file diff --git a/server.js b/server.js index f47771b..f554f2d 100644 --- a/server.js +++ b/server.js @@ -1,5 +1,21 @@ import cors from "cors" import express from "express" +import listEndpoints from "express-list-endpoints" +import mongoose from "mongoose" + +import userRoutes from "./routes/userRoutes.js" +import thoughtRoutes from "./routes/thoughtsRoutes.js" + +import dotenv from "dotenv" + +// Import the data from the JSON file +// import thoughtsData from "./data.json" + + +dotenv.config() + +const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/thoughts" +mongoose.connect(mongoUrl) // 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,11 +27,31 @@ const app = express() app.use(cors()) app.use(express.json()) + +// if (process.env.RESET_DB) { +// const seedDatabase = async () => { +// await Thought.deleteMany({}) +// thoughtsData.forEach(thought => { +// new Thought(thought).save() +// }) +// } +// seedDatabase() +// } + + // Start defining your routes here app.get("/", (req, res) => { - res.send("Hello Technigo!") + const endpoints = listEndpoints(app) + res.json({ + message: "Welcome to the Happy Thoughts API", + endpoints: endpoints + }) }) + +app.use("/users", userRoutes) +app.use("/thoughts", thoughtRoutes) + // Start the server app.listen(port, () => { console.log(`Server running on http://localhost:${port}`) diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..e69de29