diff --git a/data/thoughts.json b/data/thoughts.json new file mode 100644 index 0000000..281db71 --- /dev/null +++ b/data/thoughts.json @@ -0,0 +1,156 @@ +[ + { + "id": 1, + "message": "Today I learned Express!", + "hearts": 5, + "category": "learning", + "createdAt": "2026-01-10T10:00:00.00Z" + }, + { + "id": 2, + "message": "Coffee is the best", + "hearts": 12, + "category": "food", + "createdAt": "2026-01-11T08:30:00.00Z" + }, + { + "id": 3, + "message": "My cat is sleeping on my keyboard", + "hearts": 24, + "category": "home", + "createdAt": "2026-01-12T15:45:00.00Z" + }, + { + "id": 4, + "message": "Snow is finally falling outside the window", + "hearts": 7, + "category": "weather", + "createdAt": "2026-01-13T06:12:00.00Z" + }, + { + "id": 5, + "message": "Refactored old React code and everything feels cleaner", + "hearts": 18, + "category": "coding", + "createdAt": "2026-01-13T09:47:00.00Z" + }, + { + "id": 6, + "message": "Walk without podcasts felt surprisingly nice", + "hearts": 9, + "category": "mindfulness", + "createdAt": "2026-01-14T17:22:00.00Z" + }, + { + "id": 7, + "message": "Tried dark mode and never want to go back", + "hearts": 21, + "category": "tech", + "createdAt": "2026-01-14T20:05:00.00Z" + }, + { + "id": 8, + "message": "Pair programming saved me from a really stubborn bug", + "hearts": 14, + "category": "coding", + "createdAt": "2026-01-15T19:11:00.00Z" + }, + { + "id": 9, + "message": "Found the perfect playlist for focus mode", + "hearts": 16, + "category": "music", + "createdAt": "2026-01-16T21:30:00.00Z" + }, + { + "id": 10, + "message": "Pushed the first version of my new side project", + "hearts": 27, + "category": "projects", + "createdAt": "2026-01-17T09:03:00.00Z" + }, + { + "id": 11, + "message": "Closed all old browser tabs, feel like a new person", + "hearts": 19, + "category": "productivity", + "createdAt": "2026-01-17T11:58:00.00Z" + }, + { + "id": 12, + "message": "Finally wrote tests for that tricky function", + "hearts": 13, + "category": "coding", + "createdAt": "2026-01-18T14:40:00.00Z" + }, + { + "id": 13, + "message": "Inbox down to zero, almost unreal feeling", + "hearts": 22, + "category": "productivity", + "createdAt": "2026-01-18T19:26:00.00Z" + }, + { + "id": 14, + "message": "Tried standing desk all day, back thanks me", + "hearts": 11, + "category": "health", + "createdAt": "2026-01-19T08:55:00.00Z" + }, + { + "id": 15, + "message": "Skiing in calendar, winter feels okay again", + "hearts": 25, + "category": "skiing", + "createdAt": "2026-01-19T16:17:00.00Z" + }, + { + "id": 16, + "message": "Logged out of social media for a week", + "hearts": 30, + "category": "mindfulness", + "createdAt": "2026-01-20T12:34:00.00Z" + }, + { + "id": 17, + "message": "Finally got my VS Code setup perfect", + "hearts": 17, + "category": "tech", + "createdAt": "2026-01-20T19:02:00.00Z" + }, + { + "id": 18, + "message": "Discovered walks solve more bugs than expected", + "hearts": 28, + "category": "life", + "createdAt": "2026-01-21T07:48:00.00Z" + }, + { + "id": 19, + "message": "Perfect coffee mid-debug session", + "hearts": 20, + "category": "food", + "createdAt": "2026-01-21T10:19:00.00Z" + }, + { + "id": 20, + "message": "Added dark mode toggle to latest project", + "hearts": 23, + "category": "projects", + "createdAt": "2026-01-21T18:33:00.00Z" + }, + { + "id": 21, + "message": "Set up automatic backups, suddenly feel adult", + "hearts": 15, + "category": "tech", + "createdAt": "2026-01-22T09:10:00.00Z" + }, + { + "id": 22, + "message": "Found new favorite lunch spot around corner", + "hearts": 10, + "category": "food", + "createdAt": "2026-01-22T11:25:00.00Z" + } +] diff --git a/models/Thought.js b/models/Thought.js new file mode 100644 index 0000000..cd1f6c4 --- /dev/null +++ b/models/Thought.js @@ -0,0 +1,34 @@ +import mongoose from "mongoose"; + +const thoughtSchema = new mongoose.Schema({ + message: { + type: String, + required: true, + minlength: 5, + maxlength: 140 + }, + hearts: { + type: Number, + default: 0 + }, + category: { + type: String, + default: "general" + }, + user: { + type: mongoose.Schema.Types.ObjectId, + ref: "User" + }, + username: { + type: String, + default: "Anonymous" + }, + createdAt: { + type: Date, + default: Date.now + } +}); + +const Thought = mongoose.model("Thought", thoughtSchema); + +export default Thought; \ No newline at end of file diff --git a/models/User.js b/models/User.js new file mode 100644 index 0000000..1989f18 --- /dev/null +++ b/models/User.js @@ -0,0 +1,41 @@ +import mongoose from "mongoose"; +import bcrypt from "bcrypt"; +import crypto from "crypto"; + +const userSchema = new mongoose.Schema({ + username: { + type: String, + required: true, + unique: true, + minlength: 3, + maxlength: 20 + }, + email: { + type: String, + required: true, + unique: true, + lowercase: true + }, + password: { + type: String, + required: true, + minlength: 8 + }, + accessToken: { + type: String, + default: () => crypto.randomUUID() + }, + createdAt: { + type: Date, + default: Date.now + } +}); + +userSchema.pre("save", async function() { + if (this.isModified("password")) { + this.password = await bcrypt.hash(this.password, 10); + } +}); + +const User = mongoose.model("User", userSchema); +export default User; \ No newline at end of file diff --git a/package.json b/package.json index bf25bb6..47194ea 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,25 @@ { - "name": "project-api", + "name": "myfirstapi", "version": "1.0.0", - "description": "Project API", + "description": "", + "license": "ISC", + "author": "", + "type": "module", + "main": "server.js", "scripts": { - "start": "babel-node server.js", - "dev": "nodemon server.js --exec babel-node" + "start": "node server.js", + "dev": "nodemon server.js --exec babel-node", + "test": "echo \"Error: no test specified\" && exit 1" }, - "author": "", - "license": "ISC", "dependencies": { - "@babel/core": "^7.17.9", - "@babel/node": "^7.16.8", - "@babel/preset-env": "^7.16.11", - "cors": "^2.8.5", - "express": "^4.17.3", - "nodemon": "^3.0.1" + "@babel/core": "^7.28.6", + "@babel/node": "^7.28.6", + "@babel/preset-env": "^7.28.6", + "bcrypt": "^6.0.0", + "cors": "^2.8.6", + "dotenv": "^17.2.3", + "express": "^5.2.1", + "mongoose": "^9.1.5", + "nodemon": "^3.1.11" } } diff --git a/server.js b/server.js index f47771b..426cec9 100644 --- a/server.js +++ b/server.js @@ -1,22 +1,417 @@ -import cors from "cors" -import express from "express" +import cors from "cors"; +import express from "express"; +import mongoose from "mongoose"; +import bcrypt from "bcrypt"; +import dotenv from "dotenv"; +import Thought from "./models/Thought.js"; +import User from "./models/User.js"; + +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() -// Add middlewares to enable cors and json body parsing app.use(cors()) app.use(express.json()) -// Start defining your routes here -app.get("/", (req, res) => { - res.send("Hello Technigo!") -}) +const authenticateUser = async (req, res, next) => { + const token = req.header("Authorization"); + + if (!token) { + return res.status(401).json({ + success: false, + error: "Access denied. No token provided" + }); + } + + try { + const user = await User.findOne({ accessToken: token }); + if (!user) { + return res.status(401).json({ + success: false, + error: "Invalid token" + }); + } + + req.user = user; + next(); + } catch (error) { + res.status(500).json({ + success: false, + error: "Authentication error" + }); + } +}; + +app.get('/', (req, res) => { + res.json({ + message: "Welcome to Happy Thoughts API", + endpoints: [ + { method: "GET", path: "/", description: "This documentation" }, + { method: "GET", path: "/thoughts", description: "Get all thoughts" }, + { method: "GET", path: "/thoughts/:id", description: "Get one thought by ID" }, + { method: "POST", path: "/thoughts", description: "Create a thought (auth required)" }, + { method: "PATCH", path: "/thoughts/:id", description: "Update a thought (auth required, owner only)" }, + { method: "POST", path: "/thoughts/:id/like", description: "Like a thought" }, + { method: "DELETE", path: "/thoughts/:id", description: "Delete a thought (auth required, owner only)" }, + { method: "POST", path: "/users", description: "Register new user" }, + { method: "POST", path: "/sessions", description: "Login (get access token)" } + ], + authentication: { + description: "Some endpoints require authentication", + howTo: "Include 'Authorization' header with your access token", + protectedEndpoints: ["POST /thoughts", "PATCH /thoughts/:id", "DELETE /thoughts/:id"] + } + }); +}); + +// Get all unique categories from database +app.get("/categories", async (req, res) => { + try { + const categories = await Thought.distinct("category"); + res.json(categories.filter(Boolean).sort()); + } catch (error) { + res.status(500).json({ + success: false, + error: "Could not fetch categories", + message: error.message + }); + } +}); + +app.get("/thoughts", async (req, res) => { + try { + const { category, sort, page = 1, limit = 20 } = req.query; + + let filter = {}; + + if (category) { + filter.category = category.toLowerCase(); + } + + let query = Thought.find(filter); + + if (sort === "hearts") { + query = query.sort({ heats: -1 }); + } else if (sort === "date") { + query = query.sort({ createdAt: -1 }); + } else { + query = query.sort({ createdAt: -1 }); + } + + const skip = (Number(page) -1) * Number(limit); + query = query.skip(skip).limit(Number(limit)); + + const thoughts = await query; + + const total = await Thought.countDocuments(); + + res.json({ + total, + page: Number(page), + limit: Number(limit), + totalPages: Math.ceil(total / Number(limit)), + results: thoughts, + }); + } catch (error) { + res.status(500).json({ + success: false, + error: "Could not fetch thoughts", + message: error.message + }); + } +}); + +app.get('/thoughts/:id', async (req, res) => { + try { + const id = req.params.id; + const thought = await Thought.findById(req.params.id); + + if (!thought) { + return res.status(404).json({ + success: false, + error: "Thought not found", + message: `No thought with id ${id} exists`, + }); + } + + res.json(thought); + } catch(error) { + res.status(400).json({ + success: false, + error: "Invalid id format", + message: error.message + }); + } +}); + +app.post("/thoughts", authenticateUser, async(req, res) => { + try { + const { message, category } = req.body; + + const thought = new Thought({ + message, + category, + user: req.user._id, + username: req.user.username + }); + + const savedThought = await thought.save(); + res.status(201).json(savedThought); + } catch (error) { + res.status(400).json({ + success: false, + error: "Could not create thought", + message: error.message + }); + } +}); + +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({ + success: false, + error: "Thought not found", + }); + } + + res.json(thought) + + } catch (error) { + res.status(400).json({ + success: false, + error: "Could not like thought", + message: error.message + }); + } +}); + +app.delete("/thoughts/:id", authenticateUser, async (req, res) => { + try { + // First find the thought to check ownership + const thought = await Thought.findById(req.params.id); + + if (!thought) { + return res.status(404).json({ + success: false, + error: "Thought not found" + }); + } + + // Check if user owns this thought + if (thought.user && thought.user.toString() !== req.user._id.toString()) { + return res.status(403).json({ + success: false, + error: "Not authorized - you can only delete your own thoughts" + }); + } + + // Now delete it + await Thought.findByIdAndDelete(req.params.id); + + res.json({ + success: true, + message: "Thought deleted", + deleted: thought + }); + } catch (error) { + res.status(400).json({ + success: false, + error: "Could not delete thought", + message: error.message + }); + } +}); + +// PATCH /thoughts/:id - Update a thought (only owner can update) +app.patch("/thoughts/:id", authenticateUser, async (req, res) => { + try { + const { message, category } = req.body; + + // First find the thought to check ownership + const thought = await Thought.findById(req.params.id); + + if (!thought) { + return res.status(404).json({ + success: false, + error: "Thought not found" + }); + } + + // Check if user owns this thought + if (thought.user && thought.user.toString() !== req.user._id.toString()) { + return res.status(403).json({ + success: false, + error: "Not authorized - you can only edit your own thoughts" + }); + } + + // Update the thought + const updatedThought = await Thought.findByIdAndUpdate( + req.params.id, + { message, category }, + { new: true, runValidators: true } + ); + + res.json(updatedThought); + } catch (error) { + res.status(400).json({ + success: false, + error: "Could not update thought", + message: error.message + }); + } +}); + +// DELETE /users/me - Delete own account +app.delete("/users/me", authenticateUser, async (req, res) => { + try { + // Delete all user's thoughts first + await Thought.deleteMany({ user: req.user._id }); + + // Delete the user + await User.findByIdAndDelete(req.user._id); + + res.json({ + success: true, + message: "Account deleted successfully" + }); + } catch (error) { + res.status(500).json({ + success: false, + error: "Could not delete account", + message: error.message + }); + } +}); + +// DELETE /users/:id - Delete user by ID (for debugging - remove in production!) +app.delete("/users/:id", async (req, res) => { + try { + // Delete all user's thoughts first + await Thought.deleteMany({ user: req.params.id }); + + const user = await User.findByIdAndDelete(req.params.id); + + if (!user) { + return res.status(404).json({ + success: false, + error: "User not found" + }); + } + + res.json({ + success: true, + message: "User and their thoughts deleted", + deleted: { username: user.username, email: user.email } + }); + } catch (error) { + res.status(400).json({ + success: false, + error: "Could not delete user", + message: error.message + }); + } +}); + +// GET /users - List all users (for debugging - remove in production!) +app.get("/users", async (req, res) => { + try { + const users = await User.find({}, { password: 0, accessToken: 0 }); // Exclude sensitive fields + res.json(users); + } catch (error) { + res.status(500).json({ + success: false, + error: "Could not fetch users", + message: error.message + }); + } +}); + +app.post("/users", async (req, res) => { + try { + const { username, email, password } = req.body; + + const existingUser = await User.findOne({ email }); + if (existingUser) { + return res.status(400).json({ + success: false, + error: "Email already exists" + }); + } + + const user = new User({ username, email, password }); + const savedUser = await user.save(); + + res.status(201).json({ + success: true, + userId: savedUser._id, + username: savedUser.username, + accessToken: savedUser.accessToken + }); + } catch (error) { + res.status(400).json({ + success: false, + error: "Could not create user", + message: error.message + }) + } +}); + +app.post("/sessions", async (req, res) => { + try { + const { email, password } = req.body; + + const user = await User.findOne({ email }); + if (!user) { + return res.status(401).json({ + success: false, + error: "Invalid email or password" + }) + } + + const isMatch = await bcrypt.compare(password, user.password); + if(!isMatch) { + return res.status(401).json({ + success:false, + error: "Invalid email or password" + }); + } + user.accessToken = crypto.randomUUID(); + await user.save(); + + res.json({ + success: true, + userId: user._id, + username: user.username, + accessToken: user.accessToken + }); + } catch (error) { + res.status(500).json({ + success: false, + error: "Login failed", + message: error.message + }) + } +}); + +mongoose + .connect(process.env.MONGO_URL) + .then(() => { + console.log("Connected to MongoDB"); + + app.listen(port, () => { + console.log(`Server running on http://localhost:${port}`) + }); -// Start the server -app.listen(port, () => { - console.log(`Server running on http://localhost:${port}`) -}) + }) + .catch((error) => { + console.error("Could not connect to MongoDB:", error.message) + });