diff --git a/data.json b/data.json index a2c844f..20e18df 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", @@ -100,7 +100,7 @@ { "_id": "682c6e65951f7a0017130021", "message": "The weather is nice!", - "hearts": 0, + "hearts": 2, "createdAt": "2025-05-20T11:58:29.662Z", "__v": 0 }, @@ -117,5 +117,75 @@ "hearts": 37, "createdAt": "2025-05-19T22:07:08.999Z", "__v": 0 + }, + { + "_id": "650b32c9e4bce17db221d8a7", + "message": "Long bike rides with no destination πŸš΄β€β™€οΈ", + "hearts": 19, + "createdAt": "2025-03-03T10:43:19.553Z", + "__v": 0 + }, + { + "_id": "64fcde98be72c80afc8d33ee", + "message": "When everything just clicks at work πŸ’‘", + "hearts": 25, + "createdAt": "2025-01-17T14:11:07.734Z", + "__v": 0 + }, + { + "_id": "63d1ecfc22cb0f173a6a8894", + "message": "A surprise sunny day in February β˜€οΈ", + "hearts": 30, + "createdAt": "2025-02-20T12:50:45.286Z", + "__v": 0 + }, + { + "_id": "64bb9b72a7a6b87ffce30d44", + "message": "Waking up before the alarm feeling refreshed 😴", + "hearts": 13, + "createdAt": "2025-04-10T06:39:58.914Z", + "__v": 0 + }, + { + "_id": "649a1dc0cced95dfad9a7e33", + "message": "Watching birds from the balcony 🐦", + "hearts": 6, + "createdAt": "2025-03-28T08:23:30.201Z", + "__v": 0 + }, + { + "_id": "652ee0db90de8445dfc29a2e", + "message": "Late-night deep talks with a friend πŸŒ™", + "hearts": 31, + "createdAt": "2025-01-04T23:57:12.483Z", + "__v": 0 + }, + { + "_id": "63cf410d27b5f2cf6a97c028", + "message": "A clean desk and a clear mind 🧼🧠", + "hearts": 10, + "createdAt": "2025-05-06T11:45:01.006Z", + "__v": 0 + }, + { + "_id": "64aaf9ee8d8e2953df0cd987", + "message": "Hearing your favorite song on the radio πŸ“»", + "hearts": 20, + "createdAt": "2025-04-25T15:20:38.527Z", + "__v": 0 + }, + { + "_id": "64b4b702df25e8e93f8a3e49", + "message": "The smell of fresh rain 🌧️", + "hearts": 12, + "createdAt": "2025-03-10T18:11:26.749Z", + "__v": 0 + }, + { + "_id": "6504d2b53a7bc742b7fbe114", + "message": "Seeing someone smile because of you 😊", + "hearts": 28, + "createdAt": "2025-02-12T20:02:14.318Z", + "__v": 0 } -] \ No newline at end of file +] diff --git a/middlewares/authenticationUser.js b/middlewares/authenticationUser.js new file mode 100644 index 0000000..1fb4ffd --- /dev/null +++ b/middlewares/authenticationUser.js @@ -0,0 +1,25 @@ +import User from "../models/User.js"; + +const authenticationUser = async (req, res, next) => { + try { + const authHeader = req.headers && req.headers.authorization; + const accessToken = + authHeader && authHeader.startsWith("Bearer ") + ? authHeader.split(" ")[1] + : null; + if (!accessToken) { + return res.status(401).json({ error: "Access token required" }); + } + const user = await User.findOne({ accessToken }); + if (user) { + req.user = user; + return next(); + } else { + return res.status(401).json({ loggedout: true, error: "Unauthorized" }); + } + } catch (error) { + return res.status(500).json({ error: "Internal server error" }); + } +}; + +export default authenticationUser; diff --git a/models/Thought.js b/models/Thought.js new file mode 100644 index 0000000..89f72d7 --- /dev/null +++ b/models/Thought.js @@ -0,0 +1,28 @@ +import mongoose from "mongoose"; + +const thoughtSchema = new mongoose.Schema({ + message: { + type: String, + trim: true, + required: true, + minlength: 5, + maxlength: 140, + }, + hearts: { + type: Number, + default: 0, + }, + createdAt: { + type: Date, + default: Date.now, + }, + username: { + type: String, + required: true, + trim: true, + }, +}); + +const Thought = mongoose.model("Thought", thoughtSchema); + +export default Thought; diff --git a/models/User.js b/models/User.js new file mode 100644 index 0000000..bf2e77d --- /dev/null +++ b/models/User.js @@ -0,0 +1,29 @@ +import mongoose from "mongoose"; +import crypto from "crypto"; + +const userSchema = new mongoose.Schema({ + username: { + type: String, + required: true, + unique: true, + index: true, // Add index for optimization + }, + email: { + type: String, + required: true, + unique: true, + index: true, // Add index for optimization + }, + password: { + type: String, + required: true, + }, + accessToken: { + type: String, + default: () => crypto.randomBytes(37).toString("hex"), + }, +}); + +const User = mongoose.models.User || mongoose.model("User", userSchema); + +export default User; diff --git a/package.json b/package.json index bf25bb6..dfda471 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,35 @@ { - "name": "project-api", + "name": "js-project-api", "version": "1.0.0", - "description": "Project API", + "description": " JS Project API", + "homepage": "https://github.com/KidFromCalifornia/js-project-api#readme", + "bugs": { + "url": "https://github.com/KidFromCalifornia/js-project-api/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/KidFromCalifornia/js-project-api.git" + }, + "license": "ISC", + "author": "", + "type": "commonjs", + "main": "server.js", "scripts": { "start": "babel-node server.js", "dev": "nodemon server.js --exec babel-node" }, - "author": "", - "license": "ISC", "dependencies": { "@babel/core": "^7.17.9", "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", + "bcrypt-nodejs": "^0.0.3", + "bcryptjs": "^3.0.2", "cors": "^2.8.5", - "express": "^4.17.3", - "nodemon": "^3.0.1" + "dotenv": "^16.5.0", + "express": "^4.21.2", + "express-list-endpoints": "^7.1.1", + "mongoose": "^8.15.1", + "nodemon": "^3.0.1", + "validator": "^13.15.15" } } diff --git a/routes/thoughts.js b/routes/thoughts.js new file mode 100644 index 0000000..7488751 --- /dev/null +++ b/routes/thoughts.js @@ -0,0 +1,170 @@ +import express from "express"; +import Thought from "../models/Thought.js"; +import authenticationUser from "../middlewares/authenticationUser.js"; + +const router = express.Router(); + +router.get("/", async (_, res) => { + try { + const thoughts = await Thought.find().sort({ createdAt: -1 }).limit(20); + res.json(thoughts); + } catch (err) { + res.status(500).json({ error: "Internal server error" }); + } +}); + +router.get("/search/:word", async (req, res) => { + const word = req.params.word.toLowerCase(); + try { + const filtered = await Thought.find({ + message: { $regex: word, $options: "i" }, + }); + res.json(filtered); + } catch (err) { + res.status(500).json({ error: "Internal server error" }); + } +}); + +router.get("/hearts/:min", async (req, res) => { + const min = Number(req.params.min); + if (isNaN(min)) { + return res + .status(400) + .json({ error: "Invalid 'min' parameter. Must be a number." }); + } + try { + const filteredThoughts = await Thought.find({ hearts: { $gte: min } }); + res.json(filteredThoughts); + } catch (err) { + res.status(500).json({ error: "Internal server error" }); + } +}); + +router.get("/page/:page", async (req, res) => { + const page = Number(req.params.page); + const pageSize = 5; + if (!Number.isInteger(page) || page <= 0) { + return res.status(400).json({ error: "Page must 1 or more " }); + } + try { + const thoughts = await Thought.find() + .sort({ createdAt: -1 }) + .skip((page - 1) * pageSize) + .limit(pageSize); + res.json(thoughts); + } catch (err) { + res.status(500).json({ error: "Internal server error" }); + } +}); + +router.get("/:id", async (req, res) => { + try { + const thought = await Thought.findById(req.params.id); + if (thought) { + res.json(thought); + } else { + res.status(404).json({ error: "Thought not found" }); + } + } catch (err) { + res.status(400).json({ error: "Invalid ID" }); + } +}); +///////////////// +// post routes // +///////////////// + +router.post("/", authenticationUser, async (req, res) => { + const { message, hearts } = req.body; + try { + const newThought = await Thought.create({ + message, + hearts: hearts || 0, + username: req.user.username, + }); + res.status(201).json(newThought); + } catch (err) { + res + .status(400) + .json({ error: "Could not create thought", details: err.message }); + } +}); + +router.post("/:id/like", authenticationUser, async (req, res) => { + try { + const addHearts = await Thought.findByIdAndUpdate( + req.params.id, + { $inc: { hearts: 1 } }, + { new: true } + ); + if (addHearts) { + res.json(addHearts); + } else { + res.status(404).json({ error: "Thought not found" }); + } + } catch (err) { + res.status(400).json({ error: "Invalid ID" }); + } +}); + +//////////////////// +// Delete routes /// +//////////////////// + +router.delete("/:id", authenticationUser, async (req, res) => { + try { + const thought = await Thought.findById(req.params.id); + if (!thought) { + return res.status(404).json({ error: "Thought not found" }); + } + if (thought.username !== req.user.username) { + // <--- Compare username + return res + .status(403) + .json({ error: "You can only delete your own thoughts" }); + } + await thought.deleteOne(); + res.json({ success: true, deleted: thought }); + } catch (err) { + res.status(400).json({ error: "Invalid ID" }); + } +}); +////////////////// +// Patch routes // +////////////////// + +router.patch("/:id", authenticationUser, async (req, res) => { + try { + const thought = await Thought.findById(req.params.id); + if (!thought) { + return res.status(404).json({ error: "Thought not found" }); + } + if (thought.username !== req.user.username) { + return res + .status(403) + .json({ error: "You can only edit your own thoughts" }); + } + const updatedThought = await Thought.findByIdAndUpdate( + req.params.id, + req.body, + { new: true, runValidators: true } + ); + res.json({ success: true, thought: updatedThought }); + } catch (err) { + res + .status(400) + .json({ error: "Could not update thought", details: err.message }); + } +}); + +//if (process.env.RESET_DATABASE) { +// console.log("Resetting database"); +//const seedThoughts = async () => { +//await Thought.deleteMany({}); +//await new Thought({ message: "This is a test one", hearts: 0 }).save(); +//await new Thought({ message: "This is a test two", hearts: 7 }).save(); +//await new Thought({ message: "This is a test 3", hearts: 5 }).save(); +//}; +//seedThoughts(); +//} + +export default router; diff --git a/routes/users.js b/routes/users.js new file mode 100644 index 0000000..6942227 --- /dev/null +++ b/routes/users.js @@ -0,0 +1,57 @@ +import express from "express"; +import bcrypt from "bcryptjs"; +import validator from "validator"; +import User from "../models/User.js"; + +const router = express.Router(); + +router.post("/login", async (req, res) => { + const { username, password } = req.body; + const user = await User.findOne({ username }); + if (user && bcrypt.compareSync(password, user.password)) { + res.json({ userId: user._id, accessToken: user.accessToken }); + } else { + res.status(401).json({ error: "Invalid username or password" }); + } +}); + +router.post("/register", async (req, res) => { + const { username, email, password } = req.body; + + // 1. Validate input first! + if (!username || !email || !password) { + return res.status(400).json({ error: "All fields are required" }); + } + if (!validator.isEmail(email)) { + return res.status(400).json({ error: "Invalid email format" }); + } + if (password.length < 6) { + return res + .status(400) + .json({ error: "Password must be at least 6 characters long" }); + } + const existingUser = await User.findOne({ + $or: [{ username }, { email }], + }); + if (existingUser) { + return res.status(400).json({ error: "Username or email already exists" }); + } + + // 2. Only hash and save after validation + const salt = bcrypt.genSaltSync(); + const user = new User({ + username, + email, + password: bcrypt.hashSync(password, salt), + }); + await user.save(); + + res.status(201).json({ + success: true, + message: "User created successfully", + userId: user._id, + accessToken: user.accessToken, + user, + }); +}); +export default router; diff --git a/server.js b/server.js index f47771b..cca267f 100644 --- a/server.js +++ b/server.js @@ -1,22 +1,38 @@ -import cors from "cors" -import express from "express" +import cors from "cors"; +import express from "express"; +import mongoose from "mongoose"; +import dotenv from "dotenv"; +import thoughtsRoutes from "./routes/thoughts.js"; +import usersRoutes from "./routes/users.js"; -// 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() +dotenv.config(); -// Add middlewares to enable cors and json body parsing -app.use(cors()) -app.use(express.json()) +const mongoUrl = process.env.MONGO_URL; +mongoose.connect(mongoUrl); +mongoose.Promise = Promise; + +const port = process.env.PORT || 8088; +const app = express(); +const listEndpoints = require("express-list-endpoints"); +app.use( + cors({ + origin: "*", // Allow all origins for development + }) +); +app.use(express.json()); + +// Use routesrs +app.use("/thoughts", thoughtsRoutes); +app.use("/", usersRoutes); -// Start defining your routes here app.get("/", (req, res) => { - res.send("Hello Technigo!") -}) + const endpoints = listEndpoints(app); + res.json({ + message: " Welcome to my Endpoint to Happy Thoughts API", + endpoints, + }); +}); -// Start the server app.listen(port, () => { - console.log(`Server running on http://localhost:${port}`) -}) + console.log(`Server running on http://localhost:${port}`); +});