diff --git a/middleware/auth.js b/middleware/auth.js new file mode 100644 index 0000000..e26b512 --- /dev/null +++ b/middleware/auth.js @@ -0,0 +1,13 @@ +import User from "../models/User.js"; + +const authenticateUser = async (req, res, next) => { + const user = await User.findOne({ accessToken: req.header("Authorization") }); + if (user) { + req.user = user; + next(); + } else { + res.status(401).json({ error: "Authentication failed" }); + } +}; + +export default authenticateUser; diff --git a/models/User.js b/models/User.js new file mode 100644 index 0000000..897fcb2 --- /dev/null +++ b/models/User.js @@ -0,0 +1,28 @@ +import mongoose from "mongoose"; +import bcrypt from "bcryptjs"; +import crypto from "crypto"; + +const userSchema = new mongoose.Schema({ + name: { + type: String, + unique: true, + required: true, + }, + email: { + type: String, + unique: true, + required: true, + }, + password: { + type: String, + required: true, + }, + accessToken: { + type: String, + default: () => crypto.randomBytes(128).toString("hex"), + }, +}); + +const User = mongoose.model("User", userSchema); + +export default User; diff --git a/package.json b/package.json index bf25bb6..2e7d8f5 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,12 @@ "@babel/core": "^7.17.9", "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", + "bcryptjs": "^3.0.3", "cors": "^2.8.5", - "express": "^4.17.3", + "dotenv": "^17.2.3", + "express": "^4.22.1", + "jsonwebtoken": "^9.0.3", + "mongoose": "^9.1.5", "nodemon": "^3.0.1" } } diff --git a/routes/userRoutes.js b/routes/userRoutes.js new file mode 100644 index 0000000..2cbf070 --- /dev/null +++ b/routes/userRoutes.js @@ -0,0 +1,51 @@ +import express from "express"; +import jwt from "jsonwebtoken"; +import User from "../models/User.js"; +import bcrypt from "bcryptjs"; + +const router = express.Router(); + +// Register new user +router.post("/users", async (req, res) => { + try { + // Get data from request body + const { name, email, password } = req.body; + + // Does the email already exist? + const existingUser = await User.findOne({ email }); + if (existingUser) { + // if the email already exists, return error messages + return res.status(400).json({ error: "That email already exists" }); + } + + // Create a new user with a hashed password + const user = new User({ name, email, password: bcrypt.hashSync(password) }); + + await user.save(); + + // Return user ID and token on successful registration + res.status(201).json({ id: user._id, accessToken: user.accessToken }); + } catch (err) { + res + .status(400) + .json({ message: "Could not create user", errors: err.errors }); + } +}); + +// When user Log in, send email and password, receive accessToken. +router.post("/sessions", async (req, res) => { + try { + const user = await User.findOne({ email: req.body.email }); + + // Check if the user exists AND if the password is correct + if (user && bcrypt.compareSync(req.body.password, user.password)) { + res.json({ userId: user._id, accessToken: user.accessToken }); + } else { + res.status(401).json({ message: "Invalid email or password" }); + } + } catch (err) { + res.status(500).json({ error: "Login failed" }); + } +}); + +export default router; diff --git a/server.js b/server.js index f47771b..8584040 100644 --- a/server.js +++ b/server.js @@ -1,22 +1,201 @@ -import cors from "cors" -import express from "express" +import cors from "cors"; // Allows requests from other domains (frontend) +import express from "express"; // Web server framework +import mongoose from "mongoose"; // Database connection to MongoDB +import authenticateUser from "./middleware/auth.js"; // Middleware to protect routes +import userRoutes from "./routes/userRoutes.js"; // Routes for register/login -// 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() +// Load environment variables from the .env file +import dotenv from "dotenv"; +dotenv.config(); -// Add middlewares to enable cors and json body parsing -app.use(cors()) -app.use(express.json()) +//DATABASE CONNECTION +const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/HappyThoughts"; +mongoose.connect(mongoUrl); -// Start defining your routes here +// Log when we have connected to the database +mongoose.connection.on("connected", () => { + console.log(" Connected to MongoDB"); +}); + +// Log if something goes wrong with the database connection +mongoose.connection.on("error", (err) => { + console.log("MongoDB connection error:", err); +}); +mongoose.Promise = Promise; + +// THOUGHT-MODELL +// Defines what a "thought" looks like in the database +const Thought = mongoose.model("Thought", { + message: { + type: String, + required: true, + minlength: 3, + maxlength: 140, + }, + hearts: { + type: Number, + default: 0, + }, + createdAt: { + type: Date, + default: () => new Date(), + }, +}); + +// SERVER-SETUP +const port = process.env.PORT || 8080; +const app = express(); + +// Middleware +app.use(cors()); +app.use(express.json()); + +// ROUTES +// API-dokumentation app.get("/", (req, res) => { - res.send("Hello Technigo!") -}) + res.json({ + message: "Welcome to Happy Thoughts API", + endpoints: [ + { method: "GET", path: "/", description: "API documentation" }, + { method: "GET", path: "/thoughts", description: "Get all thoughts" }, + { + method: "GET", + path: "/thoughts/random", + description: "Get a random thought", + }, + { + method: "GET", + path: "/thoughts/:id", + description: "Get a specific thought by ID", + }, + { + method: "POST", + path: "/thoughts", + description: "Create a new thought (requires authentication)", + }, + { + method: "DELETE", + path: "/thoughts/:id", + description: "Delete a thought (requires authentication)", + }, + { + method: "PATCH", + path: "/thoughts/:id", + description: "Add a heart to a thought (requires authentication)", + }, + { + method: "PATCH", + path: "/thoughts/:id/update", + description: "Update message (requires authentication)", + }, + ], + }); +}); + +// Get all thoughts +app.get("/thoughts", async (req, res) => { + try { + const results = await Thought.find(); + res.json(results); + } catch (err) { + res.status(500).json({ error: "Could not fetch thoughts" }); + } +}); + +// Get a random thought +app.get("/thoughts/random", async (req, res) => { + try { + const thoughts = await Thought.find(); + if (thoughts.length === 0) { + return res.status(404).json({ error: "No thoughts found" }); + } + const randomIndex = Math.floor(Math.random() * thoughts.length); + res.json(thoughts[randomIndex]); + } catch (err) { + res.status(500).json({ error: "Could not get random thought" }); + } +}); + +// Get a specific thought by ID +app.get("/thoughts/: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 do not exist" }); + } + } catch (error) { + res.status(400).json({ error: "Invalid thought ID" }); + } +}); + +// Create a new thought – PROTECTED (requires login) +// authenticateUser runs FIRST and checks that the user has a valid token +app.post("/thoughts", authenticateUser, async (req, res) => { + try { + const newThought = new Thought({ + message: req.body.message, + hearts: 0, + }); + const savedThought = await newThought.save(); + res.status(201).json(savedThought); + } catch (err) { + res.status(400).json({ error: err.message }); + } +}); + +// Delete thought – PROTECTED (requires login) +app.delete("/thoughts/:id", authenticateUser, async (req, res) => { + try { + const deleted = await Thought.findByIdAndDelete(req.params.id); + + if (!deleted) { + return res.status(404).json({ error: "Thought not found" }); + } + res.status(200).json({ message: "Deleted", id: req.params.id }); + } catch (err) { + res.status(400).json({ error: "Invalid ID" }); + } +}); + +// Add a heart to a thought – PROTECTED (requires login) +app.patch("/thoughts/:id", authenticateUser, async (req, res) => { + try { + const updatedThought = await Thought.findByIdAndUpdate( + req.params.id, + { $inc: { hearts: 1 } }, + { new: true }, + ); + if (!updatedThought) { + return res.status(404).json({ error: "Thought not found" }); + } + res.status(200).json(updatedThought); + } catch { + res.status(400).json({ error: "Unable to add heart to thought" }); + } +}); + +// User updates a thought – PROTECTED (requires login) +app.patch("/thoughts/:id/update", authenticateUser, async (req, res) => { + try { + const updatedThought = await Thought.findByIdAndUpdate( + req.params.id, + { message: req.body.message }, + { new: true }, + ); + if (!updatedThought) { + return res.status(404).json({ error: "Thought not found" }); + } + res.status(200).json(updatedThought); + } catch { + res.status(400).json({ error: "Unable to update message" }); + } +}); + +app.use(userRoutes); -// Start the server +// START SERVERN app.listen(port, () => { - console.log(`Server running on http://localhost:${port}`) -}) + console.log(`Server running on http://localhost:${port}`); +});