diff --git a/data.json b/data.json index a2c844f..11ff432 100644 --- a/data.json +++ b/data.json @@ -1,5 +1,5 @@ [ - { + { "_id": "682bab8c12155b00101732ce", "message": "Berlin baby", "hearts": 37, @@ -7,7 +7,7 @@ "__v": 0 }, { - "_id": "682e53cc4fddf50010bbe739", + "_id": "682e53cc4fddf50010bbe739", "message": "My family!", "hearts": 0, "createdAt": "2025-05-22T22:29:32.232Z", @@ -25,7 +25,7 @@ "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", @@ -53,7 +53,7 @@ "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", @@ -74,7 +74,7 @@ "message": "Summer is coming...", "hearts": 2, "createdAt": "2025-05-20T15:03:22.379Z", - "__v": 0 + "__v": 0 }, { "_id": "682c706c951f7a0017130024", @@ -118,4 +118,4 @@ "createdAt": "2025-05-19T22:07:08.999Z", "__v": 0 } -] \ No newline at end of file +] diff --git a/package.json b/package.json index bf25bb6..e465568 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,12 @@ "@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": "^17.2.0", "express": "^4.17.3", + "express-list-endpoints": "^7.1.1", + "mongoose": "^8.16.4", "nodemon": "^3.0.1" } } diff --git a/server.js b/server.js index f47771b..7bb0f50 100644 --- a/server.js +++ b/server.js @@ -1,22 +1,234 @@ -import cors from "cors" -import express from "express" +import express from "express"; +import listEndpoints from "express-list-endpoints"; +import cors from "cors"; +import mongoose from "mongoose"; +import crypto from "crypto"; +import bcrypt from "bcrypt"; -// 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() +// Setup +const port = process.env.PORT || 8080; +const app = express(); +const mongoURL = process.env.mongoURL || "mongodb://127.0.0.1/happy-thoughts"; +mongoose.connect(mongoURL); +mongoose.Promise = Promise; -// 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 +// Schemas +const UserSchema = new mongoose.Schema({ + email: { + type: String, + required: [true, "Email is required"], + unique: true, + match: [/.+@.+\..+/, "Invalid email format"], + }, + password: { + type: String, + required: true, + minlength: 6, + }, + accessToken: { + type: String, + default: () => crypto.randomBytes(128).toString("hex"), + }, +}); + +const ThoughtSchema = new mongoose.Schema({ + message: { + type: String, + required: [true, "Message is required"], + minlength: 5, + maxlength: 140, + }, + hearts: { + type: Number, + default: 0, + }, + createdAt: { + type: Date, + default: Date.now, + }, + createdBy: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + default: null, + }, +}); + +const User = mongoose.model("User", UserSchema); +const Thought = mongoose.model("Thought", ThoughtSchema); + +// Auth middleware +const authenticateUser = async (req, res, next) => { + const accessToken = req.header("Authorization"); + try { + const user = await User.findOne({ accessToken }); + if (user) { + req.user = user; + next(); + } else { + res.status(401).json({ error: "Please log in to access this resource" }); + } + } catch { + res.status(401).json({ error: "Invalid request" }); + } +}; + +// Routes app.get("/", (req, res) => { - res.send("Hello Technigo!") -}) + res.json({ + message: "Welcome to Oscar's Thoughts API!", + endpoints: listEndpoints(app), + }); +}); + +app.get("/thoughts", async (req, res) => { + const { page = 1, limit = 5 } = req.query; + try { + const totalThoughts = await Thought.countDocuments(); + const thoughts = await Thought.find() + .sort({ createdAt: -1 }) + .skip((page - 1) * limit) + .limit(Number(limit)); + res.json({ + page: Number(page), + totalThoughts, + totalPages: Math.ceil(totalThoughts / limit), + results: thoughts, + }); + } catch { + res.status(500).json({ error: "Could not fetch thoughts" }); + } +}); + +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" }); + } +}); + +app.post("/thoughts", async (req, res) => { + const { message } = req.body; + const accessToken = req.header("Authorization"); + + try { + let createdBy = null; + if (accessToken) { + const user = await User.findOne({ accessToken }); + if (user) { + createdBy = user._id; + } + } + + const newThought = new Thought({ message, createdBy }); + const savedThought = await newThought.save(); + res.status(201).json(savedThought); + } catch (err) { + res.status(400).json({ error: err.message }); + } +}); + +app.post("/thoughts/:id/like", async (req, res) => { + try { + const updated = await Thought.findByIdAndUpdate( + req.params.id, + { $inc: { hearts: 1 } }, + { new: true } + ); + if (!updated) return res.status(404).json({ error: "Thought not found" }); + res.status(200).json(updated); + } catch { + res.status(400).json({ error: "Invalid ID" }); + } +}); + +app.patch("/thoughts/:id", authenticateUser, 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.createdBy || + thought.createdBy.toString() !== req.user._id.toString() + ) { + return res + .status(403) + .json({ error: "Not allowed to edit this thought" }); + } + thought.message = req.body.message; + await thought.save(); + res.json(thought); + } catch (err) { + res.status(400).json({ error: err.message }); + } +}); + +app.delete("/thoughts/:id", authenticateUser, 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.createdBy || + thought.createdBy.toString() !== req.user._id.toString() + ) { + return res + .status(403) + .json({ error: "Not allowed to delete this thought" }); + } + await thought.deleteOne(); + res.status(204).end(); + } catch { + res.status(400).json({ error: "Invalid ID" }); + } +}); + +app.post("/register", async (req, res) => { + const { email, password } = req.body; + try { + if (!email || !password) + return res.status(400).json({ error: "All fields are required" }); + + const existing = await User.findOne({ email }); + if (existing) + return res + .status(400) + .json({ error: "That email address already exists" }); + + const hashed = bcrypt.hashSync(password, bcrypt.genSaltSync()); + const newUser = await new User({ email, password: hashed }).save(); + + res.status(201).json({ + email: newUser.email, + id: newUser._id, + accessToken: newUser.accessToken, + }); + } catch (err) { + res.status(400).json({ error: err.message }); + } +}); + +app.post("/login", async (req, res) => { + const { email, password } = req.body; + try { + const user = await User.findOne({ email }); + if (!user || !bcrypt.compareSync(password, user.password)) { + return res.status(401).json({ error: "Invalid email or password" }); + } + res.status(200).json({ + email: user.email, + id: user._id, + accessToken: user.accessToken, + }); + } catch { + res.status(400).json({ error: "Something went wrong" }); + } +}); -// Start the server +// Server startup app.listen(port, () => { - console.log(`Server running on http://localhost:${port}`) -}) + console.log(`Server running on http://localhost:${port}`); +}); diff --git a/thoughts_IGNORE.json b/thoughts_IGNORE.json new file mode 100644 index 0000000..af5c6b8 --- /dev/null +++ b/thoughts_IGNORE.json @@ -0,0 +1,23 @@ +[ + { + "id": 1, + "text": "I love coding!", + "category": "Project thoughts", + "hearts": 12, + "createdAt": "2025-07-20T12:00:00Z" + }, + { + "id": 2, + "text": "Tacos for lunch today?", + "category": "Food thoughts", + "hearts": 5, + "createdAt": "2025-07-19T15:30:00Z" + }, + { + "id": 3, + "text": "Remember to water the plants.", + "category": "Home thoughts", + "hearts": 3, + "createdAt": "2025-07-18T08:15:00Z" + } +]