diff --git a/README.md b/README.md index 0f9f073..3e257bd 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,4 @@ Install dependencies with `npm install`, then start the server by running `npm r ## 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. + 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..f2980e5 --- /dev/null +++ b/middleware/authenticateUser.js @@ -0,0 +1,26 @@ +import { User } from '../models/user.js' + + +export const authenticateUser = async (req, res, next) => { + try { + const user = await User.findOne({ accessToken: req.header("Authorization") }) + + if (user) { + req.user = user + next() + + } else { + res.status(401).json({ + success: false, + message: "Authorization 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..da5f6a0 --- /dev/null +++ b/models/thought.js @@ -0,0 +1,26 @@ +import mongoose from 'mongoose' + +const thoughtSchema = new mongoose.Schema({ + message: { + type: String, + required: true, + minlength: 5, + maxlength: 140 + }, + hearts: { + type: Number, + default: 0 + }, + user: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true + }, + createdAt: { + type: Date, + default: Date.now + }, + }); + + + export const Thought = mongoose.model("Thought", thoughtSchema) diff --git a/models/user.js b/models/user.js new file mode 100644 index 0000000..181263f --- /dev/null +++ b/models/user.js @@ -0,0 +1,26 @@ +import mongoose from 'mongoose' +import crypto from 'crypto' + + +const userSchema = new mongoose.Schema({ + name: { + type: String, + required: true, + }, + email: { + type: String, + required: true, + unique: true + }, + password: { + type: String, + required: true, + }, + accessToken: { + type: String, + default: () => crypto.randomBytes(128).toString('hex') + } +}); + + +export const User = mongoose.model("User", userSchema); diff --git a/package.json b/package.json index bf25bb6..9e448e1 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "project-api", "version": "1.0.0", "description": "Project API", + "type": "module", "scripts": { "start": "babel-node server.js", "dev": "nodemon server.js --exec babel-node" @@ -9,11 +10,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.17.0", + "@babel/core": "^7.28.0", + "@babel/node": "^7.28.0", + "@babel/preset-env": "^7.28.0" + }, + "devDependencies": { + "nodemon": "^3.1.10" } } diff --git a/routes/thoughtRoutes.js b/routes/thoughtRoutes.js new file mode 100644 index 0000000..4743c77 --- /dev/null +++ b/routes/thoughtRoutes.js @@ -0,0 +1,212 @@ +import express from 'express' +import { Thought } from '../models/thought.js' +import { authenticateUser } from '../middleware/authenticateUser.js' + +const router = express.Router() + +// Endpoint for getting all thoughts. +router.get("/", async (req, res) => { + try { + const query = req.query; + const filteredThoughts = await Thought.find(query) + + if (filteredThoughts.length === 0) { + // Return 404 if no thoughts are found + return res.status(404).json({ + success: false, + response: [], + message: "No thoughts found for that query. Try another one." + }) + } + + // Return 200 if thoughts are found + res.status(200).json({ + success: true, + response: filteredThoughts, + message: "Success!" + }) + + } catch (error) { + // Return 500 server error database query fails. + res.status(500).json({ + success: false, + response: error, + message: "Internal server error! Failed to fetch thoughts." + }) + } +}) + + +// GET - Get a single thought by ID (endpoint is /thoughts/:id) +router.get("/:id", 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: "Thought not found" + }) + } + + res.status(200).json({ + success: true, + response: thought + }) + + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: "Internal server error! Failed to fetch thought." + }) + } +}) + + +// PATCH - Like a thought +router.patch("/:id/like", async (req,res) => { + const { id } = req.params + + try { + const updatedThought = await Thought.findByIdAndUpdate( + id, + { $inc: { hearts: 1 }}, // increase like by 1 + { new: true } + ) + if (!updatedThought) { + return res.status(404).json({ + success: false, + response: [], + message: "Thought not found." + }) + } + + res.status(200).json({ + success: true, + response: updatedThought, + message: "Thought liked successfully." + }) + + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: "Internal server error! Failed to like the thought." + }) + } +}) + + +// POST - Create a thought (endpoint is /thoughts) +router.post("/", authenticateUser, async (req, res) => { + const { message } = req.body + + try { + const newThought = await new Thought({ message, user: req.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 created successfully." + }) + + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: "Internal server error! Failed to post thought" + }) + } +}) + + +// PATCH - Update a thought (endpoint is thoughts/:id) +router.patch("/:id", authenticateUser, async (req, res) => { + const { id } = req.params + const { message } = req.body + + try { + const updatedThought = await Thought.findByIdAndUpdate(id, { message }, { + new: true, + runValidators: true + }) + + if(!updatedThought) { + return res.status(404).json({ + success: false, + response: [], + message: "Thought not found"}) + } + + res.status(200).json({ + success: true, + response: updatedThought, + message: "Thought updated successfully" + }) + + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: "Internal server error! Failed to update thought." + }) + } + }) + + +// DELETE - Delete a thought (endpoint is thoughts/:id) +router.delete("/:id", authenticateUser, 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! Thought not found"}) + } + + if (thought.user.toString() !== userId.toString()) { + return res.status(403).json({ + success: false, + response: [], + message: "Failed to delete thought! You are not authorized to delete this thought." + }) + } + + // Delete the thought + await Thought.findByIdAndDelete(id) + + res.status(200).json({ + success: true, + response: id, + message: "Thought deleted successfully" + }) + + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: "Internal server error! Failed to update thought." + }) + } + }) + + + export default router + + \ No newline at end of file diff --git a/routes/userRoutes.js b/routes/userRoutes.js new file mode 100644 index 0000000..dde3bc7 --- /dev/null +++ b/routes/userRoutes.js @@ -0,0 +1,71 @@ +import express from 'express' +import bcrypt from 'bcrypt' +import { User } from '../models/user.js' + +const router = express.Router() + + +// POST - Register a new user +router.post("/register", async (req, res) => { + try { + const { name, email, password } = req.body + const salt = bcrypt.genSaltSync() + const user = new User({ name, email, password: bcrypt.hashSync(password, salt) + }) + + //await to not send response before database finished saving + await user.save() + + res.status(201).json({ + success: true, + message: "User created successfully.", + response: { + id: user._id, + accessToken: user.accessToken + } + }) + + } catch (error) { + console.error("Signup error:", error) + res.status(400).json({ + success: false, + message: "Failed to create user.", + response: error.message + }) + } + }) + + +// POST - Login an existing user +router.post("/login", async (req, res) => { + try { + const { email, password } = req.body + const user = await User.findOne({ email }) + + if (user && bcrypt.compareSync(password, user.password)) { + res.status(200).json({ + success: true, + accessToken: user.accessToken, + id: user.id, + message: "Login successful" + }) + + } else { + res.status(401).json({ + success: false, + message: "Invalid email or password" + }) + } + + } catch (error) { + res.status(500).json({ + success: false, + message: "Something went wrong", + error: error.message + }) + } + }) + + + export default router + \ No newline at end of file diff --git a/server.js b/server.js index f47771b..856d28e 100644 --- a/server.js +++ b/server.js @@ -1,22 +1,41 @@ -import cors from "cors" -import express from "express" +import cors from 'cors' +import express from 'express' +import listEndpoints from 'express-list-endpoints' +import mongoose from 'mongoose' +import dotenv from 'dotenv' +import thoughtRoutes from './routes/thoughtRoutes.js' +import userRoutes from './routes/userRoutes.js' + +dotenv.config() + + +// Connect to Mongo DB database +const mongoURL = process.env.MONGO_URL || "mongodb://localhost:27017/happythoughts" +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 + +// Add middlewares to enable cors and json body parsing. app.use(cors()) app.use(express.json()) + -// Start defining your routes here +// Start defining your routes here. Add documentation of the API here with express-list-endpoints. app.get("/", (req, res) => { - res.send("Hello Technigo!") + const endpoints = listEndpoints(app) + res.json({ + message: "Welcome to the Happy Toughts", + endpoints: endpoints + }) }) -// Start the server +app.use("/thoughts", thoughtRoutes) +app.use("/users", userRoutes) + + +// Start the server. app.listen(port, () => { console.log(`Server running on http://localhost:${port}`) })