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
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,19 @@ Install dependencies with `npm install`, then start the server by running `npm r

## 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.
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. https://happyhappyhappyhappy.netlify.app/

## Endpoints
GET / -> API docs
GET /thoughts -> list latest 20
GET /thoughts/:id -> single thought
POST /thoughts/:id/like -> like
POST /signup -> create user
POST /login -> login (returns accessToken)
POST /thoughts -> create (JWT required)
PATCH /thoughts/:id -> update (JWT + author-only)
DELETE /thoughts/:id -> delete (JWT + author-only)

## Deploy
Comment on lines +13 to +24

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!

Deployed on Render:
https://js-project-api-862g.onrender.com/
23 changes: 23 additions & 0 deletions models/Thought.js
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

Choose a reason for hiding this comment

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

}
})
38 changes: 38 additions & 0 deletions models/User.js
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);
21 changes: 19 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"name": "project-api",
"version": "1.0.0",
"type": "module",
"description": "Project API",
"scripts": {
"start": "babel-node server.js",
Expand All @@ -12,8 +13,24 @@
"@babel/core": "^7.17.9",
"@babel/node": "^7.16.8",
"@babel/preset-env": "^7.16.11",
"bcryptjs": "^3.0.2",
"cors": "^2.8.5",
"express": "^4.17.3",
"dotenv": "^16.5.0",
"express": "^4.21.2",
"express-list-endpoints": "^7.1.1",
"jsonwebtoken": "^9.0.2",
"mongodb": "^6.18.0",
"mongoose": "^8.16.0",
"nodemon": "^3.0.1"
}
},
"main": "server.js",
"repository": {
"type": "git",
"url": "git+https://github.com/VariaSlu/js-project-api.git"
},
"keywords": [],
"bugs": {
"url": "https://github.com/VariaSlu/js-project-api/issues"
},
"homepage": "https://github.com/VariaSlu/js-project-api#readme"
}
245 changes: 239 additions & 6 deletions server.js
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}`)
})
14 changes: 14 additions & 0 deletions thoughts.json
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"
}
]