diff --git a/data.json b/data.json index a2c844f..fe3771d 100644 --- a/data.json +++ b/data.json @@ -1,121 +1,86 @@ [ - { - "_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": 1, + "message": "Today is a beautiful day! ๐ŸŒž", + "hearts": 5, + "createdAt": "2025-06-15T10:00:00Z", + "category": "Life" }, { - "_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": 2, + "message": "I made progress on my project!", + "hearts": 10, + "createdAt": "2025-06-14T15:30:00Z", + "category": "Work" }, { - "_id": "682e23fecf615800105107aa", - "message": "cold beer", + "id": 3, + "message": "I had the best ramen today ๐Ÿœ", "hearts": 2, - "createdAt": "2025-05-21T19:05:34.113Z", - "__v": 0 + "createdAt": "2025-06-13T20:00:00Z", + "category": "Food" }, { - "_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": 4, + "message": "I made progress on my project!", + "hearts": 10, + "createdAt": "2025-06-14T15:30:00Z", + "category": "Work" }, { - "_id": "682cebbe17487d0010a298b5", - "message": "Tacos and tequila๐ŸŒฎ๐Ÿน", + "id": 5, + "message": "I had the best ramen today ๐Ÿœ", "hearts": 2, - "createdAt": "2025-05-19T20:53:18.899Z", - "__v": 0 + "createdAt": "2025-06-13T20:00:00Z", + "category": "Food" }, { - "_id": "682ceb5617487d0010a298b4", - "message": "Netflix and late night ice-cream๐Ÿฆ", - "hearts": 1, - "createdAt": "2025-05-18T20:51:34.494Z", - "__v": 0 + "id": 6, + "message": "I learned something new today! ๐Ÿ“š", + "hearts": 8, + "createdAt": "2025-06-12T12:00:00Z", + "category": "Education" }, { - "_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": 7, + "message": "I went for a long walk in the park ๐ŸŒณ", + "hearts": 3, + "createdAt": "2025-06-11T08:30:00Z", + "category": "Health" }, { - "_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": 8, + "message": "I had a great time with friends last night! ๐ŸŽ‰", + "hearts": 12, + "createdAt": "2025-06-10T22:00:00Z", + "category": "Social" }, { - "_id": "682c6f0e951f7a0017130022", - "message": "Cute monkeys๐Ÿ’", - "hearts": 2, - "createdAt": "2025-05-20T12:01:18.308Z", - "__v": 0 + "id": 9, + "message": "I finished reading a fantastic book ๐Ÿ“–", + "hearts": 6, + "createdAt": "2025-06-09T18:45:00Z", + "category": "Leisure" }, { - "_id": "682c6e65951f7a0017130021", - "message": "The weather is nice!", - "hearts": 0, - "createdAt": "2025-05-20T11:58:29.662Z", - "__v": 0 + "id": 10, + "message": "I tried a new recipe and it was delicious! ๐Ÿฝ๏ธ", + "hearts": 4, + "createdAt": "2025-06-08T14:15:00Z", + "category": "Food" }, { - "_id": "682bfdb4270ca300105af221", - "message": "good vibes and good things", - "hearts": 3, - "createdAt": "2025-05-20T03:57:40.322Z", - "__v": 0 + "id": 11, + "message": "I completed a challenging workout today! ๐Ÿ’ช", + "hearts": 7, + "createdAt": "2025-06-07T16:00:00Z", + "category": "Health" }, { - "_id": "682bab8c12155b00101732ce", - "message": "Berlin baby", - "hearts": 37, - "createdAt": "2025-05-19T22:07:08.999Z", - "__v": 0 + "id": 12, + "message": "I had a productive day at work! ๐Ÿ–ฅ๏ธ", + "hearts": 9, + "createdAt": "2025-06-06T11:30:00Z", + "category": "Work" } -] \ No newline at end of file +] diff --git a/package.json b/package.json index bf25bb6..fc8a97c 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,15 @@ "@babel/core": "^7.17.9", "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", + "bcrypt": "^6.0.0", "cors": "^2.8.5", - "express": "^4.17.3", - "nodemon": "^3.0.1" + "crypto": "^1.0.1", + "dotenv": "^17.2.1", + "e": "^0.2.33", + "express": "^4.21.2", + "express-list-endpoints": "^7.1.1", + "mongoose": "^8.17.1", + "nodemon": "^3.0.1", + "xpress": "^2.4.6" } } diff --git a/server.js b/server.js index f47771b..7f58648 100644 --- a/server.js +++ b/server.js @@ -1,22 +1,296 @@ -import cors from "cors" -import express from "express" +import cors from "cors"; +import express from "express"; +import listEndpoints from "express-list-endpoints"; +import fs, { access } from "fs"; +import mongoose from "mongoose"; +import dotenv from "dotenv"; +import bcrypt from "bcrypt"; +import crypto from "crypto"; + +// Load environment variables from .env file +// This allows to set environment variables like PORT and MONGO_URL +// in a .env file for local development +dotenv.config(); // Defines the port the app will run on. Defaults to 8080, but can be overridden // when starting the server. Example command to overwrite PORT env variable value: // PORT=9000 npm start -const port = process.env.PORT || 8080 -const app = express() +const port = process.env.PORT || 8080; +const app = express(); // Add middlewares to enable cors and json body parsing -app.use(cors()) -app.use(express.json()) +app.use(cors({ + origin: ["http://localhost:5174", "https://happythoughtsappbydanu.netlify.app"], // add your deployed FE origin here later + methods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"], + allowedHeaders: ["Content-Type", "Authorization"], + // credentials: true, // only if you ever use cookies +})); +app.use(express.json()); + +//let thoughts = JSON.parse(fs.readFileSync('./data.json')); + +const mongoUrl = + process.env.MONGO_URL || "mongodb://localhost:27017/happythoughts"; + +// Connect to MongoDB +mongoose.connect(mongoUrl, { useNewUrlParser: true, useUnifiedTopology: true }); +mongoose.Promise = Promise; + +// ----- Mongoose Schema ----- +const thoughtSchema = new mongoose.Schema({ + message: { + type: String, + required: [true, "Message is required"], + minlength: [5, "Message must be at least 5 characters"], + maxlength: [140, "Message must be max 140 characters"], + }, + hearts: { + type: Number, + default: 0, + }, + category: { + type: String, + default: "General", + }, + createdBy: { + type: String, + required: true }, + createdAt: { + type: Date, + default: () => new Date(), + }, +}); + +const Thought = mongoose.model("Thought", thoughtSchema); +//User model +const userSchema = new mongoose.Schema({ + username: { + type: String, + required: [true, "Username is required"], + unique: true, + minlength: [3, "Username must be at least 3 characters"], + maxlength: [20, "Username must be max 20 characters"], + }, + password: { + type: String, + required: [true, "Password is required"], + minlength: [6, "Password must be at least 6 characters"], + }, + accessToken: { + type: String, + default: () => crypto.randomBytes(16).toString("hex"), + }, +}); + +const User = mongoose.model("User", userSchema); + +// ----- Middleware for authentication ----- +const authenticateUser = async (req, res, next) => { + const raw = req.header("Authorization") || ""; + // accept "Bearer " OR raw "" + const accessToken = raw.startsWith("Bearer ") ? raw.slice(7) : raw; + + try { + const user = await User.findOne({ accessToken }); + if (user) { + req.user = user; + next(); + } else { + res.status(401).json({ message: "Please log in" }); + } + } catch (err) { + res.status(403).json({ message: "Access denied" }); + } +}; -// Start defining your routes here +// ----- User Registration ----- +app.post("/register", async (req, res) => { + try { + const { username, password } = req.body; + + if (password.length < 5) { + return res + .status(400) + .json({ message: "Password must be at least 5 characters long" }); + } + + const salt = bcrypt.genSaltSync(); + const hashedPassword = bcrypt.hashSync(password, salt); + + const newUser = await new User({ + username, + password: hashedPassword, + }).save(); + + res.status(201).json({ + username: newUser.username, + accessToken: newUser.accessToken, + }); + } catch (err) { + if (err.code === 11000) { + res.status(400).json({ message: "That username already exists" }); + } else { + res.status(500).json({ message: "Internal server error", error: err }); + } + } +}); +// ----- User Login ----- +app.post("/login", async (req, res) => { + const { username, password } = req.body; + const user = await User.findOne({ username }); + + if (!user) { + return res + .status(400) + .json({ message: "Username or password is incorrect" }); + } + + if (bcrypt.compareSync(password, user.password)) { + res.json({ + username: user.username, + accessToken: user.accessToken, + }); + } else { + res.status(400).json({ message: "Username or password is incorrect" }); + } +}); + +// API Docs----- Routes ----- app.get("/", (req, res) => { - res.send("Hello Technigo!") -}) + res.json({ + message: "Welcome to the Happy Thoughts API ๐Ÿ’ฌ", + endpoints: listEndpoints(app), + }); +}); + +// Get all thoughts (with filtering, sorting, pagination) +app.get("/thoughts", async (req, res) => { + try { + const { heartsMin, category, sortBy, page, limit } = req.query; + let query = {}; + + if (heartsMin) { + query.hearts = { $gte: parseInt(heartsMin) }; + } + if (category) { + query.category = category; + } + + let sort = {}; + if (sortBy === "hearts") sort.hearts = -1; + if (sortBy === "date") sort.createdAt = -1; + + const pageInt = parseInt(page) || 1; + const limitInt = parseInt(limit) || 20; + const skip = (pageInt - 1) * limitInt; + + const thoughts = await Thought.find(query) + .sort(sort) + .skip(skip) + .limit(limitInt); + + res.json({ + page: pageInt, + results: thoughts, + }); + } catch (err) { + res.status(500).json({ error: "Failed to fetch thoughts" }); + } +}); + +// Get single thought +app.get("/thoughts/:id", 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 { + res.status(400).json({ error: "Invalid ID" }); + } +}); + +// Create new thought (authenticated) +app.post("/thoughts", authenticateUser, async (req, res) => { + try { + const { message, category } = req.body; + const newThought = new Thought({ + message, + category, + createdBy: req.user.username, + }); + + const savedThought = await newThought.save(); + res.status(201).json(savedThought); + } catch (err) { + res.status(400).json({ message: "Could not save thought", error: err }); + } +}); + +// Update a thought +app.patch("/thoughts/:id", authenticateUser, async (req, res) => { + try { + const thought = await Thought.findById(req.params.id); + + if (!thought) { + return res.status(404).json({ message: "Thought not found" }); + } + + if (thought.createdBy !== req.user.username) { + return res + .status(403) + .json({ message: "Not allowed to edit this thought" }); + } + + thought.message = req.body.message || thought.message; + await thought.save(); + res.json(thought); + } catch (err) { + res.status(400).json({ message: "Could not update thought", error: err }); + } +}); + +// Delete a thought +app.delete("/thoughts/:id", authenticateUser, async (req, res) => { + try { + const thought = await Thought.findById(req.params.id); + + if (!thought) { + return res.status(404).json({ message: "Thought not found" }); + } + + if (thought.createdBy !== req.user.username) { + return res + .status(403) + .json({ message: "Not allowed to delete this thought" }); + } + + await thought.deleteOne(); + res.json({ message: "Thought deleted successfully" }); + } catch (err) { + res.status(400).json({ message: "Could not delete thought", error: err }); + } +}); + +// Like a thought +app.post("/thoughts/:id/like", async (req, res) => { + try { + const thought = await Thought.findByIdAndUpdate( + req.params.id, + { $inc: { hearts: 1 } }, + { new: true } + ); + if (!thought) { + return res.status(404).json({ error: "Thought not found" }); + } + res.json(thought); + } catch (err) { + res.status(400).json({ error: err.message }); + } +}); // Start the server app.listen(port, () => { - console.log(`Server running on http://localhost:${port}`) -}) + console.log(`Server running on http://localhost:${port}`); +});