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
15 changes: 4 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
# 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.

## Getting started

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

## 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.
Link to frontend:
https://leonhappythoughts.netlify.app/
Link to API:
https://happythoughtsapi.onrender.com
10 changes: 5 additions & 5 deletions data.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
[
{
{
"_id": "682bab8c12155b00101732ce",
"message": "Berlin baby",
"hearts": 37,
"createdAt": "2025-05-19T22:07:08.999Z",
"__v": 0
},
{
"_id": "682e53cc4fddf50010bbe739",
"_id": "682e53cc4fddf50010bbe739",
"message": "My family!",
"hearts": 0,
"createdAt": "2025-05-22T22:29:32.232Z",
Expand All @@ -25,7 +25,7 @@
"message": "Newly washed bedlinen, kids that sleeps through the night.. FINGERS CROSSED 🤞🏼\n",
"hearts": 6,
"createdAt": "2025-05-21T21:42:23.862Z",
"__v": 0
"__v": 0
},
{
"_id": "682e45804fddf50010bbe736",
Expand Down Expand Up @@ -53,7 +53,7 @@
"message": "A god joke: \nWhy did the scarecrow win an award?\nBecause he was outstanding in his field!",
"hearts": 12,
"createdAt": "2025-05-20T20:54:51.082Z",
"__v": 0
"__v": 0
},
{
"_id": "682cebbe17487d0010a298b5",
Expand All @@ -74,7 +74,7 @@
"message": "Summer is coming...",
"hearts": 2,
"createdAt": "2025-05-20T15:03:22.379Z",
"__v": 0
"__v": 0
},
{
"_id": "682c706c951f7a0017130024",
Expand Down
22 changes: 22 additions & 0 deletions middleware/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import jwt from "jsonwebtoken"
import { User } from "../models/User"

export const authenticateUser = async (req, res, next) => {
try {
const token = req.header("Authorization")?.replace("Bearer ", "")
if (!token) {
return res.status(401).json({ error: "Access denied. No token provided." })
}

const decoded = jwt.verify(token, process.env.JWT_SECRET)
const user = await User.findById(decoded.id)
if (!user) {
return res.status(401).json({ error: "Invalid token." })
}

req.user = user
next()
} catch (error) {
res.status(401).json({ error: "Invalid token." })
}
}
25 changes: 25 additions & 0 deletions models/Thought.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import mongoose from "mongoose"

const ThoughtSchema = new mongoose.Schema({
message: {
type: String,
required: [true, "Message is required"],
minlength: 5,
maxlength: 140
},
hearts: {
type: Number,
default: 0
},
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true
},
createdAt: {
type: Date,
default: () => new Date()
}
})

export const Thought = mongoose.model("Thought", ThoughtSchema)
32 changes: 32 additions & 0 deletions models/User.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import mongoose from "mongoose"
import bcrypt from "bcrypt"

const UserSchema = new mongoose.Schema({
username: {
type: String,
required: [true, "Username is required"],
unique: true,
minlength: 3,
maxlength: 20
},
email: {
type: String,
required: [true, "Email is required"],
unique: true,
match: [/^\S+@\S+\.\S+$/, "Please enter a valid email"]
},
password: {
type: String,
required: [true, "Password is required"],
minlength: 6
}
}, { timestamps: true })

// Hash password before saving
UserSchema.pre("save", async function () {
if (this.isModified("password")) {
this.password = await bcrypt.hash(this.password, 10)
}
})

export const User = mongoose.model("User", UserSchema)
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,14 @@
"@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.3.1",
"express": "^4.17.3",
"express-list-endpoints": "^7.1.1",
"jsonwebtoken": "^9.0.3",
"mongodb": "^7.0.0",
"mongoose": "^9.1.5",
"nodemon": "^3.0.1"
}
}
160 changes: 154 additions & 6 deletions server.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,167 @@
import cors from "cors"
import express from "express"
import mongoose from "mongoose"
import bcrypt from "bcrypt"
import jwt from "jsonwebtoken"
import { Thought } from "./models/Thought"
import { User } from "./models/User"
import { authenticateUser } from "./middleware/auth"
import listEndpoints from "express-list-endpoints"

