diff --git a/.gitignore b/.gitignore index f1ff414..7bfe6e0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ node_modules .env.development.local .env.test.local .env.production.local -package-lock.json \ No newline at end of file +package-lock.json +todo.md \ No newline at end of file diff --git a/controllers/deleteThought.js b/controllers/deleteThought.js new file mode 100644 index 0000000..c2bd780 --- /dev/null +++ b/controllers/deleteThought.js @@ -0,0 +1,44 @@ +import { Thought } from "../models/thought.js" + +export const deleteThought = async (req, res) => { + const { id } = req.params + const userId = req.user._id + + try { + + const thought = await Thought.findById(id) + + if (!thought) { + return res.status(404).json({ + success: false, + response: [], + message: "Failed to delete thought: no thought found for the given id." + }) + } + + if (thought.userId.toString() !== userId.toString()) { + return res.status(403).json({ + success: false, + response: [], + message: "Failed to delete thought: you are not authorized to delete this thought." + }) + } + + await Thought.findByIdAndDelete(id) + + res.status(200).json({ + success: true, + response: [], + message: "Thought deleted successfully." + }) + + } catch (error) { + + res.status(500).json({ + success: false, + response: error, + message: "Internal server error: failed to delete thought. Please try again later." + }) + + } +} \ No newline at end of file diff --git a/controllers/getThought.js b/controllers/getThought.js new file mode 100644 index 0000000..310e60b --- /dev/null +++ b/controllers/getThought.js @@ -0,0 +1,31 @@ +import { Thought } from "../models/thought.js" + +export const getThought = async (req, res) => { + const { id } = req.params + + try { + const thought = await Thought.findById(id) + + if (!thought) { + return res.status(404).json({ + success: false, + response: [], + message: "No thought found for the given id. Please try again with a different id." + }) + } + + res.status(200).json({ + success: true, + response: thought, + message: "Success." + }) + } + + catch (error) { + res.status(500).json({ + success: false, + response: error, + message: "Internal server error: failed to fetch thought. Please try again later." + }) + } +} \ No newline at end of file diff --git a/controllers/getThoughts.js b/controllers/getThoughts.js new file mode 100644 index 0000000..0ec334e --- /dev/null +++ b/controllers/getThoughts.js @@ -0,0 +1,34 @@ +import { Thought } from "../models/thought.js" + +export const getThoughts = async (req, res) => { + const { hearts } = req.query; + const query = {} + + if (hearts) { + query.hearts = hearts + } + + try { + const filteredThoughts = await Thought.find(query) + + if (filteredThoughts.length === 0) { + return res.status(404).json({ + success: false, + response: [], + message: "No thoughts found for given query. Please try again with a different query" + }) + } + res.status(200).json({ + success: true, + response: filteredThoughts, + message: "Success" + }) + + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: "Failed to fetch thoughts" + }) + } +} \ No newline at end of file diff --git a/controllers/getUserData.js b/controllers/getUserData.js new file mode 100644 index 0000000..ff41124 --- /dev/null +++ b/controllers/getUserData.js @@ -0,0 +1,31 @@ +import { User } from "../models/user.js" + +export const getUserData = async (req, res) => { + try { + + const user = req.user + + if (!user) { + return res.status(404).json({ + success: false, + response: [], + message: "Failed to get user data: no user found for the given id." + }) + } + + res.status(200).json({ + success: true, + response: { name: user.name }, + message: "Success" + }) + + } catch (error) { + + res.status(500).json({ + success: false, + response: error, + message: "Internal server error: failed to get user data. Please try again later." + }) + + } +} \ No newline at end of file diff --git a/controllers/loginUser.js b/controllers/loginUser.js new file mode 100644 index 0000000..9ab5a00 --- /dev/null +++ b/controllers/loginUser.js @@ -0,0 +1,44 @@ +import bcrypt from "bcrypt" +import jwt from "jsonwebtoken" +import { User } from "../models/user.js" + +export const loginUser = 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({ + success: false, + response: [], + message: "Failed to log in: invalid email or password." + }) + } + + const accessToken = jwt.sign( + { userId: user._id }, + process.env.JWT_SECRET, + { expiresIn: process.env.JWT_EXPIRES_IN} + ) + + res.status(200).json({ + success: true, + response: { + accessToken, + name: user.name, + userId: user._id + }, + message: "Log in successful." + }) + +} catch (error) { + + res.status(500).json({ + success: false, + response: error, + message: "Internal server error: failed to log in. Please try again later." + }) + + } +} \ No newline at end of file diff --git a/controllers/patchThought.js b/controllers/patchThought.js new file mode 100644 index 0000000..84e91f3 --- /dev/null +++ b/controllers/patchThought.js @@ -0,0 +1,45 @@ +import { Thought } from "../models/thought.js" + +export const patchThought = async (req, res) => { + const { id } = req.params + const { updatedMessage } = req.body + const userId = req.user._id + + try { + const thought = await Thought.findById(id) + + if (!thought) { + return res.status(404).json({ + success: false, + response: [], + message: "Failed to update thought: no thought found for given id." + }) + } + + if (thought.userId.toString() !== userId.toString()) { + return res.status(403).json({ + success: false, + response: [], + message: "Failed to update thought: you are not authorized to update this thought." + }) + } + + thought.message = updatedMessage + await thought.save() + + res.status(200).json({ + success: true, + response: thought, + message: "Thought updated successfully." + }) + + } catch (error) { + + res.status(500).json({ + success: false, + response: error, + message: "Internal server error: failed to update thought. Please try again later." + }) + + } +} \ No newline at end of file diff --git a/controllers/patchThoughtLikes.js b/controllers/patchThoughtLikes.js new file mode 100644 index 0000000..05a0f40 --- /dev/null +++ b/controllers/patchThoughtLikes.js @@ -0,0 +1,46 @@ +import { Thought } from "../models/thought.js" + +export const patchThoughtLikes = async (req, res) => { + const { id } = req.params + const userId = req.user._id + + try { + + const thought = await Thought.findById(id) + + if (!thought) { + return res.status(404).json({ + success: false, + response: [], + message: "No thought found for the given id. Please try again with a different id." + }) + } + + const hasLiked = thought.likes.includes(userId) + + if (hasLiked) { + // remove like + thought.likes = thought.likes.filter(like => like.toString() !== userId.toString()) + } else { + // add like + thought.likes.push(userId) + } + + await thought.save() + + res.status(200).json({ + success: true, + response: thought, + message: "Likes updated successfully." + }) + + } catch (error) { + + res.status(500).json({ + success: false, + response: error, + message: "Internal server error: failed to update likes. Please try again later." + }) + + } +} \ No newline at end of file diff --git a/controllers/postThought.js b/controllers/postThought.js new file mode 100644 index 0000000..0169587 --- /dev/null +++ b/controllers/postThought.js @@ -0,0 +1,33 @@ +import { Thought } from "../models/thought.js" + +export const postThought = async (req, res) => { + const { message } = req.body + const user = req.user + + try { + const newThought = await new Thought({ message, userId: user._id }).save() + + if (!newThought) { + return res.status(400).json({ + success: false, + response: [], + message: "Failed to post thought." + }) + } + + res.status(201).json({ + success: true, + response: newThought, + message: "Thought posted successfully." + }) + + } catch (error) { + + res.status(500).json({ + success: false, + response: error, + message: "Internal server error: failed to post thought. Please try again later." + }) + + } +} \ No newline at end of file diff --git a/controllers/postUser.js b/controllers/postUser.js new file mode 100644 index 0000000..47b2c19 --- /dev/null +++ b/controllers/postUser.js @@ -0,0 +1,40 @@ +import bcrypt from "bcrypt" +import jwt from "jsonwebtoken" +import { User } from "../models/user.js" + +export const postUser = async (req, res) => { + try { + + const { name, email, password } = req.body + + const salt = bcrypt.genSaltSync() + const hashedPassword = bcrypt.hashSync(password, salt) + + const newUser = await new User({ name, email, password: hashedPassword }).save() + + const accessToken = jwt.sign( + { userId: newUser._id }, + process.env.JWT_SECRET, + { expiresIn: process.env.JWT_EXPIRES_IN } + ) + + res.status(201).json({ + success: true, + response: { + userId: newUser._id, + name: newUser.name, + accessToken + }, + message: "User registered successfully." + }) + + } catch (error) { + + res.status(500).json({ + success: false, + response: error, + message: "Internal server error: failed to register user. Please try again later." + }) + + } +} \ No newline at end of file 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/authenticateUser.js b/middleware/authenticateUser.js new file mode 100644 index 0000000..bd155bc --- /dev/null +++ b/middleware/authenticateUser.js @@ -0,0 +1,42 @@ +import jwt from "jsonwebtoken" +import { User } from "../models/user.js" + +export const authenticateUser = async (req, res, next) => { + const authHeader = req.headers.authorization + + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return res.status(401).json({ + success: false, + response: [], + message: "Authorization header missing or invalid." + }) + } + + const token = authHeader.replace("Bearer ", "") + + try { + + const decoded = jwt.verify(token, process.env.JWT_SECRET) + const user = await User.findById(decoded.userId) + + if (!user) { + return res.status(401).json({ + success: false, + response: [], + message: "User not found." + }) + } + + req.user = user + next() + + } catch (error) { + + res.status(401).json({ + success: false, + response: [], + message: "Invalid token." + }) + + } +} \ No newline at end of file diff --git a/models/thought.js b/models/thought.js new file mode 100644 index 0000000..7c6bd16 --- /dev/null +++ b/models/thought.js @@ -0,0 +1,27 @@ +import mongoose from "mongoose" + +const thoughtSchema = new mongoose.Schema({ + message: { + type: String, + required: true, + minLength: 5, + maxLength: 140 + }, + likes: [ + { + type: mongoose.Schema.Types.ObjectId, + ref: "User" + } + ], + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true + }, + createdAt: { + type: Date, + default: Date.now + } +}) + +export const Thought = mongoose.model("Thought", thoughtSchema) \ No newline at end of file diff --git a/models/user.js b/models/user.js new file mode 100644 index 0000000..c315fdc --- /dev/null +++ b/models/user.js @@ -0,0 +1,19 @@ +import mongoose from "mongoose" + +const userSchema = new mongoose.Schema({ + name: { + type: String, + required: true + }, + email: { + type: String, + required: true, + unique: true + }, + password: { + type: String, + required: true + } +}) + +export const User = mongoose.model("User", userSchema) \ No newline at end of file diff --git a/package.json b/package.json index bf25bb6..09bb299 100644 --- a/package.json +++ b/package.json @@ -9,11 +9,18 @@ "author": "", "license": "ISC", "dependencies": { - "@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", + "mongoose": "^8.15.1" + }, + "devDependencies": { + "@babel/core": "^7.27.4", + "@babel/node": "^7.27.1", + "@babel/preset-env": "^7.27.2", + "nodemon": "^3.1.10" } } diff --git a/server.js b/server.js index f47771b..effa935 100644 --- a/server.js +++ b/server.js @@ -1,21 +1,70 @@ import cors from "cors" import express from "express" +import listEndpoints from "express-list-endpoints" +import mongoose from "mongoose" +import dotenv from "dotenv" + +import { getThoughts } from "./controllers/getThoughts.js" +import { getThought } from "./controllers/getThought.js" +import { postThought } from "./controllers/postThought.js" +import { patchThought } from "./controllers/patchThought.js" +import { patchThoughtLikes } from "./controllers/patchThoughtLikes.js" +import { deleteThought } from "./controllers/deleteThought.js" +import { postUser } from "./controllers/postUser.js" +import { loginUser } from "./controllers/loginUser.js" +import { getUserData } from "./controllers/getUserData.js" +import { authenticateUser } from "./middleware/authenticateUser.js" + +dotenv.config() + +const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/happy-thoughts-api" +mongoose.connect(mongoUrl) -// 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 +// List all API endpoints for documentation + app.get("/", (req, res) => { - res.send("Hello Technigo!") + const endpoints = listEndpoints(app) + res.json({ + message: "Welcome to the Happy Thoughts API", + endpoints: endpoints + }) }) +// Set up endpoints + +// Endpoint for getting all thoughts +app.get("/thoughts", authenticateUser, getThoughts) + +// Endpoint for getting a specific thought by id +app.get("/thoughts/:id", authenticateUser, getThought) + +// Endpoint for posting a thought +app.post("/thoughts", authenticateUser, postThought) + +// Endpoint for updating a thought +app.patch("/thoughts/:id", authenticateUser, patchThought) + +// Endpoint for toggling likes +app.patch("/thoughts/:id/likes", authenticateUser, patchThoughtLikes) + +// Endpoint for deleting a thought +app.delete("/thoughts/:id", authenticateUser, deleteThought) + +// Endpoint for registering a user +app.post("/users/register", postUser) + +// Endpoint for logging in a user +app.post("/users/login", loginUser) + +// Endpoint for retrieving the data of an authenticated user +app.get("/users/me", authenticateUser, getUserData) + // Start the server app.listen(port, () => { console.log(`Server running on http://localhost:${port}`)