diff --git a/README.md b/README.md index 0f9f073..1f7f61a 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,4 @@ -# Project 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. - -## Getting started - -Install dependencies with `npm install`, then start the server by running `npm run dev` - -## 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. +Link to frontend: +https://leonhappythoughts.netlify.app/ +Link to API: +https://happythoughtsapi.onrender.com diff --git a/data.json b/data.json index a2c844f..62b0fd7 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", diff --git a/middleware/auth.js b/middleware/auth.js new file mode 100644 index 0000000..93045ec --- /dev/null +++ b/middleware/auth.js @@ -0,0 +1,22 @@ +import jwt from "jsonwebtoken" +import { User } from "../models/User" + +export const authenticateUser = async (req, res, next) => { + try { + const token = req.header("Authorization")?.replace("Bearer ", "") + if (!token) { + return res.status(401).json({ error: "Access denied. No token provided." }) + } + + const decoded = jwt.verify(token, process.env.JWT_SECRET) + const user = await User.findById(decoded.id) + if (!user) { + return res.status(401).json({ error: "Invalid token." }) + } + + req.user = user + next() + } catch (error) { + res.status(401).json({ error: "Invalid token." }) + } +} diff --git a/models/Thought.js b/models/Thought.js new file mode 100644 index 0000000..dff054d --- /dev/null +++ b/models/Thought.js @@ -0,0 +1,25 @@ +import mongoose from "mongoose" + +const ThoughtSchema = new mongoose.Schema({ + message: { + type: String, + required: [true, "Message is required"], + minlength: 5, + maxlength: 140 + }, + hearts: { + type: Number, + default: 0 + }, + user: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true + }, + createdAt: { + type: Date, + default: () => new Date() + } +}) + +export const Thought = mongoose.model("Thought", ThoughtSchema) diff --git a/models/User.js b/models/User.js new file mode 100644 index 0000000..03ee705 --- /dev/null +++ b/models/User.js @@ -0,0 +1,32 @@ +import mongoose from "mongoose" +import bcrypt from "bcrypt" + +const UserSchema = new mongoose.Schema({ + username: { + type: String, + required: [true, "Username is required"], + unique: true, + minlength: 3, + maxlength: 20 + }, + email: { + type: String, + required: [true, "Email is required"], + unique: true, + match: [/^\S+@\S+\.\S+$/, "Please enter a valid email"] + }, + password: { + type: String, + required: [true, "Password is required"], + minlength: 6 + } +}, { timestamps: true }) + +// Hash password before saving +UserSchema.pre("save", async function () { + if (this.isModified("password")) { + this.password = await bcrypt.hash(this.password, 10) + } +}) + +export const User = mongoose.model("User", UserSchema) diff --git a/package.json b/package.json index bf25bb6..c248736 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", "cors": "^2.8.5", + "dotenv": "^16.3.1", "express": "^4.17.3", + "express-list-endpoints": "^7.1.1", + "jsonwebtoken": "^9.0.3", + "mongodb": "^7.0.0", + "mongoose": "^9.1.5", "nodemon": "^3.0.1" } } diff --git a/server.js b/server.js index f47771b..2fb1b32 100644 --- a/server.js +++ b/server.js @@ -1,19 +1,167 @@ import cors from "cors" import express from "express" +import mongoose from "mongoose" +import bcrypt from "bcrypt" +import jwt from "jsonwebtoken" +import { Thought } from "./models/Thought" +import { User } from "./models/User" +import { authenticateUser } from "./middleware/auth" +import listEndpoints from "express-list-endpoints" + +import dotenv from "dotenv" +dotenv.config() + +const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/happy-thoughts" +mongoose.connect(mongoUrl) +mongoose.Promise = Promise -// 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() -// Add middlewares to enable cors and json body parsing app.use(cors()) app.use(express.json()) -// Start defining your routes here +// root - shows all available endpoints app.get("/", (req, res) => { - res.send("Hello Technigo!") + res.json(listEndpoints(app)) +}) + +// ---- AUTH ROUTES ---- + +// register +app.post("/register", async (req, res) => { + try { + const { username, email, password } = req.body + const user = await User.create({ username, email, password }) + const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, { expiresIn: "7d" }) + res.status(201).json({ + id: user._id, + username: user.username, + email: user.email, + token + }) + } catch (error) { + if (error.code === 11000) { + res.status(400).json({ error: "Username or email already exists" }) + } else { + res.status(400).json({ error: error.message }) + } + } +}) + +// login +app.post("/login", async (req, res) => { + try { + const { email, password } = req.body + const user = await User.findOne({ email }) + if (!user) { + return res.status(401).json({ error: "Invalid email or password" }) + } + const isMatch = await bcrypt.compare(password, user.password) + if (!isMatch) { + return res.status(401).json({ error: "Invalid email or password" }) + } + const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, { expiresIn: "7d" }) + res.json({ + id: user._id, + username: user.username, + email: user.email, + token + }) + } catch (error) { + res.status(500).json({ error: "Something went wrong" }) + } +}) + +// ---- THOUGHT ROUTES ---- + +// get all thoughts +app.get("/thoughts", async (req, res) => { + try { + const thoughts = await Thought.find().populate("user", "username").sort({ createdAt: -1 }) + res.json(thoughts) + } catch (error) { + res.status(500).json({ error: "Could not fetch thoughts" }) + } +}) + +// create a new thought (authenticated) +app.post("/thoughts", authenticateUser, async (req, res) => { + try { + const { message } = req.body + const thought = await Thought.create({ message, user: req.user._id }) + const populatedThought = await thought.populate("user", "username") + res.status(201).json(populatedThought) + } catch (error) { + res.status(400).json({ error: error.message }) + } +}) + +// get single thought +app.get("/thoughts/:id", async (req, res) => { + try { + const { id } = req.params + const thought = await Thought.findById(id).populate("user", "username") + if (!thought) { + return res.status(404).json({ error: "Thought not found" }) + } + res.json(thought) + } catch (error) { + res.status(400).json({ error: "Invalid ID format" }) + } +}) + +// update a thought (authenticated, only creator) +app.put("/thoughts/:id", authenticateUser, async (req, res) => { + try { + const { id } = req.params + const { message } = req.body + const thought = await Thought.findById(id) + if (!thought) { + return res.status(404).json({ error: "Thought not found" }) + } + if (thought.user.toString() !== req.user._id.toString()) { + return res.status(403).json({ error: "You can only edit your own thoughts" }) + } + thought.message = message + await thought.save() + const updatedThought = await thought.populate("user", "username") + res.json(updatedThought) + } catch (error) { + res.status(400).json({ error: error.message }) + } +}) + +// delete a thought (authenticated, only creator) +app.delete("/thoughts/:id", authenticateUser, async (req, res) => { + try { + const { id } = req.params + const thought = await Thought.findById(id) + if (!thought) { + return res.status(404).json({ error: "Thought not found" }) + } + if (thought.user.toString() !== req.user._id.toString()) { + return res.status(403).json({ error: "You can only delete your own thoughts" }) + } + await thought.deleteOne() + res.status(204).send() + } catch (error) { + res.status(400).json({ error: "Invalid ID format" }) + } +}) + +// like a thought +app.post("/thoughts/:id/like", async (req, res) => { + try { + const { id } = req.params + const thought = await Thought.findByIdAndUpdate(id, { $inc: { hearts: 1 } }, { new: true }).populate("user", "username") + if (!thought) { + return res.status(404).json({ error: "Thought not found" }) + } + res.json(thought) + } catch (error) { + res.status(400).json({ error: "Invalid ID format" }) + } }) // Start the server