diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js index a73e16a8..46db777f 100644 --- a/src/controllers/auth.controller.js +++ b/src/controllers/auth.controller.js @@ -1,36 +1,70 @@ const httpStatus = require('http-status'); const catchAsync = require('../utils/catchAsync'); -const { authService, userService, emailService } = require('../services'); +const { User, Token } = require('../models'); +const { tokenService, emailService } = require('../services'); +const AppError = require('../utils/AppError'); const register = catchAsync(async (req, res) => { - const user = await userService.createUser(req.body); - const tokens = await authService.generateAuthTokens(user.id); + if (await User.isEmailTaken(req.body.email)) { + throw new AppError(httpStatus.BAD_REQUEST, 'Email already taken'); + } + const user = await User.create(req.body); + const tokens = await tokenService.generateAuthTokens(user.id); const response = { user: user.transform(), tokens }; res.status(httpStatus.CREATED).send(response); }); const login = catchAsync(async (req, res) => { - const user = await authService.loginUser(req.body.email, req.body.password); - const tokens = await authService.generateAuthTokens(user.id); + const { email, password } = req.body; + const user = await User.findOne({ email }); + if (!user || !(await user.isPasswordMatch(password))) { + throw new AppError(httpStatus.UNAUTHORIZED, 'Incorrect email or password'); + } + const tokens = await tokenService.generateAuthTokens(user.id); const response = { user: user.transform(), tokens }; res.send(response); }); const refreshTokens = catchAsync(async (req, res) => { - const tokens = await authService.refreshAuthTokens(req.body.refreshToken); - const response = { ...tokens }; - res.send(response); + try { + const refreshTokenDoc = await tokenService.verifyToken(req.body.refreshToken, 'refresh'); + const user = await User.findById(refreshTokenDoc.user); + if (!user) { + throw new Error(); + } + await refreshTokenDoc.remove(); + const tokens = await tokenService.generateAuthTokens(user.id); + const response = { ...tokens }; + res.send(response); + } catch (error) { + throw new AppError(httpStatus.UNAUTHORIZED, 'Please authenticate'); + } }); const forgotPassword = catchAsync(async (req, res) => { - const resetPasswordToken = await authService.generateResetPasswordToken(req.body.email); + const user = await User.findOne({ email: req.body.email }); + if (!user) { + throw new AppError(httpStatus.NOT_FOUND, 'No users found with this email'); + } + const resetPasswordToken = await tokenService.generateResetPasswordToken(user.id); await emailService.sendResetPasswordEmail(req.body.email, resetPasswordToken); res.status(httpStatus.NO_CONTENT).send(); }); const resetPassword = catchAsync(async (req, res) => { - await authService.resetPassword(req.query.token, req.body.password); - res.status(httpStatus.NO_CONTENT).send(); + try { + const resetPasswordTokenDoc = await tokenService.verifyToken(req.query.token, 'resetPassword'); + const user = await User.findById(resetPasswordTokenDoc.user); + if (!user) { + throw new Error(); + } + user.password = req.body.password; + await user.save(); + await Token.deleteMany({ user: user.id, type: 'resetPassword' }); + res.status(httpStatus.NO_CONTENT).send(); + } catch (error) { + throw new AppError(httpStatus.UNAUTHORIZED, 'Password reset failed'); + } }); module.exports = { diff --git a/src/controllers/user.controller.js b/src/controllers/user.controller.js index d8ebf5cf..e616950f 100644 --- a/src/controllers/user.controller.js +++ b/src/controllers/user.controller.js @@ -1,30 +1,53 @@ const httpStatus = require('http-status'); +const { pick } = require('lodash'); +const { User } = require('../models'); +const AppError = require('../utils/AppError'); const catchAsync = require('../utils/catchAsync'); -const { userService } = require('../services'); +const { getQueryOptions } = require('../utils/query.utils'); const createUser = catchAsync(async (req, res) => { - const user = await userService.createUser(req.body); + if (await User.isEmailTaken(req.body.email)) { + throw new AppError(httpStatus.BAD_REQUEST, 'Email already taken'); + } + const user = await User.create(req.body); res.status(httpStatus.CREATED).send(user.transform()); }); const getUsers = catchAsync(async (req, res) => { - const users = await userService.getUsers(req.query); + const filter = pick(req.query, ['name', 'role']); + const options = getQueryOptions(req.query); + const users = await User.find(filter, null, options); const response = users.map(user => user.transform()); res.send(response); }); const getUser = catchAsync(async (req, res) => { - const user = await userService.getUserById(req.params.userId); + const user = await User.findById(req.params.userId); + if (!user) { + throw new AppError(httpStatus.NOT_FOUND, 'User not found'); + } res.send(user.transform()); }); const updateUser = catchAsync(async (req, res) => { - const user = await userService.updateUser(req.params.userId, req.body); + const user = await User.findById(req.params.userId); + if (!user) { + throw new AppError(httpStatus.NOT_FOUND, 'User not found'); + } + if (req.body.email && (await User.isEmailTaken(req.body.email, user.id))) { + throw new AppError(httpStatus.BAD_REQUEST, 'Email already taken'); + } + Object.assign(user, req.body); + await user.save(); res.send(user.transform()); }); const deleteUser = catchAsync(async (req, res) => { - await userService.deleteUser(req.params.userId); + const user = await User.findById(req.params.userId); + if (!user) { + throw new AppError(httpStatus.NOT_FOUND, 'User not found'); + } + await user.remove(); res.status(httpStatus.NO_CONTENT).send(); }); diff --git a/src/models/user.model.js b/src/models/user.model.js index ff96bd10..286f7aba 100644 --- a/src/models/user.model.js +++ b/src/models/user.model.js @@ -47,6 +47,16 @@ const userSchema = mongoose.Schema( } ); +userSchema.statics.isEmailTaken = async function(email, excludeUserId) { + const user = await this.findOne({ email, _id: { $ne: excludeUserId } }); + return !!user; +}; + +userSchema.methods.isPasswordMatch = async function(password) { + const user = this; + return bcrypt.compare(password, user.password); +}; + userSchema.methods.toJSON = function() { const user = this; return omit(user.toObject(), ['password']); diff --git a/src/services/auth.service.js b/src/services/auth.service.js deleted file mode 100644 index 8537d816..00000000 --- a/src/services/auth.service.js +++ /dev/null @@ -1,85 +0,0 @@ -const moment = require('moment'); -const bcrypt = require('bcryptjs'); -const httpStatus = require('http-status'); -const config = require('../config/config'); -const tokenService = require('./token.service'); -const userService = require('./user.service'); -const Token = require('../models/token.model'); -const AppError = require('../utils/AppError'); - -const generateAuthTokens = async userId => { - const accessTokenExpires = moment().add(config.jwt.accessExpirationMinutes, 'minutes'); - const accessToken = tokenService.generateToken(userId, accessTokenExpires); - - const refreshTokenExpires = moment().add(config.jwt.refreshExpirationDays, 'days'); - const refreshToken = tokenService.generateToken(userId, refreshTokenExpires); - await tokenService.saveToken(refreshToken, userId, refreshTokenExpires, 'refresh'); - - return { - access: { - token: accessToken, - expires: accessTokenExpires.toDate(), - }, - refresh: { - token: refreshToken, - expires: refreshTokenExpires.toDate(), - }, - }; -}; - -const checkPassword = async (password, correctPassword) => { - const isPasswordMatch = await bcrypt.compare(password, correctPassword); - if (!isPasswordMatch) { - throw new AppError(httpStatus.BAD_REQUEST, 'Passwords do not match'); - } -}; - -const loginUser = async (email, password) => { - try { - const user = await userService.getUserByEmail(email); - await checkPassword(password, user.password); - return user; - } catch (error) { - throw new AppError(httpStatus.UNAUTHORIZED, 'Incorrect email or password'); - } -}; - -const refreshAuthTokens = async refreshToken => { - try { - const refreshTokenDoc = await tokenService.verifyToken(refreshToken, 'refresh'); - const userId = refreshTokenDoc.user; - await userService.getUserById(userId); - await refreshTokenDoc.remove(); - return await generateAuthTokens(userId); - } catch (error) { - throw new AppError(httpStatus.UNAUTHORIZED, 'Please authenticate'); - } -}; - -const generateResetPasswordToken = async email => { - const user = await userService.getUserByEmail(email); - const expires = moment().add(config.jwt.resetPasswordExpirationMinutes, 'minutes'); - const resetPasswordToken = tokenService.generateToken(user._id, expires); - await tokenService.saveToken(resetPasswordToken, user._id, expires, 'resetPassword'); - return resetPasswordToken; -}; - -const resetPassword = async (resetPasswordToken, newPassword) => { - let userId; - try { - const resetPasswordTokenDoc = await tokenService.verifyToken(resetPasswordToken, 'resetPassword'); - userId = resetPasswordTokenDoc.user; - await userService.updateUser(userId, { password: newPassword }); - } catch (error) { - throw new AppError(httpStatus.UNAUTHORIZED, 'Password reset failed'); - } - await Token.deleteMany({ user: userId, type: 'resetPassword' }); -}; - -module.exports = { - generateAuthTokens, - loginUser, - refreshAuthTokens, - generateResetPasswordToken, - resetPassword, -}; diff --git a/src/services/index.js b/src/services/index.js index 73547db9..b6466005 100644 --- a/src/services/index.js +++ b/src/services/index.js @@ -1,4 +1,2 @@ -module.exports.authService = require('./auth.service'); module.exports.emailService = require('./email.service'); module.exports.tokenService = require('./token.service'); -module.exports.userService = require('./user.service'); diff --git a/src/services/token.service.js b/src/services/token.service.js index 245c0084..7fbb6d8e 100644 --- a/src/services/token.service.js +++ b/src/services/token.service.js @@ -1,9 +1,7 @@ const jwt = require('jsonwebtoken'); const moment = require('moment'); -const httpStatus = require('http-status'); const config = require('../config/config'); const { Token } = require('../models'); -const AppError = require('../utils/AppError'); const generateToken = (userId, expires, secret = config.jwt.secret) => { const payload = { @@ -29,13 +27,42 @@ const verifyToken = async (token, type) => { const payload = jwt.verify(token, config.jwt.secret); const tokenDoc = await Token.findOne({ token, type, user: payload.sub, blacklisted: false }); if (!tokenDoc) { - throw new AppError(httpStatus.NOT_FOUND, 'Token not found'); + throw new Error('Token not found'); } return tokenDoc; }; +const generateAuthTokens = async userId => { + const accessTokenExpires = moment().add(config.jwt.accessExpirationMinutes, 'minutes'); + const accessToken = generateToken(userId, accessTokenExpires); + + const refreshTokenExpires = moment().add(config.jwt.refreshExpirationDays, 'days'); + const refreshToken = generateToken(userId, refreshTokenExpires); + await saveToken(refreshToken, userId, refreshTokenExpires, 'refresh'); + + return { + access: { + token: accessToken, + expires: accessTokenExpires.toDate(), + }, + refresh: { + token: refreshToken, + expires: refreshTokenExpires.toDate(), + }, + }; +}; + +const generateResetPasswordToken = async userId => { + const expires = moment().add(config.jwt.resetPasswordExpirationMinutes, 'minutes'); + const resetPasswordToken = generateToken(userId, expires); + await saveToken(resetPasswordToken, userId, expires, 'resetPassword'); + return resetPasswordToken; +}; + module.exports = { generateToken, saveToken, verifyToken, + generateAuthTokens, + generateResetPasswordToken, }; diff --git a/src/services/user.service.js b/src/services/user.service.js deleted file mode 100644 index b832a490..00000000 --- a/src/services/user.service.js +++ /dev/null @@ -1,66 +0,0 @@ -const httpStatus = require('http-status'); -const { pick } = require('lodash'); -const AppError = require('../utils/AppError'); -const { User } = require('../models'); -const { getQueryOptions } = require('../utils/service.util'); - -const checkDuplicateEmail = async (email, excludeUserId) => { - const user = await User.findOne({ email, _id: { $ne: excludeUserId } }); - if (user) { - throw new AppError(httpStatus.BAD_REQUEST, 'Email already taken'); - } -}; - -const createUser = async userBody => { - await checkDuplicateEmail(userBody.email); - const user = await User.create(userBody); - return user; -}; - -const getUsers = async query => { - const filter = pick(query, ['name', 'role']); - const options = getQueryOptions(query); - const users = await User.find(filter, null, options); - return users; -}; - -const getUserById = async userId => { - const user = await User.findById(userId); - if (!user) { - throw new AppError(httpStatus.NOT_FOUND, 'User not found'); - } - return user; -}; - -const getUserByEmail = async email => { - const user = await User.findOne({ email }); - if (!user) { - throw new AppError(httpStatus.NOT_FOUND, 'No user found with this email'); - } - return user; -}; - -const updateUser = async (userId, updateBody) => { - const user = await getUserById(userId); - if (updateBody.email) { - await checkDuplicateEmail(updateBody.email, userId); - } - Object.assign(user, updateBody); - await user.save(); - return user; -}; - -const deleteUser = async userId => { - const user = await getUserById(userId); - await user.remove(); - return user; -}; - -module.exports = { - createUser, - getUsers, - getUserById, - getUserByEmail, - updateUser, - deleteUser, -}; diff --git a/src/utils/service.util.js b/src/utils/query.utils.js similarity index 100% rename from src/utils/service.util.js rename to src/utils/query.utils.js