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
39 changes: 33 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,38 @@
# Project API
# Happy Thoughts 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.
A RESTful API for sharing and managing happy thoughts with user authentication.

## Getting started
## API Endpoints

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

## View it live
`GET /`
Welcome message for the API.

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.
### Thoughts

`GET /thoughts`
Retrieves all thoughts.

`POST /thoughts`
Creates a new thought.

`GET /thoughts/:id`
Retrieves a specific thought by ID.

`PUT /thoughts/:id`
Updates a specific thought.

`DELETE /thoughts/:id`
Deletes a specific thought.

`POST /thoughts/:id/like`
Adds a like (heart) to a specific thought.

### Users

`POST /users`
Registers a new user.

`POST /users/sessions`
Logs in an existing user.
48 changes: 48 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import cors from "cors";
import dotenv from "dotenv";
import express from "express";
import mongoose from "mongoose";

import userRoutes from "./routes/auth.js";
import thoughtRoutes from "./routes/thoughts.js";

dotenv.config();

const app = express();

const allowedOrigins = [
"http://localhost:5173",
"https://happythoughtproject.netlify.app",
];

// === Middleware ===
app.use(
cors({
origin: (origin, callback) => {
if (!origin) return callback(null, true);

if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error("Not allowed by CORS"));
}
},
credentials: true,
})
);
app.use(express.json());

// === Connect to MongoDB ===
const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/happyThoughts";
mongoose.connect(mongoUrl);
mongoose.Promise = Promise;

// === Routes ===
app.get("/", (req, res) => {
res.send("Welcome to the Happy Thoughts API!");
});

app.use("/thoughts", thoughtRoutes);
app.use("/users", userRoutes);

export default app;
21 changes: 21 additions & 0 deletions middlewares/authenticateUser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { User } from "../models/User.js";

const authenticateUser = async (req, res, next) => {
const authHeader = req.header("Authorization");

const accessToken = authHeader?.replace("Bearer ", "");

try {
const user = await User.findOne({ accessToken });
if (user) {
req.user = user;
next();
} else {
res.status(401).json({ message: "Access token is invalid" });
}
} catch (error) {
res.status(500).json({ message: "Server error during authentication" });
}
};

export default authenticateUser;
31 changes: 31 additions & 0 deletions models/Thought.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import mongoose from "mongoose";

const thoughtSchema = new mongoose.Schema(
{
message: {
type: String,
required: [true, "Message is required"],
minlength: [5, "Message must be at least 5 characters"],
maxlength: [140, "Message cannot exceed 140 characters"],
trim: true,
},
hearts: {
type: Number,
default: 0,
},
},
{
timestamps: true,
},
{
createdBy: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
Comment on lines +22 to +24

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great linking here to the other collection

},
}
);

const Thought = mongoose.model("Thought", thoughtSchema);

export default Thought;
29 changes: 29 additions & 0 deletions models/User.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import crypto from "crypto";
import mongoose from "mongoose";

