diff --git a/public/404.html b/public/404.html new file mode 100644 index 0000000..efacd41 --- /dev/null +++ b/public/404.html @@ -0,0 +1,148 @@ + + + + + + + 404 + + + + +
+ + +

Oh no!!

+

+ You’re either misspelling the URL
+ or requesting a page that's no longer here. +

+
+ Back to previous page +
+ + + + diff --git a/server.js b/server.js index 9825592..4ebaefc 100644 --- a/server.js +++ b/server.js @@ -1,5 +1,27 @@ +const mongoose = require('mongoose'); const express = require('express'); +const dotenv = require('dotenv'); +const path = require('path'); + +dotenv.config({ path: './config.env' }); + const app = express(); + +const DB = process.env.DATABASE.replace('', process.env.DATABASE_PASSWORD); + +mongoose + .connect(DB, { + useUnifiedTopology: true, + useNewUrlParser: true, + }) + .then(() => console.log('DB connection successful!')); + +const api = require('./src/api'); + +const port = process.env.PORT || 3000; +app.listen(port, () => { + console.log(`App running on port ${port}...`); +======= const api = require('./src/api'); const path = require('path'); const cloudinary = require('cloudinary'); @@ -12,23 +34,18 @@ cloudinary.config({ api_secret: process.env.CLOUDINARY_SECRET_KEY, }); -mongoose.connect(process.env.MONGODB_CONNECT_STRING); app.use(cors()); -app.listen('3000', () => { - console.log('Running'); -}); +// ROUTING app.use(express.json()); app.use('/api/v1', api); +// ERROR HANDLER app.use((req, res) => { - res.status(404).json({ - statusCode: 404, - message: 'API not found', - }); + res.status(404).sendFile(path.join(__dirname, '/public/404.html')); }); app.use((error, req, res, next) => { diff --git a/src/api/auth/auth.controller.js b/src/api/auth/auth.controller.js index d1c21ff..c5f7754 100644 --- a/src/api/auth/auth.controller.js +++ b/src/api/auth/auth.controller.js @@ -1,82 +1,50 @@ -const { promisify } = require('util'); -const jwt = require('jsonwebtoken'); -const User = require('../models/userModel'); -const catchAsync = require('../utils/catchAsync'); -const AppError = require('../utils/appError'); - -const signToken = (id) => { - return jwt.sign({ id }, process.env.JWT_SECRET, { - expiresIn: process.env.JWT_EXPIRES_IN, - }); +const authService = require('./auth.service'); + +module.exports = { + signup: async (req, res, next) => { + try { + res.send(await authService.signup(req.body)); + } catch (error) { + next(error); + } + }, + login: async (req, res, next) => { + try { + res.send(await authService.login(req.body)); + } catch (error) { + next(error); + } + }, + protect: async (req, res, next) => { + try { + await authService.protect(req); + next(); + } catch (error) { + next(error); + } + }, + forgotPassword: async (req, res, next) => { + try { + let DTO = await authService.forgotPassword(req); + res.status(200).json(DTO); + } catch (error) { + next(error); + } + }, + resetPassword: async (req, res, next) => { + try { + let DTO = await authService.resetPassword(req.body, req.params.token); + res.status(200).json(DTO); + } catch (error) { + next(error); + } + }, + updatePassword: async (req, res, next) => { + try { + let DTO = await authService.updatePassword(req.user.id, req.body); + res.status(200).json(DTO); + } catch (error) { + next(error); + } + }, }; - -exports.signup = catchAsync(async (req, res, next) => { - const newUser = await User.create({ - name: req.body.name, - email: req.body.email, - password: req.body.password, - passwordConfirm: req.body.passwordConfirm, - }); - - const token = signToken(newUser.id); - - res.status(201).json({ - status: 'success', - token, - data: { - user: newUser, - }, - }); -}); - -exports.login = async (req, res, next) => { - const { email, password } = req.body; - - // 1) Check if email and password exist - if (!email || !password) { - next(new AppError('Please provide both an email and password!', 400)); - } - // 2) Check if user exists and password are correct - const user = await User.findOne({ email }).select('+password'); - - if (!user || !(await user.correctPassword(password, user.password))) { - return next(new AppError('Incorrect email or password!', 401)); - } - - // 3) If everything ok, send token to client - const token = signToken(user._id); - res.status(200).json({ - status: 'success', - token, - }); -}; - -exports.protect = catchAsync(async (req, res, next) => { - let token; - // 1) Getting token and check of it's there - if (req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) { - token = req.headers.authorization.split(' ')[1]; - } - - if (!token) { - return next(new AppError('You are not logged in! Please log in to get access', 401)); - } - - // 2) Verification token - const decoded = await promisify(jwt.verify)(token, process.env.JWT_SECRET); - - // 3) Check if user still exists - const currentUser = await User.findById(decoded.id); - if (!currentUser) { - return next(new AppError('The token belonging to this token does no longer exist!', 401)); - } - - // 4) Check if user changed password after the token was issued - if (currentUser.changedPasswordAfter(decoded.iat)) { - return next(new AppError('User recently changed password! Please log in again.', 401)); - } - - // GRANT ACCESS TO PROTECTED ROUTE - req.user = currentUser; - next(); -}); diff --git a/src/api/auth/auth.service.js b/src/api/auth/auth.service.js index e69de29..3a42793 100644 --- a/src/api/auth/auth.service.js +++ b/src/api/auth/auth.service.js @@ -0,0 +1,189 @@ +const crypto = require('crypto'); +const { promisify } = require('util'); +const jwt = require('jsonwebtoken'); +const User = require('./../../models/userModel'); +const AppError = require('./../../common/appError'); +const sendEmail = require('./../../common/email'); + +const signToken = (id) => { + return jwt.sign({ id }, process.env.JWT_SECRET, { + expiresIn: process.env.JWT_EXPIRES_IN, + }); +}; + +// const createSendToken = (user, req, res) => { +// const token = signToken(user._id); + +// res.cookie('jwt', token, { +// expires: new Date(Date.now() + process.env.JWT_COOKIE_EXPIRES_IN * 24 * 60 * 60 * 1000), +// httpOnly: true, +// secure: req.secure || req.headers['x-forwarded-proto'] === 'https', +// }); + +// // Remove password from output +// user.password = undefined; + +// return { +// msg: 'Success', +// }; +// }; + +exports.signup = async (body) => { + try { + const user = await User.findOne({ email: body.email }); + if (user) { + throw new AppError(409, 'Email already exists! Please try another.'); + } + await User.create({ + name: body.name, + email: body.email, + password: body.password, + passwordConfirm: body.passwordConfirm, + }); + // createSendToken(newUser, req, res); + return { + statusCode: 200, + message: 'Your account has been created', + }; + } catch (error) { + errorStatusCode = error.statusCode ? error.statusCode : 500; + throw new AppError(errorStatusCode, error.message); + } +}; + +exports.login = async (body) => { + try { + const { email, password } = body; + // 1) Check if email and password exist + if (!email || !password) { + throw new AppError('Please provide email and password!', 400); + } + // 2) Check if user exists && password is correct + const user = await User.findOne({ email }).select('+password'); + + if (!user || !(await user.correctPassword(password, user.password))) { + throw new AppError(401, 'Incorrect email or password'); + } + + // 3) If everything ok, send token to client + // createSendToken(user, req, res); + + const token = signToken(user._id); + user.password = undefined; + return { + statusCode: 200, + message: 'Your account has logged in', + token, + data: user, + }; + } catch (error) { + errorStatusCode = error.statusCode ? error.statusCode : 500; + throw new AppError(errorStatusCode, error.message); + } +}; + +exports.forgotPassword = async (req) => { + // 1) Get user based on POST email + const user = await User.findOne({ email: req.body.email }); + if (!user) { + throw new AppError(404, 'There is no user with email address.'); + } + + // 2) Generate the random reset token + const resetToken = user.createPasswordResetToken(); + await user.save({ validateBeforeSave: false }); + + // 3) Send it to user's email + const resetURL = `${req.protocol}://${req.get('host')}/api/v1/users/resetPassword/${resetToken}`; + + const message = `Forgot your password? Submit a PATCH request with your new password and passwordConfirm to: ${resetURL}.\nIf you didn't forget your password, please ignore this email!`; + + try { + await sendEmail({ + email: user.email, + subject: 'Your password reset token (valid for 10 min)', + message, + }); + + return { + statusCode: 200, + message: 'Token sent to email!', + }; + } catch (error) { + user.passwordResetToken = undefined; + user.passwordResetExpires = undefined; + await user.save({ validateBeforeSave: false }); + + throw new AppError(500, 'There was an error sending the email. Try again later!'); + } +}; + +exports.resetPassword = async (body, token) => { + try { + // 1) Get user based on the token + const hashedToken = crypto.createHash('sha256').update(token).digest('hex'); + + const user = await User.findOne({ + passwordResetToken: hashedToken, + passwordResetExpires: { $gt: Date.now() }, + }); + + // 2) If token has not expired, and there is user, set the new password + if (!user) { + throw new AppError(400, 'Token is invalid or has expired'); + } + user.password = body.password; + user.passwordConfirm = body.passwordConfirm; + user.passwordResetToken = undefined; + user.passwordResetExpires = undefined; + await user.save(); + + // 3) Update changedPasswordAt property for the user + // 4) Log the user in, send JWT + // createSendToken(user, req, res); + token = signToken(user._id); + user.password = undefined; + return { + statusCode: 200, + message: 'Your password has been reset', + token, + data: user, + }; + } catch { + errorStatusCode = error.statusCode ? error.statusCode : 500; + throw new AppError(errorStatusCode, error.message); + } +}; + +exports.updatePassword = async (userID, body) => { + try { + // 1) Get user from collection + const user = await User.findById(userID).select('+password'); + + // 2) Check if POSTed current password is correct + if (!(await user.correctPassword(body.passwordCurrent, user.password))) { + throw new AppError(401, 'Your current password is wrong.'); + } + + // 3) If so, update password + user.password = body.password; + user.passwordConfirm = body.passwordConfirm; + await user.save(); + + // 4) Log user in, send JWT + const token = signToken(user._id); + user.password = undefined; + + return { + statusCode: 200, + message: 'Your password has been reset', + token, + data: user, + }; + + // createSendToken(user, req, res); + } catch (error) { + errorStatusCode = error.statusCode ? error.statusCode : 500; + throw new AppError(errorStatusCode, error.message); + } +}; diff --git a/src/api/auth/index.js b/src/api/auth/index.js index e69de29..4d1c729 100644 --- a/src/api/auth/index.js +++ b/src/api/auth/index.js @@ -0,0 +1,15 @@ +// route endpoint /auth +const authController = require('./auth.controller'); +const router = require('express').Router(); +const { protectRoute } = require('../../common/protectRoute'); +// const { userPermission } = require('../../common/userPermission'); + +router.post('/signup', authController.signup); +router.post('/login', authController.login); + +router.route('/forgotPassword').post(authController.forgotPassword); +router.patch('/resetPassword/:token', authController.resetPassword); + +router.route('/updateMyPassword').patch(protectRoute, authController.updatePassword); + +module.exports = router; diff --git a/src/common/appError.js b/src/common/appError.js new file mode 100644 index 0000000..21c5678 --- /dev/null +++ b/src/common/appError.js @@ -0,0 +1,14 @@ +class AppError extends Error { + constructor(statusCode, message) { + super(); + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, AppError); + } + + this.message = message; + this.statusCode = statusCode; + } +} + +module.exports = AppError; diff --git a/src/common/email.js b/src/common/email.js new file mode 100644 index 0000000..4434ec9 --- /dev/null +++ b/src/common/email.js @@ -0,0 +1,28 @@ +const nodemailer = require('nodemailer'); + +const sendEmail = async (options) => { + // 1) Create a transporter + const transporter = nodemailer.createTransport({ + host: process.env.EMAIL_HOST, + port: process.env.EMAIL_PORT, + auth: { + user: process.env.EMAIL_USERNAME, + pass: process.env.EMAIL_PASSWORD, + }, + // Activate in email "less secure app" option + }); + + // 2) Define the email options + const mailOptions = { + from: 'LongChau ', + to: options.email, + subject: options.subject, + text: options.message, + // html + }; + + // 3) Actually send the email + await transporter.sendMail(mailOptions); +}; + +module.exports = sendEmail; diff --git a/src/common/protectRoute.js b/src/common/protectRoute.js new file mode 100644 index 0000000..9dddf83 --- /dev/null +++ b/src/common/protectRoute.js @@ -0,0 +1,36 @@ +const jwt = require('jsonwebtoken'); +const AppError = require('./appError'); +const { promisify } = require('util'); +const User = require('./../models/userModel'); + +exports.protectRoute = async (req, res, next) => { + // 1) Getting token and check of it's there + let token; + if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) { + token = req.headers.authorization.split(' ')[1]; + } else if (req.cookies.jwt) { + token = req.cookies.jwt; + } + + if (!token) { + throw new AppError(401, 'You are not logged in! Please log in to get access.'); + } + + // 2) Verification token + const decoded = await promisify(jwt.verify)(token, process.env.JWT_SECRET); + + // 3) Check if user still exists + const currentUser = await User.findById(decoded.id); + if (!currentUser) { + throw new AppError(401, 'The user belonging to this token does no longer exist.'); + } + + // 4) Check if user changed password after the token was issued + if (currentUser.changedPasswordAfter(decoded.iat)) { + throw new AppError(401, 'User recently changed password! Please log in again.'); + } + + // GRANT ACCESS TO PROTECTED ROUTE + req.user = currentUser; + next(); +}; diff --git a/src/common/userPermission.js b/src/common/userPermission.js new file mode 100644 index 0000000..2865c63 --- /dev/null +++ b/src/common/userPermission.js @@ -0,0 +1,10 @@ +const AppError = require('./appError'); + +exports.userPermission = (...roles) => { + return (req, res, next) => { + if (!roles.includes(req.user.role)) { + throw new AppError(403, 'You do not have permission to perform this action'); + } + next(); + }; +}; diff --git a/src/models/order.js b/src/models/order.js new file mode 100644 index 0000000..04d3c8d --- /dev/null +++ b/src/models/order.js @@ -0,0 +1,25 @@ +const mongoose = require('mongoose'); + +const Schema = mongoose.Schema; + +const orderSchema = new Schema({ + products: [ + { + product: { type: Object, required: true }, + quantity: { type: Number, required: true }, + }, + ], + user: { + name: { + type: String, + required: true, + }, + userId: { + type: mongoose.Schema.ObjectId, + ref: 'User', + required: true, + }, + }, +}); + +module.exports = mongoose.model('Order', orderSchema); diff --git a/src/models/productModel.js b/src/models/productModel.js index 4b9f054..f8ee4e7 100644 --- a/src/models/productModel.js +++ b/src/models/productModel.js @@ -31,6 +31,10 @@ const productSchema = mongoose.Schema({ type: Date, default: Date.now, }, + countInStocks: { + type: Number, + required: true, + }, userID: { type: mongoose.Schema.Types.ObjectId, ref: 'User', @@ -38,12 +42,4 @@ const productSchema = mongoose.Schema({ }, }); -productSchema.virtual('id').get(function () { - return this._id.toHexString(); -}); - -productSchema.set('toJSON', { - virtuals: true, -}); - exports.Product = mongoose.model('Product', productSchema); diff --git a/src/models/userModel.js b/src/models/userModel.js index cd2165d..00106ca 100644 --- a/src/models/userModel.js +++ b/src/models/userModel.js @@ -110,16 +110,14 @@ userSchema.pre('save', function (next) { next(); }); -userSchema.pre(/^find/, function (next) { - // this points to the current query - this.find({ active: { $ne: false } }); - next(); -}); - userSchema.methods.correctPassword = async function (candidatePassword, userPassword) { return await bcrypt.compare(candidatePassword, userPassword); }; +userSchema.methods.duplicateEmail = async function (candidateEmail, userEmail) { + return candidateEmail === userEmail; +}; + userSchema.methods.changedPasswordAfter = function (JWTTimestamp) { if (this.passwordChangedAt) { const changedTimestamp = parseInt(this.passwordChangedAt.getTime() / 1000, 10);