import dotenv from "dotenv"
dotenv.config()

const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/happy-thoughts"
mongoose.connect(mongoUrl)
mongoose.Promise = Promise

// 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()

// Add middlewares to enable cors and json body parsing
app.use(cors())
app.use(express.json())

// Start defining your routes here
// root - shows all available endpoints
app.get("/", (req, res) => {
res.send("Hello Technigo!")
res.json(listEndpoints(app))
})

// ---- AUTH ROUTES ----

// register
app.post("/register", async (req, res) => {
try {
const { username, email, password } = req.body
const user = await User.create({ username, email, password })
const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, { expiresIn: "7d" })
res.status(201).json({
id: user._id,
username: user.username,
email: user.email,
token
})
} catch (error) {
if (error.code === 11000) {
res.status(400).json({ error: "Username or email already exists" })
} else {
res.status(400).json({ error: error.message })
}
}
})

// login
app.post("/login", async (req, res) => {
try {
const { email, password } = req.body
const user = await User.findOne({ email })
if (!user) {
return res.status(401).json({ error: "Invalid email or password" })
}
const isMatch = await bcrypt.compare(password, user.password)
if (!isMatch) {
return res.status(401).json({ error: "Invalid email or password" })
}
const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, { expiresIn: "7d" })
res.json({
id: user._id,
username: user.username,
email: user.email,
token
})
} catch (error) {
res.status(500).json({ error: "Something went wrong" })
}
})

// ---- THOUGHT ROUTES ----

// get all thoughts
app.get("/thoughts", async (req, res) => {
try {
const thoughts = await Thought.find().populate("user", "username").sort({ createdAt: -1 })
res.json(thoughts)
} catch (error) {
res.status(500).json({ error: "Could not fetch thoughts" })
}
})

// create a new thought (authenticated)
app.post("/thoughts", authenticateUser, async (req, res) => {
try {
const { message } = req.body
const thought = await Thought.create({ message, user: req.user._id })
const populatedThought = await thought.populate("user", "username")
res.status(201).json(populatedThought)
} catch (error) {
res.status(400).json({ error: error.message })
}
})

// get single thought
app.get("/thoughts/:id", async (req, res) => {
try {
const { id } = req.params
const thought = await Thought.findById(id).populate("user", "username")
if (!thought) {
return res.status(404).json({ error: "Thought not found" })
}
res.json(thought)
} catch (error) {
res.status(400).json({ error: "Invalid ID format" })
}
})

// update a thought (authenticated, only creator)
app.put("/thoughts/:id", authenticateUser, async (req, res) => {
try {
const { id } = req.params
const { message } = req.body
const thought = await Thought.findById(id)
if (!thought) {
return res.status(404).json({ error: "Thought not found" })
}
if (thought.user.toString() !== req.user._id.toString()) {
return res.status(403).json({ error: "You can only edit your own thoughts" })
}
thought.message = message
await thought.save()
const updatedThought = await thought.populate("user", "username")
res.json(updatedThought)
} catch (error) {
res.status(400).json({ error: error.message })
}
})

// delete a thought (authenticated, only creator)
app.delete("/thoughts/:id", authenticateUser, async (req, res) => {
try {
const { id } = req.params
const thought = await Thought.findById(id)
if (!thought) {
return res.status(404).json({ error: "Thought not found" })
}
if (thought.user.toString() !== req.user._id.toString()) {
return res.status(403).json({ error: "You can only delete your own thoughts" })
}
await thought.deleteOne()
res.status(204).send()
} catch (error) {
res.status(400).json({ error: "Invalid ID format" })
}
})

// like a thought
app.post("/thoughts/:id/like", async (req, res) => {
try {
const { id } = req.params
const thought = await Thought.findByIdAndUpdate(id, { $inc: { hearts: 1 } }, { new: true }).populate("user", "username")
if (!thought) {
return res.status(404).json({ error: "Thought not found" })
}
res.json(thought)
} catch (error) {
res.status(400).json({ error: "Invalid ID format" })
}
})

// Start the server
Expand Down