-
Notifications
You must be signed in to change notification settings - Fork 31
PR - Happy Thouhts API #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
abcfe2c
c644ae6
bc41ee7
f5b2b81
c35eb62
8329761
57d7e31
cd58301
58ca881
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| "postman.settings.dotenv-detection-notification-visibility": false | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,60 @@ | ||
| # 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. | ||
| Backend API for sharing happy thoughts. Users can post thoughts, like others' thoughts, and manage their own content. | ||
|
|
||
| ## Getting started | ||
| ## Live Demo | ||
|
|
||
| Install dependencies with `npm install`, then start the server by running `npm run dev` | ||
| **API:** [https://js-project-api-cathi.onrender.com](https://js-project-api-cathi.onrender.com) | ||
|
|
||
| ## View it live | ||
| ## Tech Stack | ||
|
|
||
| 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. | ||
| - Node.js & Express | ||
| - MongoDB with Mongoose | ||
| - JWT authentication | ||
| - bcrypt for passwords | ||
|
|
||
| ## API Endpoints | ||
|
|
||
| ### Authentication | ||
| - `POST /auth/register` - Register user | ||
| - `POST /auth/login` - Login user | ||
|
|
||
| ### Thoughts | ||
| - `GET /thoughts` - Get all thoughts | ||
| - `POST /thoughts` - Create thought | ||
| - `PATCH /thoughts/:id` - Update thought (own only) | ||
| - `DELETE /thoughts/:id` - Delete thought (own only) | ||
| - `PATCH /thoughts/:id/like` - Like thought | ||
| - `PATCH /thoughts/:id/unlike` - Unlike thought | ||
|
|
||
| ## Installation | ||
|
|
||
| 1. Clone repo and install dependencies: | ||
| ```bash | ||
| npm install | ||
| ``` | ||
|
|
||
| 2. Create `.env` file: | ||
| ``` | ||
| MONGO_URL=mongodb://localhost/happy-thoughts | ||
| JWT_SECRET=your-secret-key | ||
| PORT=8080 | ||
| ``` | ||
|
|
||
| 3. Start development server: | ||
| ```bash | ||
| npm run dev | ||
| ``` | ||
|
|
||
| ## Features | ||
|
|
||
| - ✅ Anonymous and authenticated posting | ||
| - ✅ User authentication with JWT | ||
| - ✅ CRUD operations for thoughts | ||
| - ✅ Like/unlike functionality | ||
| - ✅ Input validation and error handling | ||
| - ✅ Pagination and filtering | ||
|
|
||
| ## Deployment | ||
|
|
||
| Deployed on Render with MongoDB Atlas database. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| import { validationResult } from "express-validator"; | ||
| import bcrypt from "bcrypt"; | ||
| import jwt from "jsonwebtoken"; | ||
| import { User } from "../models/User.js"; | ||
|
|
||
| const JWT_SECRET = process.env.JWT_SECRET || "supersecret"; | ||
|
|
||
| // User registration | ||
| export const register = async (req, res) => { | ||
| const errors = validationResult(req); | ||
| if (!errors.isEmpty()) { | ||
| return res.status(400).json({ errors: errors.array() }); | ||
| } | ||
|
|
||
| const { email, password } = req.body; | ||
| try { | ||
| // Check if user already exists | ||
| const existingUser = await User.findOne({ email }); | ||
| if (existingUser) { | ||
| return res | ||
| .status(409) | ||
| .json({ message: "That email address already exists" }); | ||
| } | ||
|
|
||
| // Hash password | ||
| const hashedPassword = await bcrypt.hash(password, 10); | ||
|
|
||
| // Create and save user | ||
| const user = await User.create({ email, password: hashedPassword }); | ||
|
|
||
| // Create JWT token | ||
| const token = jwt.sign( | ||
| { userId: user._id, email: user.email }, | ||
| JWT_SECRET, | ||
| { expiresIn: "2h" } | ||
| ); | ||
|
|
||
| res.status(201).json({ token, user: { id: user._id, email: user.email } }); | ||
| } catch (error) { | ||
| res.status(500).json({ error: error.message }); | ||
| } | ||
| }; | ||
|
|
||
| // User login | ||
| export const login = async (req, res) => { | ||
| const errors = validationResult(req); | ||
| if (!errors.isEmpty()) { | ||
| return res.status(400).json({ errors: errors.array() }); | ||
| } | ||
|
|
||
| const { email, password } = req.body; | ||
| try { | ||
| const user = await User.findOne({ email }); | ||
| if (!user) { | ||
| return res.status(401).json({ message: "Invalid credentials" }); | ||
| } | ||
|
|
||
| const match = await bcrypt.compare(password, user.password); | ||
| if (!match) { | ||
| return res.status(401).json({ message: "Invalid credentials" }); | ||
| } | ||
|
|
||
| const token = jwt.sign( | ||
| { userId: user._id, email: user.email }, | ||
| JWT_SECRET, | ||
| { expiresIn: "2h" } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No problem to have 2 hour duration but next step would be refreshed token. |
||
| ); | ||
|
|
||
| res.status(200).json({ token, user: { id: user._id, email: user.email } }); | ||
| } catch (error) { | ||
| res.status(500).json({ error: error.message }); | ||
| } | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| import { Thought } from "../models/Thought.js"; | ||
| import { validationResult } from "express-validator"; | ||
|
|
||
| // GET /thoughts | ||
| export const getThoughts = async (req, res) => { | ||
| const { message, minHearts, sort, page = 1, limit = 10 } = req.query; | ||
| try { | ||
| const query = {}; | ||
| if (message) query.message = { $regex: new RegExp(message, "i") }; | ||
| if (minHearts) query.hearts = { $gte: Number(minHearts) }; | ||
|
|
||
| // inkludera user‐fältet så frontend ser ägaren | ||
| let thoughtsQuery = Thought.find(query).select( | ||
| "message hearts createdAt user" | ||
| ); | ||
| if (sort === "most-liked") | ||
| thoughtsQuery = thoughtsQuery.sort({ hearts: -1 }); | ||
| else if (sort === "least-liked") | ||
| thoughtsQuery = thoughtsQuery.sort({ hearts: 1 }); | ||
|
|
||
| const total = await Thought.countDocuments(query); | ||
| const results = await thoughtsQuery | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do coercion on both number and page instead of just number. "const { message, minHearts, sort, page = 1, limit = 10 } = req.query; const pageNum = Number(page); .skip((pageNum - 1) * limitNum) |
||
| .skip((page - 1) * limit) | ||
| .limit(Number(limit)); | ||
|
|
||
| res.json({ page: Number(page), limit: Number(limit), total, results }); | ||
| } catch (error) { | ||
| res.status(500).json({ error: "Server error", details: error }); | ||
| } | ||
| }; | ||
|
|
||
| // GET /thoughts/:id | ||
| export const getThoughtById = async (req, res) => { | ||
| try { | ||
| const thought = await Thought.findById(req.params.id).select( | ||
| "message hearts createdAt user" | ||
| ); | ||
| if (!thought) return res.status(404).json({ error: "Thought not found" }); | ||
| res.json(thought); | ||
| } catch (error) { | ||
| res.status(400).json({ error: "Invalid ID", details: error }); | ||
| } | ||
| }; | ||
|
|
||
| // POST /thoughts | ||
| export const createThought = async (req, res) => { | ||
| const errors = validationResult(req); | ||
| if (!errors.isEmpty()) | ||
| return res.status(400).json({ errors: errors.array() }); | ||
|
|
||
| try { | ||
| const { message } = req.body; | ||
| const newThought = new Thought({ | ||
| message, | ||
| // If user is logged in (req.user exists), save their ID, otherwise null for anonymous | ||
| user: req.user?.id || null, | ||
| }); | ||
| await newThought.save(); | ||
| res.status(201).json(newThought); | ||
| } catch (error) { | ||
| res.status(400).json({ error: "Could not create thought", details: error }); | ||
| } | ||
| }; | ||
|
|
||
| // PATCH /thoughts/:id | ||
| export const updateThought = async (req, res) => { | ||
| const errors = validationResult(req); | ||
| if (!errors.isEmpty()) | ||
| return res.status(400).json({ errors: errors.array() }); | ||
|
|
||
| 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" }); | ||
|
|
||
| // ägarskapskontroll | ||
| if (!thought.user.equals(req.user.id)) { | ||
| return res | ||
| .status(403) | ||
| .json({ error: "You can only update your own thoughts" }); | ||
| } | ||
|
|
||
| thought.message = message; | ||
| await thought.save(); | ||
| res.json(thought); | ||
| } catch (error) { | ||
| res.status(400).json({ error: "Invalid update", details: error }); | ||
| } | ||
| }; | ||
|
|
||
| // DELETE /thoughts/:id | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The user can not delete thoughts if they're not logged in - potential new feature? Connecting it to the session ID or similar? |
||
| export const deleteThought = async (req, res) => { | ||
| try { | ||
| const thought = await Thought.findById(req.params.id); | ||
| if (!thought) return res.status(404).json({ error: "Thought not found" }); | ||
|
|
||
| // ägarskapskontroll | ||
| if (!thought.user.equals(req.user.id)) { | ||
| return res | ||
| .status(403) | ||
| .json({ error: "You can only delete your own thoughts" }); | ||
| } | ||
|
|
||
| await thought.deleteOne(); | ||
| res.json({ message: "Thought deleted successfully" }); | ||
| } catch (error) { | ||
| res.status(400).json({ error: "Invalid ID", details: error }); | ||
| } | ||
| }; | ||
|
|
||
| // PATCH /thoughts/:id/like | ||
| export const likeThought = async (req, res) => { | ||
| try { | ||
| const updated = await Thought.findByIdAndUpdate( | ||
| req.params.id, | ||
| { $inc: { hearts: 1 } }, | ||
| { new: true } | ||
| ); | ||
| if (!updated) return res.status(404).json({ error: "Thought not found" }); | ||
| res.json(updated); | ||
| } catch (error) { | ||
| res.status(400).json({ error: "Failed to like thought", details: error }); | ||
| } | ||
| }; | ||
|
|
||
| // PATCH /thoughts/:id/unlike | ||
| export const unlikeThought = async (req, res) => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good safety code that thoughts cant go negative. Keeping things positive or neutral at least. Update: While logged in I can spam my own like-button and it does to negative -1 and -2 I managed to get. |
||
| try { | ||
| const thought = await Thought.findById(req.params.id); | ||
| if (!thought) return res.status(404).json({ error: "Thought not found" }); | ||
| thought.hearts = Math.max(thought.hearts - 1, 0); | ||
| await thought.save(); | ||
| res.json(thought); | ||
| } catch (error) { | ||
| res.status(400).json({ error: "Failed to unlike thought", details: error }); | ||
| } | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hardcoded fallback secret can be removed for security reasons.