Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reset password feature and update user endpoint #27

Open
wants to merge 32 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
da75583
rename files
d0peCode Jun 28, 2019
7fbddd7
add change from father branch (check if account activated)
d0peCode Jun 28, 2019
fc6d3f6
split auth and user routers
d0peCode Jun 28, 2019
dd836a4
get rid of transform (from parent branch)
d0peCode Jun 29, 2019
4db0046
split admin and user
d0peCode Jun 29, 2019
3fad9a8
refactor admin authenticate
d0peCode Jun 29, 2019
7ddc6d1
add admin model
d0peCode Jun 29, 2019
9f69097
add admin route
d0peCode Jun 29, 2019
e526e20
add additional role check
d0peCode Jun 29, 2019
3e2b611
delete useless only user access
d0peCode Jun 29, 2019
8a651ac
move hashPass and checkDup to external function, keep DRY
d0peCode Jun 30, 2019
24edc5f
move checkDupEmail to external function, keep DRY
d0peCode Jun 30, 2019
02f9238
remove roles from user.model
d0peCode Jun 30, 2019
eee1eb4
remove roles from user.model fix
d0peCode Jun 30, 2019
7d430de
move generateToken function to external file
d0peCode Jun 30, 2019
3cfd6ba
move models methods to external files to keep DRY
d0peCode Jun 30, 2019
caaac7f
add default admin credentails to .env
d0peCode Jun 30, 2019
63c5774
update readme
d0peCode Jul 1, 2019
c3d36a8
fix check active (not for admins)
d0peCode Jul 1, 2019
cb4021b
use config.host in sending mail
d0peCode Jul 2, 2019
304d9d7
remove unused packages from admin.model
d0peCode Jul 2, 2019
af401da
add parameter hook name to hashPass
d0peCode Jul 2, 2019
bc41594
export user data from passport service
d0peCode Jul 2, 2019
fc67a10
rename hostname to base_uri
d0peCode Jul 3, 2019
18891c9
add user resetPassKey to model
d0peCode Jul 3, 2019
e1fe8ed
add update, reset.sendMail and reset.updatePass methods
d0peCode Jul 3, 2019
d40e71e
add update user validator
d0peCode Jul 3, 2019
219cafb
add reset and update user routes
d0peCode Jul 3, 2019
bd96df8
use base_uri from env in config
d0peCode Jul 3, 2019
826ead6
useFindAndModify set to false (mongoose deprecation fix)
d0peCode Jul 3, 2019
41dff02
use update user validator
d0peCode Jul 3, 2019
7147d61
fix update user method
d0peCode Jul 3, 2019
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
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
NODE_ENV=dev
APP="Your App"
PORT=3000
BASE_URI=http://localhost:3000
MONGOURI=mongodb://localhost:27017/yourapp
MONGOTESTURI=mongodb://localhost:27017/test-app
APP_SECRET=somekey
TRANSPORTER_SERVICE=example-gmail
[email protected]
TRANSPORTER_PASSWORD=gmail-application_password
TRANSPORTER_PASSWORD=gmail-application_password
DEFAULT_ADMIN_NAME=admin
[email protected]
DEFAULT_ADMIN_PASSWORD=password
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,14 @@
- TRANSPORTER_PASSWORD is password to above email
- if gmail you need to generate special app-password, see for further support: https://support.google.com/mail/answer/185833?hl=en)

## Changes from original project
## Changelog

- Fixed deprecation warnings with mongoose usage.
- Updated dependencies to fix vulnerabilities.
- Added email confirmation after registration.
- Distinguish admin and user.

## TODO

- Split users and admins to two collections:
- Rename auth.controller.js to user.controller.js
- create user.route.js and refactor auth.route.js to contain only this demonstration "/secret" routes
- Integrate Swagger UI documentation
- Write unit tests
6 changes: 6 additions & 0 deletions src/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ require('dotenv').config() // load .env file

module.exports = {
port: process.env.PORT,
baseURI: process.env.BASE_URI,
app: process.env.APP,
env: process.env.NODE_ENV,
secret: process.env.APP_SECRET,
Expand All @@ -13,5 +14,10 @@ module.exports = {
service: process.env.TRANSPORTER_SERVICE,
email: process.env.TRANSPORTER_EMAIL,
password: process.env.TRANSPORTER_PASSWORD
},
admin: {
name: process.env.DEFAULT_ADMIN_NAME,
email: process.env.DEFAULT_ADMIN_EMAIL,
password: process.env.DEFAULT_ADMIN_PASSWORD
}
}
29 changes: 29 additions & 0 deletions src/controllers/admin.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use strict'

const Admin = require('../models/admin.model')
const jwt = require('jsonwebtoken')
const config = require('../config')
const httpStatus = require('http-status')
const generateToken = require('../models/utils/findAndGenerateToken')

exports.register = async (req, res, next) => {
try {
const admin = new Admin(req.body)
const savedAdmin = await admin.save()
res.status(httpStatus.CREATED)
res.send(savedAdmin.transform())
} catch (error) {
return next(Admin.checkDuplicateEmailError(error))
}
}

exports.login = async (req, res, next) => {
try {
const admin = await generateToken(req.body, 'admin')
const payload = { sub: admin.id }
const token = jwt.sign(payload, config.secret)
return res.json({ message: 'OK', token: token })
} catch (error) {
next(error)
}
}
44 changes: 0 additions & 44 deletions src/controllers/auth.controller.js

This file was deleted.

115 changes: 115 additions & 0 deletions src/controllers/user.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
'use strict'