const userSchema = new mongoose.Schema({
username: {
type: String,
unique: true,
required: true,
minlength: 3,
maxlength: 30,
},
password: {
type: String,
required: true,
minlength: 5,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a maxLength here as well?

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

export const User = mongoose.model("User", userSchema);
13 changes: 9 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@
"author": "",
"license": "ISC",
"dependencies": {
"@babel/core": "^7.17.9",
"@babel/node": "^7.16.8",
"@babel/preset-env": "^7.16.11",
"@babel/core": "^7.27.4",
"@babel/node": "^7.27.1",
"@babel/preset-env": "^7.27.2",
"bcrypt": "^6.0.0",
"bcrypt-nodejs": "^0.0.3",
"cors": "^2.8.5",
"dotenv": "^16.5.0",
"express": "^4.17.3",
"nodemon": "^3.0.1"
"express-list-endpoints": "^7.1.1",
"mongoose": "^8.15.1",
"nodemon": "^3.1.10"
}
}
59 changes: 59 additions & 0 deletions routes/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import bcrypt from "bcrypt";
import crypto from "crypto";
import express from "express";

import { User } from "../models/User.js";

const router = express.Router();

router.post("/", async (req, res) => {
console.log("Incoming body:", req.body);
const { username, password } = req.body;

try {
const salt = bcrypt.genSaltSync();
const hashedPassword = bcrypt.hashSync(password, salt);
const user = new User({
username,
password: hashedPassword,
accessToken: crypto.randomBytes(128).toString("hex"),
email: new Date().getTime(),
});

await user.save();

res.status(201).json({
userId: user._id,
username: user.username,
accessToken: user.accessToken,
});
} catch (err) {
console.error("Signup error:", err);
if (err.code === 11000) {
res.status(400).json({ message: "Username already exists" });
} else {
res.status(400).json({ message: "Invalid request", error: err });
}
}
});

router.post("/sessions", async (req, res) => {
const { username, password } = req.body;

try {
const user = await User.findOne({ username });
if (user && bcrypt.compareSync(password, user.password)) {
res.json({
username: user.username,
userId: user._id,
accessToken: user.accessToken,
});
} else {
res.status(401).json({ error: "Username or password is incorrect" });
}
} catch (err) {
res.status(500).json({ error: "Internal server error" });
}
});

export default router;
103 changes: 103 additions & 0 deletions routes/thoughts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import express from "express";

import authenticateUser from "../middlewares/authenticateUser.js";
import HappyThought from "../models/Thought.js";

const router = express.Router();

// get last 20 posts
router.get("/", async (req, res) => {
try {
const thoughts = await HappyThought.find()
.sort({ createdAt: -1 })
.limit(20);
res.status(200).json(thoughts);
} catch (error) {
res.status(500).json({ error: "Could not fetch thoughts" });
}
});

// get single post
router.get("/:id", async (req, res) => {
try {
const thought = await HappyThought.findById(req.params.id);
if (thought) {
res.json(thought);
} else {
res.status(404).json({ error: "Thought not found" });
}
} catch (error) {
res.status(400).json({ error: "Invalid ID format" });
}
});
Comment on lines +20 to +32
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this used in the frontend?? It's a neat solution, just asking what the use of getting a single thought is here!


// add new post
router.post("/", authenticateUser, async (req, res) => {
const { message } = req.body;

try {
const newThought = new HappyThought({
message,
createdBy: req.user._id, // lägg till användaren som skapar
});

const savedThought = await newThought.save();
res.status(201).json(savedThought);
} catch (error) {
res.status(400).json({ error: "Invalid input", details: error.errors });
}
});
Comment on lines +34 to +49
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly add input validation here as well?


// update existing post
router.put("/:id", async (req, res) => {
try {
const updatedThought = await HappyThought.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
);

if (updatedThought) {
res.status(200).json(updatedThought);
} else {
res.status(404).json({ error: "Thought not found" });
}
} catch (error) {
res.status(400).json({ error: "Invalid update", details: error.errors });
}
});

// delete post
router.delete("/:id", async (req, res) => {
try {
const deleted = await HappyThought.findByIdAndDelete(req.params.id);
if (deleted) {
res.status(200).json({ success: true });
} else {
res.status(404).json({ error: "Thought not found" });
}
} catch (error) {
res.status(400).json({ error: "Invalid ID format" });
}
});

// like post
router.post("/:id/like", async (req, res) => {
try {
const thought = await HappyThought.findByIdAndUpdate(
req.params.id,
{ $inc: { hearts: 1 } },
{ new: true }
);

if (thought) {
res.status(200).json(thought);
} else {
res.status(404).json({ error: "Thought not found" });
}
} catch (error) {
res.status(400).json({ error: "Invalid ID format" });
}
});

export default router;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For future developments - maybe consider adding some rate limits on creation of posts? That way, a user can't flood the feed. :)

28 changes: 10 additions & 18 deletions server.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,14 @@
import cors from "cors"
import express from "express"
import listEndpoints from "express-list-endpoints";

// 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()
import app from "./app.js";

// Add middlewares to enable cors and json body parsing
app.use(cors())
app.use(express.json())
const PORT = process.env.PORT || 8081;

// Start defining your routes here
app.get("/", (req, res) => {
res.send("Hello Technigo!")
})
app.listen(PORT, () => {
console.log(`🚀 Server running on http://localhost:${PORT}`);
console.log(`📝 Thoughts API: http://localhost:${PORT}/thoughts`);
console.log(`👤 Users API: http://localhost:${PORT}/users`);

// Start the server
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`)
})
console.log("\n📋 All API Endpoints:");
console.log(listEndpoints(app));
});