diff --git a/data.json b/data.json index a2c844f..8536a42 100644 --- a/data.json +++ b/data.json @@ -1,121 +1,121 @@ [ - { - "_id": "682bab8c12155b00101732ce", + { "message": "Berlin baby", - "hearts": 37, + "hearts": 2, "createdAt": "2025-05-19T22:07:08.999Z", + "createdBy": "64b2f3c6a87e2b12e4567890", "__v": 0 }, { - "_id": "682e53cc4fddf50010bbe739", "message": "My family!", "hearts": 0, "createdAt": "2025-05-22T22:29:32.232Z", + "createdBy": "64b2f3c6a87e2b12e4567890", "__v": 0 }, { - "_id": "682e4f844fddf50010bbe738", "message": "The smell of coffee in the morning....", "hearts": 23, "createdAt": "2025-05-22T22:11:16.075Z", + "createdBy": "64b2f3c6a87e2b12e4567890", "__v": 0 }, { - "_id": "682e48bf4fddf50010bbe737", "message": "Newly washed bedlinen, kids that sleeps through the night.. FINGERS CROSSED 🤞🏼\n", - "hearts": 6, + "hearts": 62, "createdAt": "2025-05-21T21:42:23.862Z", - "__v": 0 + "createdBy": "64b2f3c6a87e2b12e4567890", + "__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", + "createdBy": "64b2f3c6a87e2b12e4567890", "__v": 0 }, { - "_id": "682e23fecf615800105107aa", "message": "cold beer", "hearts": 2, "createdAt": "2025-05-21T19:05:34.113Z", + "createdBy": "64b2f3c6a87e2b12e4567890", "__v": 0 }, { - "_id": "682e22aecf615800105107a9", "message": "My friend is visiting this weekend! <3", "hearts": 6, "createdAt": "2025-05-21T18:59:58.121Z", + "createdBy": "64b2f3c6a87e2b12e4567890", "__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 + "createdBy": "64b2f3c6a87e2b12e4567890", + "__v": 0 }, { - "_id": "682cebbe17487d0010a298b5", "message": "Tacos and tequila🌮🍹", "hearts": 2, "createdAt": "2025-05-19T20:53:18.899Z", + "createdBy": "64b2f3c6a87e2b12e4567890", "__v": 0 }, { - "_id": "682ceb5617487d0010a298b4", "message": "Netflix and late night ice-cream🍦", "hearts": 1, "createdAt": "2025-05-18T20:51:34.494Z", + "createdBy": "64b2f3c6a87e2b12e4567890", "__v": 0 }, { - "_id": "682c99ba3bff2d0010f5d44e", "message": "Summer is coming...", "hearts": 2, "createdAt": "2025-05-20T15:03:22.379Z", - "__v": 0 + "createdBy": "64b2f3c6a87e2b12e4567890", + "__v": 0 }, { - "_id": "682c706c951f7a0017130024", "message": "Exercise? I thought you said extra fries! 🍟😂", "hearts": 14, "createdAt": "2025-05-20T12:07:08.185Z", + "createdBy": "64b2f3c6a87e2b12e4567890", "__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", + "createdBy": "64b2f3c6a87e2b12e4567890", "__v": 0 }, { - "_id": "682c6f0e951f7a0017130022", "message": "Cute monkeys🐒", "hearts": 2, "createdAt": "2025-05-20T12:01:18.308Z", + "createdBy": "64b2f3c6a87e2b12e4567890", "__v": 0 }, { - "_id": "682c6e65951f7a0017130021", "message": "The weather is nice!", "hearts": 0, "createdAt": "2025-05-20T11:58:29.662Z", + "createdBy": "64b2f3c6a87e2b12e4567890", "__v": 0 }, { - "_id": "682bfdb4270ca300105af221", "message": "good vibes and good things", "hearts": 3, "createdAt": "2025-05-20T03:57:40.322Z", + "createdBy": "64b2f3c6a87e2b12e4567890", "__v": 0 }, { - "_id": "682bab8c12155b00101732ce", "message": "Berlin baby", "hearts": 37, "createdAt": "2025-05-19T22:07:08.999Z", + "createdBy": "64b2f3c6a87e2b12e4567890", "__v": 0 } ] \ No newline at end of file diff --git a/endpoints/deleteThought.js b/endpoints/deleteThought.js new file mode 100644 index 0000000..7a3afe8 --- /dev/null +++ b/endpoints/deleteThought.js @@ -0,0 +1,42 @@ +import { Thought } from "../models/thought" + +export const deleteThought = async (req, res) => { + const { id } = req.params + const userId = req.user._id; + + try { + const thought = await Thought.findById(id) + + if (!thought) { + return res.status(404).json({ + success: false, + response: null, + message: 'Thought could not be found' + }) + } + + // Check if the current user owns the thought + if (thought.createdBy.toString() !== userId.toString()) { + return res.status(403).json({ + success: false, + message: "You are not allowed to delete this thought" + }); + } + + await thought.deleteOne(); + + res.status(200).json({ + success: true, + response: thought, + message: 'The thought was deleted' + }) + + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: 'Could not delete thought' + } + ) + } +} \ No newline at end of file diff --git a/endpoints/getHome.js b/endpoints/getHome.js new file mode 100644 index 0000000..57bc954 --- /dev/null +++ b/endpoints/getHome.js @@ -0,0 +1,15 @@ +import listEndpoints from "express-list-endpoints"; + +export const getHome = (app) => (req, res) => { + const endpoints = listEndpoints(app); + res.json({ + message: "Welcome to the Happy Thoughts API", + endpoints, + thoughtQueries: { + minimumHearts: "http://localhost:8080/thoughts?minHearts=10", + sortByHearts: "http://localhost:8080/thoughts?sort=hearts", + pages: "http://localhost:8080/thoughts?page=1", + combined: "http://localhost:8080/thoughts?minHearts=5&sort=hearts&page=1" + } + }); +}; \ No newline at end of file diff --git a/endpoints/getSecrets.js b/endpoints/getSecrets.js new file mode 100644 index 0000000..7f8d447 --- /dev/null +++ b/endpoints/getSecrets.js @@ -0,0 +1,10 @@ +export const getSecrets = (req, res) => { + res.json({ secret: "This is secret" }); +}; + +// app.get("/secrets", authenticateUser) +// app.get("/secrets", (req, res) => { +// res.json({ +// secret: "This is secret" +// }) +// }) \ No newline at end of file diff --git a/endpoints/getThoughtById.js b/endpoints/getThoughtById.js new file mode 100644 index 0000000..79b53b5 --- /dev/null +++ b/endpoints/getThoughtById.js @@ -0,0 +1,41 @@ +import { response } from "express"; +import thoughtData from "../data.json"; +import { Thought } from "../models/thought"; + +export const getThoughtById = async (req, res) => { + const { id } = req.params + + try { + const thought = await Thought.findById(id) + if (!thought) { + return res.status(400).json({ + success: false, + response: null, + message: 'Thought not found' + }) + } + res.status(200).json({ + success: true, + response: thought, + message: 'The flower was found' + }) + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: 'Thought could not be found' + }) + } + +} + +// const thought = thoughtData.find(thought => thought._id === req.params.id); + +// console.log(thought); + +// if (!thought) { +// return res.status(404).send({ error: "Thought not found" }); +// } + +// res.json(thought); +// }; \ No newline at end of file diff --git a/endpoints/getThoughts.js b/endpoints/getThoughts.js new file mode 100644 index 0000000..15e7398 --- /dev/null +++ b/endpoints/getThoughts.js @@ -0,0 +1,41 @@ +import { Thought } from "../models/thought"; +import { getPages } from "../utils/getPages"; +import { getSortedThoughts } from "../utils/getSortedThoughts"; +import { getFilteredThoughts } from "../utils/getFiltredThoughts" + +export const getThoughts = async (req, res) => { + try { + const { minHearts, sort, page } = req.query + let result = await Thought.find() + + // Filters the hearts by the number and above + // URL example: http://localhost:8080/thoughts?minHearts=10 + if (minHearts) { + result = getFilteredThoughts(result, minHearts) + } + + // Sorts the heart in an accending order + //URL exapmle: http://localhost:8080/thoughts?sort=hearts + if (sort === "hearts") { + result = getSortedThoughts(result, true) + } else { + // Always sort by createdAt descending if not sorting by hearts + result = result.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); + } + + // Page function + // URL example: http://localhost:8080/thoughts?page=1 + const pagedResults = getPages(result, page) + const totalPages = Math.ceil(result.length / 10) + + res.json({ + pagedResults, + totalPages + }) + // res.json(pagedResults) + } catch (error) { + console.error("Error fetching thoughts:", error) + res.status(500).json({ error: "Failed to fetch thoughts." }) + } +} + diff --git a/endpoints/patchThought.js b/endpoints/patchThought.js new file mode 100644 index 0000000..95c66cc --- /dev/null +++ b/endpoints/patchThought.js @@ -0,0 +1,44 @@ +import { Thought } from "../models/thought" + +export const patchThought = async (req, res) => { + const { id } = req.params + const { newMessage } = req.body + const userId = req.user._id; + + try { + const thought = await Thought.findById(id); + // const thought = await Thought.findByIdAndUpdate(id, { message: newMessage }, { new: true, runValidators: true }) + + if (!thought) { + return res.status(400).json({ + success: false, + response: null, + message: 'Thought could not be changed' + }); + } + + // Check if the user owns the thought + if (thought.createdBy.toString() !== userId.toString()) { + return res.status(403).json({ + success: false, + message: "You are not allowed to edit this thought" + }); + } + + thought.message = newMessage; + + const updatedThought = await thought.save(); + + res.status(201).json({ + success: true, + response: thought, + message: 'Thought successfully changed' + }); + } catch (error) { + res.status(400).json({ + success: false, + response: error, + message: 'Could not change thought in the database' + }); + } +} \ No newline at end of file diff --git a/endpoints/postLike.js b/endpoints/postLike.js new file mode 100644 index 0000000..4ecb499 --- /dev/null +++ b/endpoints/postLike.js @@ -0,0 +1,33 @@ +import { Thought } from "../models/thought"; + +export const postLike = async (req, res) => { + const { id } = req.params; + + try { + const updatedThought = await Thought.findByIdAndUpdate( + id, + { $inc: { hearts: 1 } }, + { new: true } + ); + + if (!updatedThought) { + return res.status(400).json({ + success: false, + response: null, + message: 'Thought could not be liked' + }); + } + + res.status(201).json({ + success: true, + response: updatedThought, + message: 'Thought successfully liked' + }); + } catch (error) { + res.status(400).json({ + success: false, + response: error, + message: 'Could not like thought to the database' + }); + } +}; \ No newline at end of file diff --git a/endpoints/postSession.js b/endpoints/postSession.js new file mode 100644 index 0000000..7460e5e --- /dev/null +++ b/endpoints/postSession.js @@ -0,0 +1,17 @@ +import { User } from "../models/user"; +import bcrypt from "bcrypt" + +export const postSession = async (req, res) => { + const user = await User.findOne({ + email: req.body.email + }) + + if (user && bcrypt.compareSync(req.body.password, user.password)) { + res.json({ + userId: user._id, + accessToken: user.accessToken + }) + } else { + res.json({ notFound: true }) + } +} \ No newline at end of file diff --git a/endpoints/postThought.js b/endpoints/postThought.js new file mode 100644 index 0000000..84fe9e8 --- /dev/null +++ b/endpoints/postThought.js @@ -0,0 +1,36 @@ +import { Thought } from "../models/thought"; + +export const postThought = async (req, res) => { + const { message } = req.body; + const user = req.user; + + try { + const newThought = new Thought({ + message, + createdBy: user._id + }); + + const savedNewThought = await newThought.save(); + + if (!savedNewThought) { + return res.status(400).json({ + success: false, + response: null, + message: 'Thought could not be saved' + }); + } + + res.status(201).json({ + success: true, + response: savedNewThought, + message: "Thought created" + }); + + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: "Could not post thought" + }); + } +}; \ No newline at end of file diff --git a/endpoints/postUser.js b/endpoints/postUser.js new file mode 100644 index 0000000..7af71a2 --- /dev/null +++ b/endpoints/postUser.js @@ -0,0 +1,25 @@ +import { User } from "../models/user"; +import bcrypt from "bcrypt" + +export const postUser = async (req, res) => { + try { + const { name, email, password } = req.body + const salt = bcrypt.genSaltSync() + const user = new User({ name, email, password: bcrypt.hashSync(password, salt) }) + await user.save() + res.status(201).json({ + success: true, + message: "User created", + id: user._id, + accessToken: user.accessToken, + }) + } catch (error) { + console.error("❌ Error creating user:", error); + + res.status(400).json({ + success: false, + message: "Could not create user", + errors: error + }); + } +} \ No newline at end of file diff --git a/middleware/authenticateUser.js b/middleware/authenticateUser.js new file mode 100644 index 0000000..b7d69b0 --- /dev/null +++ b/middleware/authenticateUser.js @@ -0,0 +1,14 @@ +import { User } from "../models/user.js" + +export 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({ loggedOut: true }) + } +} \ No newline at end of file diff --git a/models/thought.js b/models/thought.js new file mode 100644 index 0000000..4bf048d --- /dev/null +++ b/models/thought.js @@ -0,0 +1,26 @@ +import mongoose from "mongoose"; + +const ThoughtSchema = new mongoose.Schema({ + message: { + type: String, + required: true, + trim: true, + minlength: 5, + maxlength: 140 + }, + hearts: { + type: Number, + default: 0, + }, + createdBy: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + }, + createdAt: { + type: Date, + default: Date.now + } +}); + +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..1ee199d --- /dev/null +++ b/models/user.js @@ -0,0 +1,41 @@ +import mongoose from "mongoose" +import crypto from "crypto" + +const UserSchema = new mongoose.Schema({ + name: { + type: String, + required: true, + unique: true + }, + email: { + type: String, + required: true, + unique: true + }, + password: { + type: String, + required: true + }, + accessToken: { + type: String, + default: () => crypto.randomBytes(128).toString("hex") + } +}) + +export const User = mongoose.model("User", UserSchema); + + +// export 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({ +// loggedOut: true +// }) +// } +// } \ No newline at end of file diff --git a/package.json b/package.json index bf25bb6..5e4fa7d 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,14 @@ "@babel/core": "^7.17.9", "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", + "bcrypt": "^6.0.0", + "bcrypt-nodejs": "^0.0.3", "cors": "^2.8.5", - "express": "^4.17.3", - "nodemon": "^3.0.1" + "crypto": "^1.0.1", + "dotenv": "^16.5.0", + "express": "^4.21.2", + "express-list-endpoints": "^7.1.1", + "mongoose": "^8.15.1", + "nodemon": "^3.1.10" } } diff --git a/server.js b/server.js index f47771b..50b59ed 100644 --- a/server.js +++ b/server.js @@ -1,22 +1,66 @@ +//#region ---- Imports ---- + +import dotenv from "dotenv" import cors from "cors" -import express from "express" +import express, { response } from "express" +import mongoose from "mongoose" + +import { authenticateUser } from "./middleware/authenticateUser.js" +import { resetDatabase } from "./setup/resetDatabase.js" + +import { getHome } from "./endpoints/getHome" +import { getSecrets } from "./endpoints/getSecrets" +import { getThoughtById } from "./endpoints/getThoughtById" +import { getThoughts } from "./endpoints/getThoughts" +import { postLike } from "./endpoints/postLike" +import { postSession } from "./endpoints/postSession" +import { postThought } from "./endpoints/postThought" +import { postUser } from "./endpoints/postUser" +import { patchThought } from "./endpoints/patchThought" +import { deleteThought } from "./endpoints/deleteThought" + +//#endregion + +//#region ---- Set up ---- + +// Runs the env file +dotenv.config(); + +const mongoUrl = process.env.MONGO_URL || 'mongodb://localhost/testing'; +// const mongoUrl = 'mongodb://localhost/testing'; +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: -// PORT=9000 npm start +// The setup of the port const port = process.env.PORT || 8080 const app = express() -// Add middlewares to enable cors and json body parsing +// The middleware app.use(cors()) app.use(express.json()) -// Start defining your routes here -app.get("/", (req, res) => { - res.send("Hello Technigo!") -}) +// Reset the database with: RESET_DB=true npm start +resetDatabase() + +//#endregion + +//#region ---- endpoint ---- -// Start the server +app.get("/", getHome(app)) +app.get("/thoughts", authenticateUser, getThoughts) +app.get("/thoughts/:id", authenticateUser, getThoughtById) +app.get("/secrets", authenticateUser, getSecrets) +app.post("/thoughts", authenticateUser, postThought) +app.post("/thoughts/:id/like", postLike) +app.post("/users", postUser) +app.post("/sessions", postSession) +app.patch("/thoughts/:id", authenticateUser, patchThought) +app.delete('/thoughts/:id', authenticateUser, deleteThought) + +//#endregion + +//#region ---- Starts the server ---- app.listen(port, () => { console.log(`Server running on http://localhost:${port}`) }) + +//#endregion \ No newline at end of file diff --git a/setup/resetDatabase.js b/setup/resetDatabase.js new file mode 100644 index 0000000..7dba691 --- /dev/null +++ b/setup/resetDatabase.js @@ -0,0 +1,16 @@ +import { Thought } from "../models/thought"; +import thoughtData from "../data.json" + +export const resetDatabase = () => { + if (process.env.RESET_DB) { + const seedDatabase = async () => { + console.log("🌱 Resetting and seeding database..."); + await Thought.deleteMany({}); + thoughtData.forEach(thought => { + new Thought(thought).save(); + }); + console.log("✅ Seeding complete."); + }; + seedDatabase(); + } +} \ No newline at end of file diff --git a/todo b/todo new file mode 100644 index 0000000..472eff1 --- /dev/null +++ b/todo @@ -0,0 +1 @@ +[X] Fix seeding on npm run dev \ No newline at end of file diff --git a/utils/getFiltredThoughts.js b/utils/getFiltredThoughts.js new file mode 100644 index 0000000..ad211ef --- /dev/null +++ b/utils/getFiltredThoughts.js @@ -0,0 +1,11 @@ +export const getFilteredThoughts = (thoughts, heartsQuery) => { + let filteredThoughts = thoughts + + if (heartsQuery) { + filteredThoughts = filteredThoughts.filter( + (thought) => Number(thought.hearts) >= Number(heartsQuery) + ) + } + + return filteredThoughts +}; \ No newline at end of file diff --git a/utils/getPages.js b/utils/getPages.js new file mode 100644 index 0000000..3229102 --- /dev/null +++ b/utils/getPages.js @@ -0,0 +1,6 @@ +export const getPages = (result, page = 1, pageLenght = 10) => { + const pageNumber = Number(page) + const start = (pageNumber - 1) * pageLenght + const end = start + pageLenght + return result.slice(start, end) +} \ No newline at end of file diff --git a/utils/getSortedThoughts.js b/utils/getSortedThoughts.js new file mode 100644 index 0000000..08c6238 --- /dev/null +++ b/utils/getSortedThoughts.js @@ -0,0 +1,9 @@ +export const getSortedThoughts = (thoughts, heartsQuery) => { + let sortedThoughts = thoughts + + if (heartsQuery) { + sortedThoughts = [...thoughts].sort((a, b) => b.hearts - a.hearts) + } + + return sortedThoughts +} \ No newline at end of file