diff --git a/package.json b/package.json index bf25bb6..dc60ca8 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" @@ -12,8 +13,12 @@ "@babel/core": "^7.17.9", "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", - "cors": "^2.8.5", + "bcrypt": "^6.0.0", + "cors": "^2.8.6", + "dotenv": "^17.2.4", "express": "^4.17.3", + "express-list-endpoints": "^7.1.1", + "mongoose": "^9.2.0", "nodemon": "^3.0.1" } } diff --git a/routes/thoughtRoutes.js b/routes/thoughtRoutes.js new file mode 100644 index 0000000..b6018bb --- /dev/null +++ b/routes/thoughtRoutes.js @@ -0,0 +1,248 @@ +import express from "express" +import mongoose from "mongoose" +import { User } from "./userRoutes.js" + +const router = express.Router(); + +const authenticateUser = async (req, res, next) => { + try { + const user = await User.findOne({ + accessToken: req.header('Authorization').replace("Bearer ", ""), + }) + if (user) { + req.user = user + next() + } else { + res.status(401).json({ + message: "Authentication missing / invalid", + loggedOut: true + }) + } + } catch (error) { + res.status(500).json({ message: "Internal server error", error: error.message }) + } +} + +// Thought schema +const thoughtSchema = new mongoose.Schema({ + message: { type: String, required: true }, + hearts: { type: Number, default: 0 }, + createdAt: { type: Date, default: Date.now }, + userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true } +}) + +const Thought = mongoose.model('Thought', thoughtSchema) + + +// Show all thoughts +router.get("/thoughts", async (req, res) => { + try { + const thoughts = await Thought.find().sort({ createdAt: "desc" }) + res.json(thoughts) + + } catch (error) { + res.status(500).json({ error: "Failed to fetch thougts" }) + } +}) + + +// Show thoughts with likes +router.get("/thoughts/like", async (req, res) => { + const { hearts } = req.query + + const dbQuery = {} + if (hearts !== undefined) { + const heartsNum = Number(hearts) + if (!Number.isNaN(heartsNum)) { + dbQuery.hearts = heartsNum + } + } + + try { + const thoughts = await Thought.find(dbQuery) + if (thoughts.length === 0) { + return res.status(404).json({ + success: false, + response: [], + message: "No thoughts match the query" + }) + } + + return res.status(200).json({ + success: true, + response: thoughts, + message: "Thoughts retrieved" + }) + } catch (error) { + console.error(error); + return res.status(500).json({ + success: false, + response: [], + message: error + }) + } +}) + + +// Filter by ID +router.get("/thoughts/:id", async (req, res) => { + const id = req.params.id + + try { + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ + success: false, + response: null, + message: "Invalid ID format", + + }) + } + const thought = await Thought.findById(id) + + if (!thought) { + return res.status(404).json({ + success: false, + response: null, + message: "Thought not found", + }) + } + + return res.status(200).json({ + success: true, + response: thought, + message: "Success", + }) + } + + catch (error) { + return res.status(500).json({ + success: false, + response: null, + message: error, + }) + } +} +) + + +// Post +router.post("/thoughts", authenticateUser, async (req, res) => { + const { message } = req.body + + try { + const newThought = await new Thought({ + message, userId: req.user._id, + }).save() + + if (!newThought) { + return res + .status(400) + .json({ success: false, data: null, message: "Failed to post thought" }) + } + + res.status(201).json({ + success: true, + data: newThought, + message: "Thought created successfully." + }) + + } catch (error) { + console.log(error) + res.status(500).json({ + success: false, + data: null, + message: error.message || "Server error" + }) + } +}) + + +// Edit +router.patch('/thoughts/:id', authenticateUser, async (req, res) => { + const { id } = req.params + + try { + const thought = await Thought.findById(id); + if (!thought) { + return res.status(404).json({ error: "Thought not found" }); + } + if (thought.userId.toString() !== req.user._id.toString()) { + return res.status(403).json({ error: "You can only edit your own thoughts" }); + } + + thought.message = req.body.message ?? thought.message; + thought.hearts = req.body.hearts ?? thought.hearts; + await thought.save(); + + return res.json({ success: true, thought }); + } catch (err) { + return res.status(400).json({ error: "Invalid request", details: err.message }); + } +}) + + +// Like +router.post("/thoughts/:id/like", authenticateUser, async (req, res) => { + const { id } = req.params + + if (!mongoose.Types.ObjectId.isValid(id)) { + return res + .status(400) + .json({ success: false, message: "Invalid thought ID" }) + } + + try { + const thought = await Thought.findById(id) + if (!thought) { + return res + .status(404) + .json({ success: false, message: "Thought not found" }) + } + + thought.hearts += 1 + await thought.save() + + return res + .status(200) + .json({ success: true, hearts: thought.hearts, message: "Liked!" }) + } catch (err) { + console.error(err) + return res + .status(500) + .json({ success: false, message: err.message }) + } +}) + + +// Delete +router.delete("/thoughts/:id", authenticateUser, async (req, res) => { + const id = req.params.id; + try { + const thought = await Thought.findById(id) + + if (!thought) { + return res.status(404).json({ + success: false, + response: [], + message: "Thought not found" + }) + } + + 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: null, + message: error, + }) + } +}) + +export default router diff --git a/routes/userRoutes.js b/routes/userRoutes.js new file mode 100644 index 0000000..57959d9 --- /dev/null +++ b/routes/userRoutes.js @@ -0,0 +1,93 @@ +import express from "express" +import bcrypt from "bcrypt" +import crypto from "crypto" +import mongoose from "mongoose" + +const router = express.Router() + +// User schema +const UserSchema = new mongoose.Schema({ + 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) + + +// New User +router.post('/users/signup', async (req, res) => { + try { + const { email, password } = req.body + + const existingUser = await User.findOne({ + email: email.toLowerCase() + }) + + if (existingUser) { + return res.status(400).json({ + success: false, + message: "User with this email already exists" + }) + } + + const salt = bcrypt.genSaltSync() + const hashedPassword = bcrypt.hashSync(password, salt) + const user = new User({ email, password: hashedPassword }) + + await user.save() + + res.status(200).json({ + success: true, + message: "User created successfully", + response: { + email: user.email, + id: user._id, + accessToken: user.accessToken, + }, + }) + } catch (error) { + res.status(400).json({ + success: false, + message: 'Could not create user', + response: error, + }) + } +}) + +// Log In +router.post('/users/login', async (req, res) => { + try { + const { email, password } = req.body + const user = await User.findOne({ email: email.toLowerCase() }) + + if (user && bcrypt.compareSync(password, user.password)) { + res.json({ + success: true, + message: "Logged in successfully", + response: { + email: user.email, + id: user._id, + accessToken: user.accessToken + }, + }) + } else { + res.status(401).json({ + success: false, + message: "Wrong e-mail or password", + response: null, + }) + } + } catch (error) { + res.status(500).json({ + success: false, + message: "Something went wrong", + response: error + }) + } +}) + +export default router \ No newline at end of file diff --git a/server.js b/server.js index f47771b..b63de6f 100644 --- a/server.js +++ b/server.js @@ -1,21 +1,63 @@ import cors from "cors" import express from "express" +import mongoose from "mongoose" +import "dotenv/config" +import listEndpoints from "express-list-endpoints" +import thoughtData from "./data.json" with { type: "json" } +import thoughtRoutes from "./routes/thoughtRoutes.js" +import userRoutes from "./routes/userRoutes.js" + +const mongoUrl = process.env.MONGO_URL +mongoose.connect(mongoUrl) +mongoose.Promise = Promise + +if (process.env.RESET_DB === "true") { + const seedDatabase = async () => { + await Thought.deleteMany() + thoughtData.forEach((thought) => { + new Thought(thought).save() + }) + } + + console.log("seeding database") + await seedDatabase() +} -// 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() +app.use(cors({ origin: "*" })) + +// const allowedOrigins = [ +// "http://localhost:5173", +// ] + +// app.use( +// cors({ +// origin: (origin, callback) => { +// if (!origin || allowedOrigins.includes(origin)) { +// callback(null, true) +// } else { +// callback(new Error("Not allowed by CORS")) +// } +// }, +// credentials: true, +// }) +// ) -// Add middlewares to enable cors and json body parsing -app.use(cors()) app.use(express.json()) -// Start defining your routes here +// Shows all endpoints app.get("/", (req, res) => { - res.send("Hello Technigo!") + const endpoints = listEndpoints(app) + res.json({ + message: "Welcome to the happy thoughts API. Here is a list of all endpoints", + endpoints: endpoints, + }) }) +app.use("/", thoughtRoutes) +app.use("/", userRoutes) + +const port = process.env.PORT || 8080 // Start the server app.listen(port, () => { console.log(`Server running on http://localhost:${port}`)