const User = require('../models/user.model')
const jwt = require('jsonwebtoken')
const config = require('../config')
const httpStatus = require('http-status')
const uuidv1 = require('uuid/v1')
const generateToken = require('../models/utils/findAndGenerateToken')
const passport = require('../services/passport')
const bcrypt = require('bcrypt-nodejs')
const transporter = require('../services/transporter')
const APIError = require('../utils/APIError')

exports.register = async (req, res, next) => {
try {
const activationKey = uuidv1()
const body = req.body
body.activationKey = activationKey
const user = new User(body)
const savedUser = await user.save()
res.status(httpStatus.CREATED)
res.send(savedUser.transform())
} catch (error) {
return next(User.checkDuplicateEmailError(error))
}
}

exports.login = async (req, res, next) => {
try {
const user = await generateToken(req.body, 'user')
const payload = {sub: user.id}
const token = jwt.sign(payload, config.secret)
return res.json({ message: 'OK', token: token })
} catch (error) {
next(error)
}
}

exports.update = async (req, res, next) => {
try {
if (!passport.user || !req.body.password) {
res.status(httpStatus.UNAUTHORIZED)
return res.send(new APIError(`Password mismatch`, httpStatus.UNAUTHORIZED))
}

const user = await User.findOne({ 'email': passport.user.email }).exec()
if (!user.passwordMatches(req.body.password)) {
res.status(httpStatus.UNAUTHORIZED)
return res.send(new APIError(`Password mismatch`, httpStatus.UNAUTHORIZED))
}

if (req.body.newPassword) req.body.password = bcrypt.hashSync(req.body.newPassword)
await User.findOneAndUpdate(
{ '_id': passport.user._id },
{ $set: req.body }
)
return res.json({ message: 'OK' })
} catch (error) {
next(error)
}
}

exports.confirm = async (req, res, next) => {
try {
await User.findOneAndUpdate(
{ 'activationKey': req.query.key },
{ 'active': true }
)
return res.json({ message: 'OK' })
} catch (error) {
next(error)
}
}

exports.reset = {
async sendMail (req, res, next) {
try {
const resetPasswordKey = uuidv1()
await User.findOneAndUpdate(
{ '_id': passport.user._id },
{ 'resetPasswordKey': resetPasswordKey }
)
const mailOptions = {
from: 'noreply',
to: passport.user.email,
subject: 'Reset password',
html: `<div><h1>Hello user!</h1><p>Click <a href="${config.baseURI}/api/user/resetConfirm?key=${resetPasswordKey}">link</a> to reset your password.</p></div><div><h1>Hello developer!</h1><p>Feel free to change this template ;).</p></div>`
}

transporter.sendMail(mailOptions, function (error, info) {
if (error) {
next(error)
} else {
return res.json({ message: 'OK' })
}
})
} catch (error) {
next(error)
}
},
async updatePass (req, res, next) {
try {
if (!req.query.key) return res.status(httpStatus.BAD_REQUEST)
const newPassword = uuidv1()
const newPasswordHash = bcrypt.hashSync(newPassword)
await User.findOneAndUpdate(
{ 'resetPasswordKey': req.query.key },
{ $set: { 'password': newPasswordHash, 'resetPasswordKey': '' } }
)
return res.json({ message: 'OK', newPassword: newPassword })
} catch (error) {
next(error)
}
}
}
12 changes: 5 additions & 7 deletions src/middlewares/authorization.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
'use strict'

const User = require('../models/user.model')
const passport = require('passport')
const APIError = require('../utils/APIError')
const httpStatus = require('http-status')
const bluebird = require('bluebird')

// handleJWT with roles
const handleJWT = (req, res, next, roles) => async (err, user, info) => {
const handleJWT = (req, res, next, role) => async (err, user, info) => {
const error = err || info
const logIn = bluebird.promisify(req.logIn)
const apiError = new APIError(
error ? error.message : 'Unauthorized',
httpStatus.UNAUTHORIZED
error ? error.message : 'Unauthorized', httpStatus.UNAUTHORIZED
)

// log user in
Expand All @@ -24,7 +22,7 @@ const handleJWT = (req, res, next, roles) => async (err, user, info) => {
}

// see if user is authorized to do the action
if (!roles.includes(user.role)) {
if (role && role.includes('admin') && !user.admin) {
return next(new APIError('Forbidden', httpStatus.FORBIDDEN))
}

Expand All @@ -34,11 +32,11 @@ const handleJWT = (req, res, next, roles) => async (err, user, info) => {
}

// exports the middleware
const authorize = (roles = User.roles) => (req, res, next) =>
const authorize = (role) => (req, res, next) =>
passport.authenticate(
'jwt',
{ session: false },
handleJWT(req, res, next, roles)
handleJWT(req, res, next, role)
)(req, res, next)

module.exports = authorize
50 changes: 50 additions & 0 deletions src/models/admin.model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use strict'
const mongoose = require('mongoose')
const bcrypt = require('bcrypt-nodejs')
const Schema = mongoose.Schema
const hashPass = require('./utils/hashPass')
const checkDuplicateEmailError = require('./utils/checkDuplicateEmailError')

const adminSchema = new Schema({
email: {
type: String,
required: true,
unique: true,
lowercase: true
},
password: {
type: String,
required: true,
minlength: 4,
maxlength: 128
},
name: {
type: String,
maxlength: 50
},
admin: {
type: Boolean,
default: true
}
}, {
timestamps: true
})

hashPass(adminSchema, 'save')

adminSchema.method({
transform () {
const transformed = {}
const fields = ['id', 'name', 'email', 'createdAt']
fields.forEach((field) => { transformed[field] = this[field] })
return transformed
},

passwordMatches (password) {
return bcrypt.compareSync(password, this.password)
}
})

adminSchema.statics = { checkDuplicateEmailError }

module.exports = mongoose.model('Admin', adminSchema)
Loading