Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 37 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,43 @@
# Project 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.
Render: https://js-project-api-e8xy.onrender.com
Netlify: https://project-happy-thoughts-ml.netlify.app/

## Getting started
Welcome to my first backend project! A RESTful API for sharing and liking thoughts with user authentication.

Install dependencies with `npm install`, then start the server by running `npm run dev`
## Features

## View it live
- User Authentication** - Sign up and log in with email/password
- Create Thoughts** - Share your thoughts (5-140 characters)
- Like Thoughts** - Increase heart count on any thought
- Update Thoughts** - Edit your own thoughts
- Delete Thoughts** - Remove your own thoughts
- Password Encryption** - Bcrypt for secure password storage

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.
## Tech Stack

Backend:
- Node.js
- Express.js
Database:
- MongoDB with Mongoose
Authentication:
- access tokens
Security:
- Bcrypt password hashing
- CORS

## API Endpoints
Authentication endpoints:
- POST /signup- Create new account
- POST /login - Log in to existing account

Thoughts endpoints:
- GET /thoughts - Get all thoughts
- GET /thoughts/:id - Get single thought
- POST /thoughts - Create thought (authenticated)
- PATCH /thoughts/:id - Update thought (authenticated)
- DELETE /thoughts/:id - Delete thought (authenticated)
- POST /thoughts/:id/like - Like a thought

# ENJOY #
15 changes: 15 additions & 0 deletions middleware/authMiddleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { User } from "../schema/User.js";

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
});
}
};
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@
"@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": "^17.2.3",
"express": "^4.17.3",
"express-list-endpoints": "^7.1.1",
"mongoose": "^9.1.5",
"node.js": "^0.0.1-security",
"nodemon": "^3.0.1"
}
}
119 changes: 119 additions & 0 deletions routes/thoughtRoutes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import express from "express";
import { Thought } from "../schema/Thoughts.js";
import { authenticateUser } from "../middleware/authMiddleware.js";

export const router = express.Router();

// Endpoint for all the thoughts.
router.get("/thoughts", async (req, res) => {
try {
const thoughts = await Thought.find()
res.json(thoughts);
} catch (error) {
res.status(500).json({ error: "Could not fetch thoughts" })
}
});

// Endpoint for the thoughts id, to get one specific thought.
router.get("/thoughts/:id", async (req, res) => {
try {
const thoughtsId = await Thought.findById(req.params.id)

if (!thoughtsId) {
return res.status(404).json({ error: `Thought with id ${req.params.id} does not exist` })
}
res.json(thoughtsId)

} catch (error) {
return res.status(500).json({ error: `Could not fetch thoughts` })
}
});

// Adding a new message to the database
router.post("/thoughts", authenticateUser, async (req, res) => {
try {
const { message } = req.body

if (!message || message.trim().length === 0) {
return res.status(400).json({ error: `Message is required` })
}

const newThought = await Thought.create({ message })

res.status(201).json(newThought)
} catch (error) {
res.status(500).json({ error: `Could not create thought` })
}
});

// Endpoint for liking a thought, increases hearts by 1
router.post("/thoughts/:id/like", async (req, res) => {
try {
const id = req.params.id;
const thought = await Thought.findById(id);

if (!thought) {
return res.status(404).json({ error: "Thought not found" });
}
thought.hearts += 1;
await thought.save();
res.json(thought);

} catch (error) {
res.status(500).json({ error: "Could not like thought" });
}
});


// Updates a thought - can update message and/or hearts
router.patch("/thoughts/:id", authenticateUser, async (req, res) => {
try {
const id = req.params.id
const { message, hearts } = req.body

const update = {}

if (message !== undefined) {
if (message.trim().length === 0) {
return res.status(400).json({ error: "Message can not be empty" })
}
update.message = message
}

if (hearts !== undefined) {
if (isNaN(hearts)) {
return res.status(400).json({ error: "Hearts must be a number" })
}
update.hearts = hearts
}

const updatedThought = await Thought.findByIdAndUpdate(
id,
update,
{ new: true }
)

if (!updatedThought) {
return res.status(404).json({ error: "Thought not found" })
}

res.json(updatedThought)
} catch (error) {
res.status(500).json({ error: "Could not update thought" })
}
});

// Deletes a thought
router.delete("/thoughts/:id", authenticateUser, async (req, res) => {
try {
const id = req.params.id
const deletedThought = await Thought.findByIdAndDelete(id)

if (!deletedThought) {
return res.status(404).json({ error: `Thought with id ${id} does not exist` })
}
res.json(deletedThought)
} catch (error) {
res.status(500).json({ error: "Could not delete thought " })
}
});
93 changes: 93 additions & 0 deletions routes/userRoutes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import express from "express";
import bcrypt from "bcrypt";
import { User } from "../schema/User.js";
import { authenticateUser } from "../middleware/authMiddleware.js";

export const router = express.Router();

// Creates a new user. Sign-up
router.post("/signup", async (req, res) => {
try {
const { email, password } = req.body;

if (!email || !password) {
return res.status(400).json({
success: false,
message: "Email and password are required"
});
}

const existingUser = await User.findOne({ email: email.toLowerCase() })

if (existingUser) {
return res.status(400).json({
success: false,
message: "Invalid email or password",
});
}

const salt = bcrypt.genSaltSync(10) // 10 making it harder to hack the password.
const hashedPassword = bcrypt.hashSync(password, salt)
const user = new User({ email, password: hashedPassword });

await user.save();
res.status(201).json({
success: true,
message: "User created",
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 endpoint. Finds user that has created an account.
router.post("/login", async (req, res) => {
try {
const { email, password } = req.body;

if (!email || !password) {
return res.status(400).json({
success: false,
message: "Email and password are required",
});
}

const user = await User.findOne({ email: email.toLowerCase() });

if (!user || !bcrypt.compareSync(password, user.password)) {
return res.status(401).json({
success: false,
message: "Invalid email or password",
})
}
res.json({
success: true,
message: "Login successful",
response: {
email: user.email,
id: user._id,
accessToken: user.accessToken
}
});
} catch (error) {
res.status(500).json({
success: false,
message: "Something went wrong",
});
}
});

// ======= Protected Routes - not in use =======
router.get("/secrets", authenticateUser, (req, res) => {
res.json({ secret: "This is a super secret message." })
});
18 changes: 18 additions & 0 deletions schema/Thoughts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Schema, model } from "mongoose";

const thoughtSchema = new Schema({
message: {
type: String,
required: true
},
hearts: {
type: Number,
default: 0,
},
createdAt: {
type: Date,
default: () => new Date()
}
})

export const Thought = model("thought", thoughtSchema);
20 changes: 20 additions & 0 deletions schema/User.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Schema, model } from "mongoose";
import crypto from "crypto";

const userSchema = new Schema({
email: {
type: String,
unique: true,
required: true,
},
password: {
type: String,
required: true
},
accessToken: {
type: String,
default: () => crypto.randomBytes(128).toString("hex")
}
});

export const User = model("User", userSchema);
Loading