diff --git a/data.json b/data.json deleted file mode 100644 index a2c844f..0000000 --- a/data.json +++ /dev/null @@ -1,121 +0,0 @@ -[ - { - "_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": "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 - }, - { - "_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 - }, - { - "_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", - "__v": 0 - } -] \ No newline at end of file diff --git a/middleware/authMiddleware.js b/middleware/authMiddleware.js new file mode 100644 index 0000000..92f10f5 --- /dev/null +++ b/middleware/authMiddleware.js @@ -0,0 +1,22 @@ +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 }); + 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 + }); + } +}; diff --git a/models/Thought.js b/models/Thought.js new file mode 100644 index 0000000..26b8d69 --- /dev/null +++ b/models/Thought.js @@ -0,0 +1,27 @@ +import mongoose, { Schema } from "mongoose"; + +const thoughtSchema = new mongoose.Schema({ + message: { + type: String, + required: true, + minLength: 5, + maxLength: 140, + }, + hearts: { + type: Number, + default: 0, + min: 0, + }, + createdAt: { + type: Date, + default: Date.now, + }, + user: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true + } +}); + +export const Thought = mongoose.model("Thought", thoughtSchema); + diff --git a/models/user.js b/models/user.js new file mode 100644 index 0000000..4c39948 --- /dev/null +++ b/models/user.js @@ -0,0 +1,24 @@ +import mongoose from "mongoose"; + +const userSchema = new mongoose.Schema({ + username: { + type: String, + required: true, + unique: true, + trim: true, + minlength: 3, + maxlength: 10 + }, + password: { + type: String, + required: true, + minlength: 6 + }, + accessToken: { + type: String, + default: null + } +}); + +export const User = mongoose.model("User", userSchema); + diff --git a/package.json b/package.json index bf25bb6..5b7bb32 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.5.0", "express": "^4.17.3", - "nodemon": "^3.0.1" + "express-list-endpoints": "^7.1.1", + "jsonwebtoken": "^9.0.2", + "mongodb": "^6.17.0", + "mongoose": "^8.15.1", + "nodemon": "^3.1.10" } -} +} \ No newline at end of file diff --git a/routes/auth.js b/routes/auth.js new file mode 100644 index 0000000..789d6bc --- /dev/null +++ b/routes/auth.js @@ -0,0 +1,56 @@ +import express from "express"; +import bcrypt from "bcrypt"; +import crypto from "crypto"; +import { User } from "../models/user.js"; + +const router = express.Router(); + +// Helper to generate a random access token +const generateAccessToken = () => crypto.randomBytes(32).toString("hex"); + +// Register endpoint +router.post("/register", async (req, res) => { + const { username, password } = req.body; + if (!username || !password) { + return res.status(400).json({ error: "Username and password are required." }); + } + try { + const existingUser = await User.findOne({ username }); + if (existingUser) { + return res.status(400).json({ error: "Username already exists." }); + } + const hashedPassword = await bcrypt.hash(password, 10); + const accessToken = generateAccessToken(); + const user = new User({ username, password: hashedPassword, accessToken }); + await user.save(); + res.status(201).json({ message: "User registered successfully.", accessToken, username: user.username }); + } catch (error) { + res.status(500).json({ error: "Failed to register user.", details: error.message }); + } +}); + +// Login endpoint +router.post("/login", async (req, res) => { + const { username, password } = req.body; + if (!username || !password) { + return res.status(400).json({ error: "Username and password are required." }); + } + try { + const user = await User.findOne({ username }); + if (!user) { + return res.status(401).json({ error: "Invalid username or password." }); + } + const passwordMatch = await bcrypt.compare(password, user.password); + if (!passwordMatch) { + return res.status(401).json({ error: "Invalid username or password." }); + } + // Generate a new accessToken on login + user.accessToken = generateAccessToken(); + await user.save(); + res.status(200).json({ accessToken: user.accessToken, username: user.username }); + } catch (error) { + res.status(500).json({ error: "Failed to log in.", details: error.message }); + } +}); + +export default router; \ No newline at end of file diff --git a/server.js b/server.js index f47771b..126b192 100644 --- a/server.js +++ b/server.js @@ -1,22 +1,147 @@ -import cors from "cors" -import express from "express" +import cors from "cors"; +import express from "express"; +import listEndpoints from "express-list-endpoints"; -// 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() +import mongoose from "mongoose"; +import dotenv from "dotenv"; +import { Thought } from "./models/Thought.js"; // Adjust the import path as necessary +import authRouter from "./routes/auth.js"; +import { authenticateUser } from "./middleware/authMiddleware.js"; -// Add middlewares to enable cors and json body parsing -app.use(cors()) -app.use(express.json()) +dotenv.config(); -// Start defining your routes here +const mongoUrl = process.env.MONGO_URL || "mongodb://localhost:27017/happy-thoughts-api"; +mongoose.connect(mongoUrl); + +mongoose.connection.on("error", (error) => { + console.error("MongoDB connection error:", error); +}); + +mongoose.connection.once("open", () => { + console.log("Connected to MongoDB"); +}); + +const port = process.env.PORT || 8080; +const app = express(); + +app.use(cors()); +app.use(express.json()); + + +// Endpoint to get API documentation app.get("/", (req, res) => { - res.send("Hello Technigo!") -}) + res.json(listEndpoints(app)); +}); + + +// Endpoint to get all thoughts +app.get("/thoughts", async (req, res) => { + try { + const thoughts = await Thought.find().sort({ createdAt: -1 }).limit(20); + res.status(200).json(thoughts); + } catch (error) { + res.status(500).json({ error: "Failed to fetch thoughts", details: error.message }); + } +}); + +// Endpoint to create a new thought (authenticated) +app.post("/thoughts", authenticateUser, async (req, res) => { + const { message } = req.body; + if (!message || message.length < 5 || message.length > 140) { + return res.status(400).json({ error: "Message must be between 5 and 140 characters" }); + } + try { + const newThought = await new Thought({ message, user: req.user._id }).save(); + res.status(201).json(newThought); + } catch (error) { + res.status(500).json({ error: "Failed to create thought", details: error.message }); + } +}); + +// Endpoint to like a thought +app.post("/thoughts/:id/like", async (req, res) => { + const { id } = req.params; + + try { + const updatedThought = await Thought.findByIdAndUpdate( + id, + { $inc: { hearts: 1 } }, // Increment the hearts count + { new: true } // Return the updated document + ); + + if (!updatedThought) { + return res.status(404).json({ error: "Thought not found" }); + } + + res.status(200).json(updatedThought); + } catch (error) { + res.status(500).json({ error: "Failed to like the thought", details: error.message }); + } +}); + +// Endpoint to delete a thought (authenticated, only owner) +app.delete("/thoughts/:id", authenticateUser, async (req, res) => { + const { id } = req.params; + try { + const thought = await Thought.findById(id); + if (!thought) { + return res.status(404).json({ error: "Thought not found" }); + } + if (String(thought.user) !== String(req.user._id)) { + return res.status(403).json({ error: "You are not authorized to delete this thought" }); + } + await thought.deleteOne(); + res.status(200).json({ message: "Thought deleted successfully" }); + } catch (error) { + res.status(500).json({ error: "Failed to delete thought", details: error.message }); + } +}); + +// Endpoint to get a single thought by ID +app.get("/thoughts/:id", async (req, res) => { + const { id } = req.params; + + try { + const thought = await Thought.findById(id); + + if (!thought) { + return res.status(404).json({ error: "Thought not found" }); + } + + res.status(200).json(thought); + } catch (error) { + res.status(500).json({ error: "Failed to fetch the thought", details: error.message }); + } +}); + +// Endpoint to update a thought (authenticated, only owner) +app.put("/thoughts/:id", authenticateUser, async (req, res) => { + const { id } = req.params; + const { message } = req.body; + if (!message || message.length < 5 || message.length > 140) { + return res.status(400).json({ error: "Message must be between 5 and 140 characters" }); + } + try { + const thought = await Thought.findById(id); + if (!thought) { + return res.status(404).json({ error: "Thought not found" }); + } + if (String(thought.user) !== String(req.user._id)) { + return res.status(403).json({ error: "You are not authorized to update this thought" }); + } + thought.message = message; + await thought.save(); + res.status(200).json(thought); + } catch (error) { + res.status(500).json({ error: "Failed to update thought", details: error.message }); + } +}); + +app.use("/auth", authRouter); // Start the server app.listen(port, () => { - console.log(`Server running on http://localhost:${port}`) -}) + console.log(`Server running on http://localhost:${port}`); +}); + +