diff --git a/README.md b/README.md index 0f9f073..86b25e6 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,6 @@ 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. +https://happy-thoughts-happy-mind.netlify.app/ +https://js-project-api-afon.onrender.com/ + diff --git a/data.json b/data.json index a2c844f..4d5c0d7 100644 --- a/data.json +++ b/data.json @@ -3,7 +3,7 @@ "_id": "682bab8c12155b00101732ce", "message": "Berlin baby", "hearts": 37, - "createdAt": "2025-05-19T22:07:08.999Z", + "createdAt": "2025-05-31T02:07:08.999Z", "__v": 0 }, { @@ -112,10 +112,10 @@ "__v": 0 }, { - "_id": "682bab8c12155b00101732ce", + "_id": "682bab8c12155b00101732cc", "message": "Berlin baby", "hearts": 37, - "createdAt": "2025-05-19T22:07:08.999Z", + "createdAt": "2025-05-30T02:07:08.999Z", "__v": 0 } ] \ No newline at end of file diff --git a/middleware/authMiddleware.js b/middleware/authMiddleware.js new file mode 100644 index 0000000..dc9f017 --- /dev/null +++ b/middleware/authMiddleware.js @@ -0,0 +1,11 @@ +import { User } from "../models/user" + +export 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({loggedOut: true}) + } +} \ No newline at end of file diff --git a/models/thought.js b/models/thought.js new file mode 100644 index 0000000..56548a6 --- /dev/null +++ b/models/thought.js @@ -0,0 +1,25 @@ +import mongoose from "mongoose"; + +const thoughtSchema = new mongoose.Schema({ + message: { + type: String, + required: true, + minlength: 5, + maxlength: 140 + }, + hearts: { + type: Number, + default: 0 + }, + createdAt: { + type: Date, + default: Date.now + }, + user: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true + } +}) + +export const Thought = mongoose.model("Thought", thoughtSchema) \ No newline at end of file diff --git a/models/user.js b/models/user.js new file mode 100644 index 0000000..07d4d94 --- /dev/null +++ b/models/user.js @@ -0,0 +1,30 @@ +//import bcrypt from "bcrypt" +import crypto from "crypto" +import mongoose from "mongoose"; + +const userSchema = new mongoose.Schema({ + email: { + type: String, + required: true, + unique: true, + minlength: 3, + maxlength: 100 + }, + password: { + type: String, + required: true, + minlength: 3, + maxlength: 100 + }, + accessToken: { + type: String, + default: () => crypto.randomBytes(128).toString("hex") + }, + createdAt: { + type: Date, + default: Date.now + } +}) + +export const User = mongoose.model("User", userSchema) + diff --git a/notes.md b/notes.md new file mode 100644 index 0000000..3339ee5 --- /dev/null +++ b/notes.md @@ -0,0 +1,67 @@ +W1: +[x] Your API should have at least three (for now) routes. Try to push yourself to do more, though! + [x] The endpoint "/" should return documentation of your API using e.g. Express List Endpoints + [x] A minimum of one endpoint to return a collection of results (an array of elements). + [x] A minimum of one endpoint to return a single result (single element). +[x] Your API should be RESTful +[x] You should follow the guidelines on how to write clean code. + +W2: +[X] Your API should use Mongoose models to model your data and use these models to fetch data from the database. +[x] Your API should validate user input and return appropriate errors if the input is invalid. +[x] You should implement error handling for all your routes, with proper response statuses. +[x] Your frontend should be updated with the possibility to Update and Delete a thought. + [x] Update + [x] Delete +[x] Deployed backend API with deployed DB +[x] Posibility to: + [x] POST + [x] PATCH + [x] DELETE +[x] Frontend posibility to POST (to your API) + +W3: +[x] Signup and Login existing Happy Thoughts +[x] Your API should have routes to register and log in +[x] Your endpoints to Create, Update and Delete should be authenticated +[x] Your frontend should have a registration form which POSTs to the API to create a new user. +[x] Your frontend should have a login form to authenticate the user. +[x] Your passwords in the database should be encrypted with bcrypt. +[x] You should implement error handling. Your API should let the user know if something went wrong. Be as specific as possible to help the user, e.g. by validating the user input when creating a new user, and return "That email address already exists". Include correct status codes. Error messages should also be shown on the frontend. +[x] The validation should ensure unique email addresses and/or usernames, depending on how you'd like to structure your User model. + + +Requirements: +[x] Your API must have at least the following routes. Try to push yourself to do more, though! Endpoints for: + [x] Documentation of your API using e.g. Express List Endpoints + [x] Reading thoughts + [x] Reading a single thought + [x] Liking a thought + [x] Creating a thought (authenticated) + [x] Updating a thought (authenticated) + [x] Deleting a thought (authenticated) + [x] Signing up + [x] Logging in +[x] Your API should be RESTful +[x] You should follow the guidelines on how to write clean code +[x] Your API should use Mongoose models to model your data and use these models to fetch data from the database. +[x] Your API should validate user input and return appropriate errors if the input is invalid. +[x] The validation should ensure unique email addresses and/or usernames depending on how you'd like to structure your User model. +[x] You should implement error handling for all your routes, with proper response statuses. +[x] Your frontend should be updated with the possibility to Update and Delete a thought, as well as signing up and logging in and some error handling if something goes wrong. +[x] Your passwords in the database should be encrypted with bcrypt. +[x] Your API should be deployed to Render or similar. +[x] Everything in your backend should be reflected in your frontend. + +Stretch goal options: +[] Allow anonymous (not-logged-in) users to post thoughts +[] As a logged-in user, you should be able to see a list of the thoughts that you've liked. +[] Give the thoughts a category or tags. So you could organise them. For example, 'Food thoughts', 'Project thoughts', 'Home thoughts', etc. +[] Add filtering and sorting options to the endpoint which returns all thoughts. Examples: + [] Sorting on date or number of likes + [] Filtering to only see thoughts with more than x hearts, thoughts newer than x date or thoughts in a specific category (if you implemented categories) +[] Implement pagination in your backend & frontend so you can click through pages of thoughts. The frontend could request a specific page and show only that page. The backend would take the request for that page and return only the thoughts for that page. +[] You could also experiment with implementing infinite scrolling on the frontend rather than having a list of page numbers. This idea is similar to paging and involves frontend & backend changes. +[] When registering, display error messages from the API in the frontend, next to the field which has the error. For example, if the email address is invalid, show an error message next to the email input. +[] In the frontend, store the token in localStorage and send it in headers to persist the logged-in state for the user. +[] Validate the email address format using a regular expression. \ No newline at end of file diff --git a/package.json b/package.json index bf25bb6..4811395 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", + "bcrypt": "^6.0.0", "cors": "^2.8.5", + "dotenv": "^16.5.0", "express": "^4.17.3", + "express-list-endpoints": "^7.1.1", + "mongoose": "^8.15.1", "nodemon": "^3.0.1" } } diff --git a/server.js b/server.js index f47771b..bbaab65 100644 --- a/server.js +++ b/server.js @@ -1,5 +1,22 @@ +import bcrypt from "bcrypt" import cors from "cors" -import express from "express" +import dotenv from "dotenv" +import express, { response } from "express" +import listEndpoints from "express-list-endpoints" +import mongoose from "mongoose" + +import data from "./data.json" +import { authenticateUser } from "./middleware/authMiddleware" +import { Thought } from "./models/thought" +import { User } from "./models/user" +import { postUser } from "./utils/postUser" + +dotenv.config() + +//To connect to the DB +const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/happy-thoughts" +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: @@ -11,9 +28,218 @@ const app = express() app.use(cors()) app.use(express.json()) +//GET // Start defining your routes here +// Endpoints with listEndpoints app.get("/", (req, res) => { - res.send("Hello Technigo!") + const endpoints = listEndpoints(app) + res.json({ + message: "Welcomen to the Happy Thoughts API", + endpoints: endpoints + }) + +}) + +//Endpoint to show all thoughts +//Filter liked thoughts, thoughts?liked, +//Filter thoughts from today thoughts?thoughtsfromtoday +app.get("/thoughts", async(req, res) => { + + const today = new Date() + const todayDate = new Date(today.getFullYear(), today.getMonth(), today.getDate()) + + const { liked, thoughtsfromtoday } = req.query + + const query = {} + + if (liked !== undefined){ + query.hearts = { $gt: 0 } //greater than 0 + } + + if (thoughtsfromtoday !== undefined){ + const tomorrowDate = new Date(todayDate); + tomorrowDate.setDate(todayDate.getDate() + 1); + + query.createdAt = { + $gte: todayDate, //greater than or equal to todayDate + $lt: tomorrowDate, //less than tomorrowDate + }; + } + + try{ + const filteredThoughts = await Thought.find(query) + .sort({ createdAt: "desc" }) + .populate("user", "_id") + + if (filteredThoughts.length === 0){ + return res.status(404).json({ error: "There are no thoughts to show" }) + } + res.status(200).json(filteredThoughts) + + } catch (error) { + res.status(500).json({ error: "Failed to fetch thoughts"}) + } +}); + +//Endpoint to show a single thought id +app.get("/thoughts/:id", async (req, res) => { + const { id } = req.params + try{ + const thought = await Thought.findById(id) + + if (!thought){ + return res.status(404).json({ error: "There is no thought with that id" }) + } + res.status(200).json({ response: thought}) + + } catch (error) { + res.status(500).json({ error: "Failed to fetch thoughts"}) + } + +}) + +//POST +app.post("/thoughts", authenticateUser, async(req, res) => { + const { message } = req.body + + if (!message) { + return res.status(400).json({ error: "Message is required" }); + } + + if(!req.user) { + return res.status(403).json({ error: "You must be logged in to post" }) + } + + try { + const newThought = await new Thought({ + message, + user: req.user._id + }).save() + + res.status(201).json({ response: newThought }) + + } catch (error) { + res.status(500).json({ error: "Thought could not be created"}) + } +}) + +app.post("/thoughts/:id/like", async (req, res) => { + const { id } = req.params; + + try { + const newThoughtLike = await Thought.findByIdAndUpdate( + id, + { $inc: { hearts: 1 } }, + { new: true } // return the updated document + ); + + if (!newThoughtLike) { + return res.status(404).json({ error: "Thought not found, could not update" }); + } + res.status(200).json({ + message: `Thought with message: ${newThoughtLike.message}, was liked.`, + hearts: newThoughtLike.hearts, + }); + + } catch (error) { + res.status(500).json({ error: "Failed to fetch thoughts" }); + } +}); + + +//DELETE +app.delete("/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 id was not found, could not deleted" }) + } + + if(!thought.user.equals(req.user._id)){ + return res.status(403).json({ error: "Not authorized to delete this thought" }) + } + + await thought.deleteOne() + res.status(200).json({message: `Thought with message: ${thought.message}, was deleted`}) + } catch (error) { + res.status(500).json({ error: "Failed to fetch thoughts"}) + } +}) + +//PATCH +//Endpoint /thoughts/:id, json body {"newThoughtMessage": "edited message"} +app.patch("/thoughts/:id", authenticateUser, async(req, res) => { + const { id } = req.params + const { newThoughtMessage } = req.body + + try{ + + const thought = await Thought.findById(id); + + if(!thought){ + return res.status(404).json({ error: "Thought id was not found, could not update" }) + } + + if(!thought.user.equals(req.user._id)) { + return res.status(403).json({ error: "Not authorized to edit this thought" }) + } + + thought.message = newThoughtMessage; + await thought.save(); + + res.status(200).json({ message: `Thought was updated to: ${newThoughtMessage}`}) + + } catch (error) { + res.status(500).json({ error: "Failed to fetch thoughts"}) + } +}) + +app.get("/users", async (req, res) => { + const { email } = req.params + + try { + const user = await User.find(email) + + if (!user) { + return res.status(404).json({ + success: false, + response: null, + message: "No users found" + }) + } + + res.status(200).json({ + success: true, + response: user + }) + } catch (error) { + res.status(500).json({ + success: false, + response: error, + message: "Failed to fetch users" + }) + } +}) + +app.post("/users", postUser) + +app.get("/secrets", authenticateUser, (req, res) => { + res.status(200).json({ message: "This is a protected route", user: req.user }) +}) + +//Signin endpoint +app.post("/sessions", async (req, res) => { + const user = await User.findOne({email: req.body.email}) + + if(user && bcrypt.compareSync(req.body.password, user.password)){ + res.status(200).json({success: true, userId: user._id, accessToken: user.accessToken}) + } else { + res.status(401).json({ success: false, error: "Invalid email or password"}) + } }) // Start the server diff --git a/utils/postUser.js b/utils/postUser.js new file mode 100644 index 0000000..c26c56a --- /dev/null +++ b/utils/postUser.js @@ -0,0 +1,43 @@ +import bcrypt from "bcrypt" + +import { User } from "../models/user"; + +//app.get("/users", async (req, res) => +export const postUser = async (req, res) => { + try{ + const { email, password } = req.body + + // Check if user already exists + const existingUser = await User.findOne({ email }); + if (existingUser) { + return res.status(400).json({ + success: false, + message: "User already exists" + }); + } + + if (password.length < 3) { + return res.status(400).json({ + success: false, + message: "Password must be at least 3 characters long" + }); + } + + const salt = bcrypt.genSaltSync() + const user = new User({email, password: bcrypt.hashSync(password, salt)}) + await user.save() + res.status(200).json({ + success: true, + response: email, + userId:user._id, + accessToken: user.accessToken + + }) + } catch(error){ + res.status(400).json({ + message: "Could not create user", + errors: error.errors + }) + } +} +