generated from Technigo/express-api-starter
-
Notifications
You must be signed in to change notification settings - Fork 30
happy tears and thoughts api project by Varia #27
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
Open
VariaSlu
wants to merge
18
commits into
Technigo:master
Choose a base branch
from
VariaSlu:master
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
d47cf15
server and thoughts.jsonendpoints
VariaSlu ae945c0
seetup mongodb
VariaSlu f2be46a
set up MongoDB server, created Thouhgtt model and GET/POST/thoughts r…
VariaSlu e58ff19
addd POST and thoughts rout
VariaSlu 5681c05
Add GET /thoughts/:id route and fix listEndpoints import
VariaSlu 703c228
Added input validation for thoughts: required and min lengt
VariaSlu 17e6890
user authentication with signup and login routes, password hashing, a…
VariaSlu e4d7314
nnow have X-User-Id header support for creating thoughs
VariaSlu 517689f
linked thoughts to useers
VariaSlu d775205
documentation at / using express-list-endpoints
VariaSlu 548f992
JWT auth and protect POST /thoughts; verified token flow
VariaSlu 750bb6b
Deploy-ready updated readmee
VariaSlu 62f3dec
readmeee update
VariaSlu 45c393c
readme mongo url added
VariaSlu 3c9e280
deleted mongo url from read me
VariaSlu 39bc956
readme updated
VariaSlu 5dcb978
clean
VariaSlu 02d6540
more clean
VariaSlu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import mongoose from 'mongoose'; | ||
|
||
export const Thought = mongoose.model('Thought', { | ||
message: { | ||
type: String, | ||
required: true, | ||
minlength: 5, | ||
maxlength: 140, | ||
}, | ||
hearts: { | ||
type: Number, | ||
default: 0 | ||
}, | ||
createdAt: { | ||
type: Date, | ||
default: () => new Date(), | ||
}, | ||
createdBy: { | ||
type: mongoose.Schema.Types.ObjectId, | ||
ref: 'User', | ||
required: true | ||
Comment on lines
+18
to
+21
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. ⭐ |
||
} | ||
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import mongoose from 'mongoose'; | ||
import bcrypt from 'bcryptjs'; | ||
|
||
|
||
const userSchema = new mongoose.Schema( | ||
{ | ||
email: { | ||
type: String, | ||
required: true, | ||
trim: true, | ||
lowercase: true, | ||
unique: true, | ||
match: [/^\S+@\S+\.\S+$/, 'Invalid email format'] | ||
}, | ||
password: { | ||
type: String, | ||
required: true, | ||
minlength: 6 | ||
}, | ||
|
||
}, | ||
{ timestamps: true } // <-- options go here, not inside the fields | ||
); | ||
|
||
// Hash password before saving | ||
userSchema.pre('save', async function (next) { | ||
if (!this.isModified('password')) return next(); // Skip if password not changed | ||
this.password = await bcrypt.hash(this.password, 10); // Salt rounds = 10 | ||
next(); | ||
}); | ||
|
||
// to compare passwords later | ||
userSchema.methods.comparePassword = function (candidatePassword) { | ||
return bcrypt.compare(candidatePassword, this.password); | ||
}; | ||
|
||
|
||
export const User = mongoose.model('User', userSchema); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +1,255 @@ | ||
import cors from "cors" | ||
import express from "express" | ||
import mongoose from 'mongoose'; | ||
import dotenv from 'dotenv'; | ||
import { Thought } from "./models/Thought.js" | ||
import { User } from './models/User.js'; | ||
import listEndpoints from 'express-list-endpoints'; | ||
import jwt from 'jsonwebtoken'; | ||
|
||
|
||
dotenv.config(); | ||
|
||
// 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 | ||
|
||
// middlewares to enable cors and json body parsing | ||
app.use(cors()) | ||
app.use(express.json()) | ||
|
||
// Start defining your routes here | ||
app.get("/", (req, res) => { | ||
res.send("Hello Technigo!") | ||
}) | ||
// Connect to MongoDB | ||
mongoose | ||
.connect(process.env.MONGO_URL) | ||
.then(() => { | ||
console.log('Connected to MongoDB!'); | ||
//seed(); // optional | ||
}) | ||
.catch((err) => { | ||
console.error('Mongo error', err); | ||
}); | ||
|
||
// seed one thought if DB is empty (optional) | ||
const seed = async () => { | ||
const existing = await Thought.find(); | ||
if (existing.length === 0) { | ||
await new Thought({ | ||
message: 'This is my very first seeded thought!', | ||
hearts: 0 | ||
}).save(); | ||
console.log('Seeded one thought'); | ||
} | ||
}; | ||
|
||
const auth = (req, res, next) => { | ||
const raw = req.header('Authorization') || ''; | ||
const token = raw.startsWith('Bearer ') ? raw.slice(7) : null; | ||
|
||
console.log('[AUTH] raw header =', JSON.stringify(raw)); | ||
console.log('[AUTH] token len =', token ? token.length : 0); | ||
console.log('[AUTH] token parts =', token ? token.split('.').length : 0); | ||
|
||
if (!token) return res.status(401).json({ error: 'Missing token' }); | ||
|
||
try { | ||
const payload = jwt.verify(token, process.env.JWT_SECRET); | ||
req.userId = payload.userId; | ||
return next(); | ||
} catch (err) { | ||
console.error('JWT verify error:', err.message); | ||
return res.status(401).json({ error: 'Invalid or expired token', details: err.message }); | ||
} | ||
}; | ||
|
||
|
||
//basee route | ||
app.get('/', (req, res) => { | ||
try { | ||
const endpoints = listEndpoints(app).map(e => ({ | ||
path: e.path, | ||
methods: e.methods.sort(), | ||
})); | ||
res.json({ | ||
name: 'Happy Thoughts API', | ||
version: '1.0.0', | ||
endpoints | ||
}); | ||
} catch (err) { | ||
res.status(500).json({ error: 'Failed to list endpoints', details: err.message }); | ||
} | ||
}); | ||
|
||
// Get all thoughts | ||
app.get('/thoughts', async (req, res) => { | ||
try { | ||
const thoughts = await Thought.find() | ||
.sort({ createdAt: -1 }) | ||
.limit(20) | ||
.populate('createdBy', 'email'); | ||
|
||
res.json(thoughts); | ||
} catch (err) { | ||
res.status(500).json({ error: 'Could not fetch thoughts' }); | ||
} | ||
}); | ||
|
||
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: 'Thought not found' }); | ||
} | ||
|
||
res.json(thought); | ||
} catch (err) { | ||
res.status(400).json({ | ||
error: 'Invalid ID', | ||
details: err.message, | ||
}); | ||
} | ||
}); | ||
|
||
|
||
app.post('/thoughts/:id/like', async (req, res) => { | ||
const { id } = req.params; | ||
|
||
try { | ||
|
||
const updatedThought = await Thought.findByIdAndUpdate( | ||
id, | ||
{ $inc: { hearts: 1 } }, | ||
{ new: true } //return the updaated document | ||
); | ||
|
||
if (!updatedThought) { | ||
return res.status(404).json({ error: 'Thought not found' }); | ||
} | ||
|
||
res.json(updatedThought); | ||
|
||
} catch (err) { | ||
res.status(400).json({ | ||
error: 'Invalid ID or request', | ||
details: err.message //show whats wrong | ||
}); | ||
} | ||
|
||
}); | ||
|
||
app.post('/thoughts', auth, async (req, res) => { | ||
const { message } = req.body; | ||
|
||
try { | ||
const newThought = await new Thought({ | ||
message, | ||
createdBy: req.userId | ||
}).save(); | ||
|
||
res.status(201).json(newThought); | ||
} catch (err) { | ||
if (err.name === 'ValidationError') { | ||
return res.status(400).json({ error: 'Validation failed', details: err.message }); | ||
} | ||
res.status(500).json({ error: 'Could not save thought', details: err.message }); | ||
} | ||
}); | ||
|
||
app.patch('/thoughts/:id', auth, async (req, res) => { | ||
const { message } = req.body; | ||
if (typeof message !== 'string' || message.length < 5 || message.length > 140) { | ||
return res.status(400).json({ error: 'Message must be 5–140 chars' }); | ||
} | ||
|
||
try { | ||
const t = await Thought.findById(req.params.id); | ||
if (!t) return res.status(404).json({ error: 'Thought not found' }); | ||
if (String(t.createdBy) !== req.userId) { | ||
return res.status(403).json({ error: 'Not your thought' }); | ||
} | ||
|
||
t.message = message; | ||
await t.save(); | ||
res.json(t); | ||
} catch (err) { | ||
res.status(400).json({ error: 'Invalid ID', details: err.message }); | ||
} | ||
}); | ||
|
||
app.delete('/thoughts/:id', auth, async (req, res) => { | ||
try { | ||
const t = await Thought.findById(req.params.id); | ||
if (!t) return res.status(404).json({ error: 'Thought not found' }); | ||
if (String(t.createdBy) !== req.userId) { | ||
return res.status(403).json({ error: 'Not your thought' }); | ||
} | ||
|
||
await t.deleteOne(); | ||
res.json({ success: true, message: 'Thought deleted' }); | ||
} catch (err) { | ||
res.status(400).json({ error: 'Invalid ID', details: err.message }); | ||
} | ||
}); | ||
|
||
app.post('/signup', async (req, res) => { | ||
const { email, password } = req.body; | ||
|
||
try { | ||
const newUser = await new User({ email, password }).save(); | ||
res.status(201).json({ | ||
email: newUser.email, | ||
_id: newUser._id | ||
}); | ||
} catch (err) { | ||
res.status(400).json({ error: 'Could not create user', details: err.message }); | ||
} | ||
}); | ||
|
||
// LOGIN ROUTE | ||
app.post('/login', async (req, res) => { | ||
const { email, password } = req.body; | ||
|
||
try { | ||
// 1. Check if user exists | ||
const user = await User.findOne({ email }); | ||
if (!user) { | ||
return res.status(401).json({ error: 'Invalid email or password' }); | ||
} | ||
|
||
// 2. Compare passwords | ||
const isMatch = await user.comparePassword(password); | ||
if (!isMatch) { | ||
return res.status(401).json({ error: 'Invalid email or password' }); | ||
} | ||
|
||
// after password check passes | ||
const accessToken = jwt.sign( | ||
{ userId: user._id }, | ||
process.env.JWT_SECRET, | ||
{ expiresIn: '7d' } | ||
); | ||
|
||
res.json({ | ||
success: true, | ||
message: 'Login successful', | ||
userId: user._id, | ||
email: user.email, | ||
accessToken | ||
}); | ||
|
||
|
||
} catch (err) { | ||
res.status(500).json({ error: 'Server error', details: err.message }); | ||
} | ||
}); | ||
|
||
|
||
// Start the server | ||
// Start server | ||
app.listen(port, () => { | ||
console.log(`Server running on http://localhost:${port}`) | ||
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
[ | ||
{ | ||
"id": 1, | ||
"message": "I'm happy because I'm learning backend!", | ||
"hearts": 5, | ||
"createdAt": "2025-06-05T09:00:00.000Z" | ||
}, | ||
{ | ||
"id": 2, | ||
"message": "Coffee makes everything better.", | ||
"hearts": 2, | ||
"createdAt": "2025-06-05T10:00:00.000Z" | ||
} | ||
] |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
great to add documentation in the readme too!