diff --git a/06-jobs-api/starter/app.js b/06-jobs-api/starter/app.js index 4d6385152d..57823292b3 100644 --- a/06-jobs-api/starter/app.js +++ b/06-jobs-api/starter/app.js @@ -1,27 +1,55 @@ require('dotenv').config(); require('express-async-errors'); +const helmet = require('helmet'); +const cors = require('cors'); +const xss = require('xss-clean'); +const rateLimiter = require('express-rate-limit'); + const express = require('express'); const app = express(); +const connectDB = require('./db/connect'); +const authenticateUser = require('./middleware/authentication'); + +// routers +const authRouter = require('./routes/auth'); +const jobsRouter = require('./routes/jobs'); -// error handler +// error handlings const notFoundMiddleware = require('./middleware/not-found'); const errorHandlerMiddleware = require('./middleware/error-handler'); + +app.set("trust proxy", 1); +app.use( + rateLimiter({ + // 15 minutes + windowMs: 15 * 60 * 1000, + // limit each IP to 100 requests per windowMs + max: 100, + }) +); app.use(express.json()); -// extra packages +app.use(helmet()); +app.use(cors()); +app.use(xss()); -// routes -app.get('/', (req, res) => { - res.send('jobs api'); +app.get("/", (req, res) => { + res.send('

Jobs API

