diff --git a/README.md b/README.md index 0f9f073..fc6f271 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,38 @@ -# Project API +# Happy Thoughts API -This project includes the packages and babel setup for an express server, and is just meant to make things a little simpler to get up and running with. +A RESTful API for sharing and managing happy thoughts with user authentication. -## Getting started +## API Endpoints -Install dependencies with `npm install`, then start the server by running `npm run dev` +### Root -## View it live +`GET /` +Welcome message for the API. -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. +### Thoughts + +`GET /thoughts` +Retrieves all thoughts. + +`POST /thoughts` +Creates a new thought. + +`GET /thoughts/:id` +Retrieves a specific thought by ID. + +`PUT /thoughts/:id` +Updates a specific thought. + +`DELETE /thoughts/:id` +Deletes a specific thought. + +`POST /thoughts/:id/like` +Adds a like (heart) to a specific thought. + +### Users + +`POST /users` +Registers a new user. + +`POST /users/sessions` +Logs in an existing user. diff --git a/app.js b/app.js new file mode 100644 index 0000000..1b3a237 --- /dev/null +++ b/app.js @@ -0,0 +1,48 @@ +import cors from "cors"; +import dotenv from "dotenv"; +import express from "express"; +import mongoose from "mongoose"; + +import userRoutes from "./routes/auth.js"; +import thoughtRoutes from "./routes/thoughts.js"; + +dotenv.config(); + +const app = express(); + +const allowedOrigins = [ + "http://localhost:5173", + "https://happythoughtproject.netlify.app", +]; + +// === Middleware === +app.use( + cors({ + origin: (origin, callback) => { + if (!origin) return callback(null, true); + + if (allowedOrigins.includes(origin)) { + callback(null, true); + } else { + callback(new Error("Not allowed by CORS")); + } + }, + credentials: true, + }) +); +app.use(express.json()); + +// === Connect to MongoDB === +const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/happyThoughts"; +mongoose.connect(mongoUrl); +mongoose.Promise = Promise; + +// === Routes === +app.get("/", (req, res) => { + res.send("Welcome to the Happy Thoughts API!"); +}); + +app.use("/thoughts", thoughtRoutes); +app.use("/users", userRoutes); + +export default app; diff --git a/middlewares/authenticateUser.js b/middlewares/authenticateUser.js new file mode 100644 index 0000000..5c0ecf9 --- /dev/null +++ b/middlewares/authenticateUser.js @@ -0,0 +1,21 @@ +import { User } from "../models/User.js"; + +const authenticateUser = async (req, res, next) => { + const authHeader = req.header("Authorization"); + + const accessToken = authHeader?.replace("Bearer ", ""); + + try { + const user = await User.findOne({ accessToken }); + if (user) { + req.user = user; + next(); + } else { + res.status(401).json({ message: "Access token is invalid" }); + } + } catch (error) { + res.status(500).json({ message: "Server error during authentication" }); + } +}; + +export default authenticateUser; diff --git a/models/Thought.js b/models/Thought.js new file mode 100644 index 0000000..e9ca4b9 --- /dev/null +++ b/models/Thought.js @@ -0,0 +1,31 @@ +import mongoose from "mongoose"; + +const thoughtSchema = new mongoose.Schema( + { + message: { + type: String, + required: [true, "Message is required"], + minlength: [5, "Message must be at least 5 characters"], + maxlength: [140, "Message cannot exceed 140 characters"], + trim: true, + }, + hearts: { + type: Number, + default: 0, + }, + }, + { + timestamps: true, + }, + { + createdBy: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: 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..0151c36 --- /dev/null +++ b/models/User.js @@ -0,0 +1,29 @@ +import crypto from "crypto"; +import mongoose from "mongoose"; + +const userSchema = new mongoose.Schema({ + username: { + type: String, + unique: true, + required: true, + minlength: 3, + maxlength: 30, + }, + password: { + type: String, + required: true, + minlength: 5, + }, + accessToken: { + type: String, + default: () => crypto.randomBytes(128).toString("hex"), + }, + email: { + type: String, + unique: true, + required: true, + default: () => crypto.randomBytes(10).toString("hex"), + }, +}); + +export const User = mongoose.model("User", userSchema); diff --git a/package.json b/package.json index bf25bb6..864d8ac 100644 --- a/package.json +++ b/package.json @@ -9,11 +9,16 @@ "author": "", "license": "ISC", "dependencies": { - "@babel/core": "^7.17.9", - "@babel/node": "^7.16.8", - "@babel/preset-env": "^7.16.11", + "@babel/core": "^7.27.4", + "@babel/node": "^7.27.1", + "@babel/preset-env": "^7.27.2", + "bcrypt": "^6.0.0", + "bcrypt-nodejs": "^0.0.3", "cors": "^2.8.5", + "dotenv": "^16.5.0", "express": "^4.17.3", - "nodemon": "^3.0.1" + "express-list-endpoints": "^7.1.1", + "mongoose": "^8.15.1", + "nodemon": "^3.1.10" } } diff --git a/routes/auth.js b/routes/auth.js new file mode 100644 index 0000000..284cf59 --- /dev/null +++ b/routes/auth.js @@ -0,0 +1,59 @@ +import bcrypt from "bcrypt"; +import crypto from "crypto"; +import express from "express"; + +import { User } from "../models/User.js"; + +const router = express.Router(); + +router.post("/", async (req, res) => { + console.log("Incoming body:", req.body); + const { username, password } = req.body; + + try { + const salt = bcrypt.genSaltSync(); + const hashedPassword = bcrypt.hashSync(password, salt); + const user = new User({ + username, + password: hashedPassword, + accessToken: crypto.randomBytes(128).toString("hex"), + email: new Date().getTime(), + }); + + await user.save(); + + res.status(201).json({ + userId: user._id, + username: user.username, + accessToken: user.accessToken, + }); + } catch (err) { + console.error("Signup error:", err); + if (err.code === 11000) { + res.status(400).json({ message: "Username already exists" }); + } else { + res.status(400).json({ message: "Invalid request", error: err }); + } + } +}); + +router.post("/sessions", async (req, res) => { + const { username, password } = req.body; + + try { + const user = await User.findOne({ username }); + if (user && bcrypt.compareSync(password, user.password)) { + res.json({ + username: user.username, + userId: user._id, + accessToken: user.accessToken, + }); + } else { + res.status(401).json({ error: "Username or password is incorrect" }); + } + } catch (err) { + res.status(500).json({ error: "Internal server error" }); + } +}); + +export default router; diff --git a/routes/thoughts.js b/routes/thoughts.js new file mode 100644 index 0000000..ac650df --- /dev/null +++ b/routes/thoughts.js @@ -0,0 +1,103 @@ +import express from "express"; + +import authenticateUser from "../middlewares/authenticateUser.js"; +import HappyThought from "../models/Thought.js"; + +const router = express.Router(); + +// get last 20 posts +router.get("/", async (req, res) => { + try { + const thoughts = await HappyThought.find() + .sort({ createdAt: -1 }) + .limit(20); + res.status(200).json(thoughts); + } catch (error) { + res.status(500).json({ error: "Could not fetch thoughts" }); + } +}); + +// get single post +router.get("/:id", async (req, res) => { + try { + const thought = await HappyThought.findById(req.params.id); + if (thought) { + res.json(thought); + } else { + res.status(404).json({ error: "Thought not found" }); + } + } catch (error) { + res.status(400).json({ error: "Invalid ID format" }); + } +}); + +// add new post +router.post("/", authenticateUser, async (req, res) => { + const { message } = req.body; + + try { + const newThought = new HappyThought({ + message, + createdBy: req.user._id, // lägg till användaren som skapar + }); + + const savedThought = await newThought.save(); + res.status(201).json(savedThought); + } catch (error) { + res.status(400).json({ error: "Invalid input", details: error.errors }); + } +}); + +// update existing post +router.put("/:id", async (req, res) => { + try { + const updatedThought = await HappyThought.findByIdAndUpdate( + req.params.id, + req.body, + { new: true, runValidators: true } + ); + + if (updatedThought) { + res.status(200).json(updatedThought); + } else { + res.status(404).json({ error: "Thought not found" }); + } + } catch (error) { + res.status(400).json({ error: "Invalid update", details: error.errors }); + } +}); + +// delete post +router.delete("/:id", async (req, res) => { + try { + const deleted = await HappyThought.findByIdAndDelete(req.params.id); + if (deleted) { + res.status(200).json({ success: true }); + } else { + res.status(404).json({ error: "Thought not found" }); + } + } catch (error) { + res.status(400).json({ error: "Invalid ID format" }); + } +}); + +// like post +router.post("/:id/like", async (req, res) => { + try { + const thought = await HappyThought.findByIdAndUpdate( + req.params.id, + { $inc: { hearts: 1 } }, + { new: true } + ); + + if (thought) { + res.status(200).json(thought); + } else { + res.status(404).json({ error: "Thought not found" }); + } + } catch (error) { + res.status(400).json({ error: "Invalid ID format" }); + } +}); + +export default router; diff --git a/server.js b/server.js index f47771b..15adb01 100644 --- a/server.js +++ b/server.js @@ -1,22 +1,14 @@ -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 app from "./app.js"; -// Add middlewares to enable cors and json body parsing -app.use(cors()) -app.use(express.json()) +const PORT = process.env.PORT || 8081; -// Start defining your routes here -app.get("/", (req, res) => { - res.send("Hello Technigo!") -}) +app.listen(PORT, () => { + console.log(`🚀 Server running on http://localhost:${PORT}`); + console.log(`📝 Thoughts API: http://localhost:${PORT}/thoughts`); + console.log(`👤 Users API: http://localhost:${PORT}/users`); -// Start the server -app.listen(port, () => { - console.log(`Server running on http://localhost:${port}`) -}) + console.log("\n📋 All API Endpoints:"); + console.log(listEndpoints(app)); +});