From 7adc1baa9a497c1bd196fd7fb36767389963b540 Mon Sep 17 00:00:00 2001 From: Erota Date: Sun, 16 Oct 2022 15:21:15 +0100 Subject: [PATCH] finished test --- controllers/orderControllers.js | 133 ++++++++++++++++++++++++++ controllers/userControllers.js | 93 ++++++++++++++++++ db/database.js | 20 ++++ db/users.json | 0 index.js | 107 ++++++--------------- orderModel.js => models/orderModel.js | 18 ++-- models/userModel.js | 49 ++++++++++ routes/authRouter.js | 44 +++++++++ routes/orderRoutes.js | 16 ++++ routes/userRoutes.js | 17 ++++ test/order.test.js | 69 +++++++++++++ utilities/authenticate.js | 24 +++++ utilities/passport-jwt.js | 79 +++++++++++++++ views/index.html | 16 ++++ views/login.html | 23 +++++ views/signup.html | 31 ++++++ 16 files changed, 654 insertions(+), 85 deletions(-) create mode 100644 controllers/orderControllers.js create mode 100644 controllers/userControllers.js create mode 100644 db/database.js create mode 100644 db/users.json rename orderModel.js => models/orderModel.js (54%) create mode 100644 models/userModel.js create mode 100644 routes/authRouter.js create mode 100644 routes/orderRoutes.js create mode 100644 routes/userRoutes.js create mode 100644 test/order.test.js create mode 100644 utilities/authenticate.js create mode 100644 utilities/passport-jwt.js create mode 100644 views/index.html create mode 100644 views/login.html create mode 100644 views/signup.html diff --git a/controllers/orderControllers.js b/controllers/orderControllers.js new file mode 100644 index 00000000..826195db --- /dev/null +++ b/controllers/orderControllers.js @@ -0,0 +1,133 @@ +const moment = require('moment'); +const orderModel = require('../models/orderModel'); +const { + authenticateOrder, + authenticateUser, +} = require('../utilities/authenticate'); + +exports.getAllOrders = async (req, res) => { + try { + const queryObj = { ...req.query }; + const excludedFields = ['page', 'sort']; + excludedFields.forEach((el) => delete queryObj[el]); + + let query = orderModel.find(queryObj); + + if (req.query.sort) { + const sortBy = req.query.sort.split(',').join(' '); + query = query.sort(sortBy); + } else { + query = query.sort('total_price created_at'); + } + + if (req.query.state) { + query = query.find({ state: req.query.state }); + } else { + query = query.find({ state: 1 }); + } + + if (req.query.page) { + const page = req.query.page * 1 || 1; + const limit = req.query.limit * 1 || 100; + const skip = (page - 1) * limit; + query = query.skip(skip).limit(limit); + } + + const orders = await query; + + const orderLength = orders.reduce((acc, cur) => (acc += 1), 0); + + return res.json({ status: 'success', orderLength, orders }); + } catch (err) { + res.status(404).json({ + status: 'fail', + message: err, + }); + } +}; + +exports.getOrder = async (req, res) => { + try { + await authenticateOrder(req, res, 'user'); + const { id } = req.params; + const order = await orderModel.findById(id); + + if (!order) { + return res.status(404).json({ status: false, order: null }); + } + + return res.json({ status: true, order }); + } catch (err) { + res.status(404).json({ + status: 'fail', + message: err, + }); + } +}; + +exports.createOrder = async (req, res) => { + try { + await authenticateUser(req, res); + const body = req.body; + const total_price = body.items.reduce((prev, curr) => { + prev += curr.price * curr.quantity; + return prev; + }, 0); + + const order = await orderModel.create({ + items: body.items, + created_at: moment().toDate(), + total_price, + }); + + return res.json({ status: true, order }); + } catch (err) { + res.status(404).json({ + status: 'fail', + message: err, + }); + } +}; + +exports.updateOrder = async (req, res) => { + try { + await authenticateOrder(req, res, 'admin'); + const { id } = req.params; + const { state } = req.body; + + const order = await orderModel.findById(id); + + if (!order) { + return res.status(404).json({ status: false, order: null }); + } + + if (state < order.state) { + return res + .status(422) + .json({ status: false, order: null, message: 'Invalid operation' }); + } + + order.state = state; + + await order.save(); + + return res.json({ status: true, order }); + } catch (err) {} +}; + +exports.deleteOrder = async (req, res) => { + try { + await authenticateUser(req, res); + await authenticateOrder(req, res, 'admin'); + const { id } = req.params; + + const order = await orderModel.deleteOne({ _id: id }); + + return res.json({ status: true, message: 'order deleted', order }); + } catch (err) { + res.status(404).json({ + status: 'fail', + message: err, + }); + } +}; \ No newline at end of file diff --git a/controllers/userControllers.js b/controllers/userControllers.js new file mode 100644 index 00000000..6362e8cf --- /dev/null +++ b/controllers/userControllers.js @@ -0,0 +1,93 @@ +const User = require('../models/userModel'); +const { authenticateUser } = require('../utilities/authenticate'); + +exports.getAllUsers = async (req, res) => { + try { + await authenticateUser(req, res); + const users = await User.find(); + res.status(200).json({ + status: 'success', + data: { + users, + }, + }); + } catch (err) { + res.status(404).json({ + status: 'fail', + message: err, + }); + } +}; + +exports.getUser = async (req, res) => { + try { + const { id } = req.params; + const user = await User.findById(id); + res.status(200).json({ + status: 'success', + data: { + user, + }, + }); + } catch (err) { + res.status(404).json({ + status: 'fail', + message: err, + }); + } +}; + +exports.createUser = async (req, res) => { + try { + const newUser = await User.create({ + username: req.body.username, + email: req.body.email, + password: req.body.password, + }); + res.status(200).json({ + status: 'success', + data: { + newUser, + }, + }); + } catch (err) { + res.status(404).json({ + status: 'fail', + message: err, + }); + } +}; + +exports.updateUser = async (req, res) => { + try { + await authenticateUser(req, res); + const updatedUser = await User.findByIdAndUpdate(req.params.id, req.body, { + new: true, + runValidators: true, + }); + res.status(200).json({ + status: 'success', + updatedUser, + }); + } catch (err) { + res.status(404).json({ + status: 'fail', + message: err, + }); + } +}; + +exports.deleteUser = async (req, res) => { + try { + await Tour.findByIdAndDelete(req.params.id); + res.status(204).json({ + status: 'success', + data: null, + }); + } catch (err) { + res.status(404).json({ + status: 'fail', + message: err, + }); + } +}; diff --git a/db/database.js b/db/database.js new file mode 100644 index 00000000..7cc7c608 --- /dev/null +++ b/db/database.js @@ -0,0 +1,20 @@ +const moogoose = require('mongoose'); +require('dotenv').config(); + +const MONGODB_URI = process.env.MONGODB_URI; + +// connect to mongodb +function connection() { + moogoose.connect(MONGODB_URI); + + moogoose.connection.on('connected', () => { + console.log('Connected to MongoDB successfully'); + }); + + moogoose.connection.on('error', (err) => { + console.log('Error connecting to MongoDB', err); + }) +} + + +module.exports = connection; \ No newline at end of file diff --git a/db/users.json b/db/users.json new file mode 100644 index 00000000..e69de29b diff --git a/index.js b/index.js index fd771db0..7141bbc4 100644 --- a/index.js +++ b/index.js @@ -1,95 +1,50 @@ const express = require('express'); -const moment = require('moment'); -const mongoose = require('mongoose'); -const orderModel = require('./orderModel'); +const passport = require('passport'); +const orderRouter = require('./routes/orderRoutes'); +const authRouter = require('./routes/authRouter'); +const userRouter = require('./routes/userRoutes'); -const PORT = 3334 +require('./db/database')(); +require('dotenv').config(); -const app = express() +require('./utilities/passport-jwt'); -app.use(express.json()); - - -app.get('/', (req, res) => { - return res.json({ status: true }) -}) - - -app.post('/order', async (req, res) => { - const body = req.body; - - const total_price = body.items.reduce((prev, curr) => { - prev += curr.price - return prev - }, 0); - - const order = await orderModel.create({ - items: body.items, - created_at: moment().toDate(), - total_price - }) - - return res.json({ status: true, order }) -}) - -app.get('/order/:orderId', async (req, res) => { - const { orderId } = req.params; - const order = await orderModel.findById(orderId) - - if (!order) { - return res.status(404).json({ status: false, order: null }) - } - - return res.json({ status: true, order }) -}) +const PORT = process.env.PORT || 3000; -app.get('/orders', async (req, res) => { - const orders = await orderModel.find() - return res.json({ status: true, orders }) -}) -app.patch('/order/:id', async (req, res) => { - const { id } = req.params; - const { state } = req.body; +const app = express(); - const order = await orderModel.findById(id) +app.set('view-engine', 'ejs'); - if (!order) { - return res.status(404).json({ status: false, order: null }) - } - - if (state < order.state) { - return res.status(422).json({ status: false, order: null, message: 'Invalid operation' }) - } - - order.state = state; - - await order.save() - - return res.json({ status: true, order }) -}) - -app.delete('/order/:id', async (req, res) => { - const { id } = req.params; +app.use(express.json()); +app.use(express.urlencoded({ extended: false })); +app.use(passport.initialize()); - const order = await orderModel.deleteOne({ _id: id}) - return res.json({ status: true, order }) -}) +app.use('/', authRouter); +app.use( + '/orders', + passport.authenticate('jwt', { session: false }), + orderRouter +); +app.use('/users', userRouter); -mongoose.connect('mongodb://localhost:27017') +app.get('/', (req, res) => { + res.render('index.ejs'); +}); -mongoose.connection.on("connected", () => { - console.log("Connected to MongoDB Successfully"); +app.get('/login', (req, res) => { + res.render('Login.ejs'); }); -mongoose.connection.on("error", (err) => { - console.log("An error occurred while connecting to MongoDB"); - console.log(err); +app.get('/signup', (req, res) => { + res.render('Signup.ejs'); }); app.listen(PORT, () => { - console.log('Listening on port, ', PORT) -}) \ No newline at end of file + console.log('Listening on port, ', PORT); +}); + +module.exports = app; \ No newline at end of file diff --git a/orderModel.js b/models/orderModel.js similarity index 54% rename from orderModel.js rename to models/orderModel.js index 22bdd86d..7055436b 100644 --- a/orderModel.js +++ b/models/orderModel.js @@ -8,14 +8,14 @@ const OrderSchema = new Schema({ created_at: Date, state: { type: Number, default: 1 }, total_price: Number, - items: [{ - name: String, - price: Number, - size: { type: String, enum: ['m', 's', 'l']}, - quantity: Number, - }] + items: [ + { + name: String, + price: Number, + size: { type: String, enum: ['m', 's', 'l'] }, + quantity: Number, + }, + ], }); -const Order = mongoose.model('Order', OrderSchema); - -module.exports = Order; +const Order = mongoose.model('Order', OrderSchema); \ No newline at end of file diff --git a/models/userModel.js b/models/userModel.js new file mode 100644 index 00000000..3bf4819c --- /dev/null +++ b/models/userModel.js @@ -0,0 +1,49 @@ +const { default: mongoose } = require('mongoose'); +const bcrypt = require('bcrypt'); + +const userSchema = new mongoose.Schema({ + username: { + type: String, + required: [true, 'A user must have a username'], + trim: true, + unique: true, + }, + password: { + type: String, + required: [true, 'A user must have a password'], + minlength: 5, + maxlength: 16, + select: false, + }, + user_type: { + type: String, + enum: ['admin', 'user'], + default: 'user', + }, + email: { + type: String, + trim: true, + lowercase: true, + unique: true, + required: 'Email address is required', + }, +}); + +userSchema.pre('save', async function (next) { + if (!this.isModified('password')) return next(); + + this.password = await bcrypt.hash(this.password, 12); + + next(); +}); + +userSchema.methods.correctPassword = async function ( + candidatePassword, + userPassword +) { + return await bcrypt.compare(candidatePassword, userPassword); +}; + +const User = mongoose.model('User', userSchema); + +module.exports = User; \ No newline at end of file diff --git a/routes/authRouter.js b/routes/authRouter.js new file mode 100644 index 00000000..952e2e5e --- /dev/null +++ b/routes/authRouter.js @@ -0,0 +1,44 @@ +const passport = require('passport'); +const express = require('express'); +const jwt = require('jsonwebtoken'); + +const router = express.Router(); +require('dotenv').config(); + +router.post('/login', async (req, res, next) => { + passport.authenticate('login', async (err, user, info) => { + try { + if (err) { + return next(err); + } + + if (!user) return next(new Error('Username or Password is Incorrect')); + + req.login(user, { session: false }, async (err) => { + if (err) return next(err); + + const body = { _id: user._id, email: user.email }; + + const token = jwt.sign({ user: body }, process.env.JWT_SECRET, { + expiresIn: process.env.JWT_EXPIRES_IN, + }); + + return res.status(201).json({ + token, + }); + }); + } catch (err) { + return next(err); + } + })(req, res, next); +}); + +router.post( + '/signup', + passport.authenticate('signup', { session: false }), + (req, res) => { + res.redirect('/login'); + } +); + +module.exports = router; \ No newline at end of file diff --git a/routes/orderRoutes.js b/routes/orderRoutes.js new file mode 100644 index 00000000..cb10bc1c --- /dev/null +++ b/routes/orderRoutes.js @@ -0,0 +1,16 @@ +const express = require('express'); +const router = express.Router(); +const orderController = require('../controllers/orderController'); + +router + .route('/') + .get(orderController.getAllOrders) + .post(orderController.createOrder); + +router + .route('/:id') + .get(orderController.getOrder) + .patch(orderController.updateOrder) + .delete(orderController.deleteOrder); + +module.exports = router; \ No newline at end of file diff --git a/routes/userRoutes.js b/routes/userRoutes.js new file mode 100644 index 00000000..8f335614 --- /dev/null +++ b/routes/userRoutes.js @@ -0,0 +1,17 @@ +const express = require('express'); +const userController = require('../controllers/userController'); + +const router = express.Router(); + +router + .route('/') + .get(userController.getAllUsers) + .post(userController.createUser); + +router + .route('/:id') + .get(userController.getUser) + .patch(userController.updateUser) + .delete(userController.deleteUser); + +module.exports = router; \ No newline at end of file diff --git a/test/order.test.js b/test/order.test.js new file mode 100644 index 00000000..48ecb6cd --- /dev/null +++ b/test/order.test.js @@ -0,0 +1,69 @@ +const supertest = require('supertest'); +const app = require('../../index'); + +describe('Order Route', () => { + it('should get all /orders', async () => { + const response = await supertest(app).get('/orders'); + expect(response.status).toBe(200); + expect(response.body.orders.length).toBe(6); + }); + + it('should get an /order', async () => { + const response = await supertest(app).get( + '/orders/6336e9f3746eb01167d5d8eb' + ); + expect(response.status).toBe(200); + expect(response.body.order.items[0].name).toBe('McPlaren'); + expect(response.body.order.items.length).toBe(3); + }); + + it('should create an /order', async () => { + const order = { + username: 'Patrick Schaum', + password: 'cing67', + items: [ + { + name: 'LG Air conditioner', + price: 650, + size: 'l', + quantity: 2, + }, + ], + }; + const response = await supertest(app).post('/orders').send(order); + expect(response.status).toBe(200); + expect(response.body.order.items[0].name).toBe('LG Air conditioner'); + expect(response.body.order.items[0].quantity).toBe(2); + }); + + it('should update an /order', async () => { + const updatedOrder = { + state: 35, + }; + const response = await supertest(app) + .patch('/orders/632ef2f6eba99e5bc5e60a62') + .send(updatedOrder); + expect(response.status).toBe(200); + expect(response.body.order.state).toBe(35); + }); + + it('should delete an order', async () => { + const user = { + username: 'Zeuhz Droid', + password: 'gridz09', + }; + const response = await supertest(app) + .delete('/orders/6336f4bce897a8fa1988706b') + .send(user); + expect(response.status).toBe(200); + expect(response.body.message).toBe('order deleted'); + + const response2 = await supertest(app).get('/orders'); + expect(response2.status).toBe(200); + expect(response2.body.orders.length).toBe(6); + }); +}); + +afterAll(() => { + console.log('it worked'); +}); \ No newline at end of file diff --git a/utilities/authenticate.js b/utilities/authenticate.js new file mode 100644 index 00000000..1c295149 --- /dev/null +++ b/utilities/authenticate.js @@ -0,0 +1,24 @@ +const orderModel = require('../models/orderModel'); +const userModel = require('../models/userModel'); + +const authenticateUser = (req, _) => + new Promise(async (resolve, reject) => { + const { username, password } = req.body; + if (!username || !password) reject('Invalid username or password.'); + const [user] = await userModel.find({ username: username }); + if (!user) reject('User Not Found'); + if (password !== user.password) reject('Invalid username or password'); + else resolve('Done'); + }); + +const authenticateOrder = (req, res, type) => + new Promise(async (resolve, reject) => { + const { id } = req.params; + const order = await orderModel.findById(id); + if (!order) reject('Invalid ID'); + if (type !== 'user' && type !== 'admin') + reject("can't perform this operation"); + else resolve('Access granted'); + }); + +module.exports = { authenticateUser, authenticateOrder }; \ No newline at end of file diff --git a/utilities/passport-jwt.js b/utilities/passport-jwt.js new file mode 100644 index 00000000..e5a31a74 --- /dev/null +++ b/utilities/passport-jwt.js @@ -0,0 +1,79 @@ +const { authenticate } = require('passport'); +const passport = require('passport'); +const localStrategy = require('passport-local').Strategy; +const userModel = require('../models/userModel'); +const userController = require('../controllers/userController'); +const JWTStrategy = require('passport-jwt').Strategy; +const ExtractJWT = require('passport-jwt').ExtractJwt; +require('dotenv').config(); + +passport.use( + new JWTStrategy( + { + secretOrKey: process.env.JWT_SECRET, + jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken('token'), + }, + async (token, done) => { + try { + return done(null, token.user); + } catch (err) { + done(err); + } + } + ) +); + +passport.use( + 'signup', + new localStrategy( + { + usernameField: 'email', + passwordField: 'password', + passReqToCallback: true, + }, + async (req, email, password, done) => { + try { + const user = await userModel.create({ + username: req.body.username, + email, + password, + }); + + return done(null, user); + } catch (error) { + return done(error); + } + } + ) +); + +const authenticateUser = async (email, password, done) => { + try { + const user = await userModel.findOne({ email }).select('+password'); + + if (!user) { + return done(null, false, { message: 'User Not Found' }); + } + + const validate = await user.correctPassword(password, user.password); + + if (!validate) { + return done(null, false, { message: 'Email or Password Incorrect' }); + } + + return done(null, user, { message: 'Logged in Successfully' }); + } catch (error) { + return done(error); + } +}; + +passport.use( + 'login', + new localStrategy( + { + usernameField: 'email', + passwordField: 'password', + }, + authenticateUser + ) +); \ No newline at end of file diff --git a/views/index.html b/views/index.html new file mode 100644 index 00000000..fae0360e --- /dev/null +++ b/views/index.html @@ -0,0 +1,16 @@ + + + + + + + Document + + +

Welcome to pizza App

+
+ + +
+ + \ No newline at end of file diff --git a/views/login.html b/views/login.html new file mode 100644 index 00000000..71e29715 --- /dev/null +++ b/views/login.html @@ -0,0 +1,23 @@ + + + + + + + Document + + +

Login

+
+
+ + +
+
+ + +
+ +
+ + \ No newline at end of file diff --git a/views/signup.html b/views/signup.html new file mode 100644 index 00000000..b6b72c4e --- /dev/null +++ b/views/signup.html @@ -0,0 +1,31 @@ + + + + + + + Document + + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + \ No newline at end of file