Documentation'); }); +// routes +app.use('/api/v1/auth', authRouter); +app.use('/api/v1/jobs', authenticateUser, jobsRouter); + app.use(notFoundMiddleware); app.use(errorHandlerMiddleware); +// port const port = process.env.PORT || 3000; const start = async () => { try { + await connectDB(process.env.MONGO_URI); app.listen(port, () => console.log(`Server is listening on port ${port}...`) ); diff --git a/06-jobs-api/starter/controllers/auth.js b/06-jobs-api/starter/controllers/auth.js index e69de29bb2..aa91fb4c52 100644 --- a/06-jobs-api/starter/controllers/auth.js +++ b/06-jobs-api/starter/controllers/auth.js @@ -0,0 +1,38 @@ +const { BadRequestError, UnauthenticatedError } = require('../errors') +const User = require('../models/User') +const { StatusCodes } = require('http-status-codes') + + +const register = async (req, res) => { + const user = await User.create({ ...req.body }) + const token = user.createJWT() + res.status(StatusCodes.CREATED).json({ user: { name: user.name }, token }) +} + +const login = async (req, res) => { + const { email, password } = req.body + + if (!email || !password) { + throw new BadRequestError('Please provide email and password') + } + + const user = await User.findOne({ email }) + + if (!user) { + throw new UnauthenticatedError('Invalid Credentials') + } + + const isPasswordCorrect = await user.comparePassword(password) + + if (!isPasswordCorrect) { + throw new UnauthenticatedError('Invalid Credentials') + } + // Comparing passwords + const token = user.createJWT() + res.status(StatusCodes.OK).json({ user: { name: user.name }, token }) +} + +module.exports = { + register, + login, +} diff --git a/06-jobs-api/starter/controllers/jobs.js b/06-jobs-api/starter/controllers/jobs.js index e69de29bb2..cb84d37ff9 100644 --- a/06-jobs-api/starter/controllers/jobs.js +++ b/06-jobs-api/starter/controllers/jobs.js @@ -0,0 +1,32 @@ +const Job = require('../models/Job') +const { StatusCodes } = require('http-status-codes') +const { BadRequestError, NotFoundError } = require('../errors') + +const getAllJobs = async (req, res) => { + res.send('Get All Jobs') +} +const getJob = async (req, res) => { + res.send('Get Single Job') +} + +const createJob = async (req, res) => { + req.body.createdBy = req.user.userId + const job = await Job.create(req.body) + res.status(StatusCodes.CREATED).json({job}) +} + +const updateJob = async (req, res) => { + res.send('Update Job') +} + +const deleteJob = async (req, res) => { + res.send('Delete Job') +} + +module.exports = { + createJob, + deleteJob, + getAllJobs, + updateJob, + getJob, +} diff --git a/06-jobs-api/starter/middleware/authentication.js b/06-jobs-api/starter/middleware/authentication.js index e69de29bb2..7175e7710c 100644 --- a/06-jobs-api/starter/middleware/authentication.js +++ b/06-jobs-api/starter/middleware/authentication.js @@ -0,0 +1,23 @@ +const User = require('../models/User') +const jwt = require('jsonwebtoken') +const { UnauthenticatedError } = require('../errors') + +const auth = async (req, res, next) => { + // check header + const authHeader = req.headers.authorization + if (!authHeader || !authHeader.startsWith('Bearer')) { + throw new UnauthenticatedError('Authentication invalid') + } + const token = authHeader.split(' ')[1] + + try { + const payload = jwt.verify(token, process.env.JWT_SECRET) + // attach the user to the job routes + req.user = { userId: payload.userId, name: payload.name } + next() + } catch (error) { + throw new UnauthenticatedError('Authentication invalid') + } +} + +module.exports = auth diff --git a/06-jobs-api/starter/models/Job.js b/06-jobs-api/starter/models/Job.js index e69de29bb2..e110582ce5 100644 --- a/06-jobs-api/starter/models/Job.js +++ b/06-jobs-api/starter/models/Job.js @@ -0,0 +1,27 @@ +const mongoose = require('mongoose') +const JobSchema = new mongoose.Schema( + { + company: { + type: String, + required: [true, 'Please provide company name'], + maxlength: 50, + }, + position: { + type: String, + required: [true, 'Please provide position'], + maxlength: 100, + }, + status: { + type: String, + enum: ['interview', 'declined', 'pending'], + default: 'pending', + }, + createdBy: { + type: mongoose.Types.ObjectId, + ref: 'User', + required: [true, 'Please provide user'], + }, + }, + { timestamps: true } +) +module.exports = mongoose.model('Job', JobSchema) diff --git a/06-jobs-api/starter/models/User.js b/06-jobs-api/starter/models/User.js index e69de29bb2..cda283a95d 100644 --- a/06-jobs-api/starter/models/User.js +++ b/06-jobs-api/starter/models/User.js @@ -0,0 +1,46 @@ +const mongoose = require('mongoose') +const bcrypt = require('bcryptjs') +const jwt = require('jsonwebtoken') + +const UserSchema = new mongoose.Schema({ + name: { + type: String, + required: [true, 'Please provide name'], + maxlength: 50, + minlength: 3, + }, + email: { + type: String, + required: [true, 'Please provide email'], + match: [ + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, + 'Please provide a valid email', + ], + unique: true, + }, + password: { + type: String, + required: [true, 'Please provide password'], + minlength: 6, + }, +}) + +UserSchema.pre('save', async function () { + const salt = await bcrypt.genSalt(10) + this.password = await bcrypt.hash(this.password, salt) +}) + +UserSchema.methods.createJWT = function () { + return jwt.sign( + { userId: this._id, name: this.name }, + process.env.JWT_SECRET, + { + expiresIn: process.env.JWT_LIFETIME, + } + ) +} +UserSchema.methods.comparePassword = async function (canditatePassword) { + const isMatch = await bcrypt.compare(canditatePassword, this.password) + return isMatch +} +module.exports = mongoose.model('User', UserSchema) diff --git a/06-jobs-api/starter/routes/auth.js b/06-jobs-api/starter/routes/auth.js index e69de29bb2..0df3daff74 100644 --- a/06-jobs-api/starter/routes/auth.js +++ b/06-jobs-api/starter/routes/auth.js @@ -0,0 +1,8 @@ +const express = require('express') +const router = express.Router() +const { register, login } = require('../controllers/auth') +router.post('/register', register) +router.post('/login', login) + + +module.exports = router diff --git a/06-jobs-api/starter/routes/jobs.js b/06-jobs-api/starter/routes/jobs.js index e69de29bb2..7ee1b0a5fb 100644 --- a/06-jobs-api/starter/routes/jobs.js +++ b/06-jobs-api/starter/routes/jobs.js @@ -0,0 +1,14 @@ +const express = require('express') +const router = express.Router() +const { + createJob, + deleteJob, + getAllJobs, + updateJob, + getJob, +} = require('../controllers/jobs') + +router.route('/').post(createJob).get(getAllJobs) +router.route('/:id').get(getJob).delete(deleteJob).patch(updateJob) + +module.exports = router