From 697b04a55b7bd89cc2a0de8ebcd123b5f109bb35 Mon Sep 17 00:00:00 2001
From: Mike Fitzpatrick ${firstName}, An account was recently created on the Eventment giving platform for this email (${email}).Please confirm your account
If you did not create this account, please disregard this email.
`, + }; + + Mailer(email, mail, callback); + }, + + reset: ({ email, firstName, token }, callback = helpers.emptyFunction) => { + const link = `${config.api.url}${config.security.reset.route}/${encodeURI(token)}`; + const mail = { + subject: 'Please confirm your account', + text: `Password Reset Request\r\r\r\r${firstName},\r\rA password reset request was made for your account on the Eventment giving platform.\r\rTo reset your password, please open this link in your browser: ${link}\r\rIf you did not make this request, you may disregard this email and your password will remain unchanged.`, + html: `${firstName},
A password reset request was made for your account on the Eventment giving platform.
To reset your password, please click here.If you did not make this request, you may disregard this email and your password will remain unchanged.
`, + }; + + Mailer(email, mail, callback); + }, +}; diff --git a/lib/Mailer.js b/lib/Mailer.js new file mode 100644 index 0000000..7f57623 --- /dev/null +++ b/lib/Mailer.js @@ -0,0 +1,17 @@ +const config = require('../config.js'); +const nodemailer = require('nodemailer'); + +const from = `"${config.mail.from.name}" <${config.mail.from.address}>`; +const transporter = nodemailer.createTransport(config.mail.smtp); + +module.exports = (email, options, callback) => { + callback = typeof callback === 'function' ? callback : (error, info) => { + if (error) { + return console.log(error); + } + + console.log('Message %s sent: %s', info.messageId, info.response); + }; + + transporter.sendMail({ to: email, from, ...options }, callback); +}; diff --git a/lib/helpers.js b/lib/helpers.js new file mode 100644 index 0000000..7cf7b42 --- /dev/null +++ b/lib/helpers.js @@ -0,0 +1,6 @@ +module.exports = { + + emptyFunction: (...args) => args, + + +}; diff --git a/models/user.js b/models/user.js index ca95f5a..0f84a08 100644 --- a/models/user.js +++ b/models/user.js @@ -5,6 +5,7 @@ const timestamps = require('mongoose-timestamp'); const config = require('../config.js'); +const UserEmails = require('../emails/user'); const AddressSchema = require('./common/address.js'); const PhoneSchema = require('./common/phone.js'); @@ -97,7 +98,7 @@ const UserSchema = new mongoose.Schema( default: false, }, - resetCheckBit: { + tokenCheckBit: { type: String, default: null, }, @@ -127,11 +128,30 @@ UserSchema.methods.authenticate = function (username, password) { return false; }; -UserSchema.methods.isNomAvailable = function (nom) { - return !!!this.model('User').findOne({ nomDeBid }); +UserSchema.methods.confirmRegistration = function (callback = () => {}) { + this.isVerified = true; + this.tokenCheckBit = undefined; + this.save(callback); }; -UserSchema.methods.generateJWT = function (props = {}) { +UserSchema.methods.generateAccountToken = function (callback = () => {}) { + const tokenCheckBit = crypto.randomBytes(16).toString('hex'); + const token = jwt.sign({ + sub: this.id, + key: tokenCheckBit, + iss: config.security.jwt.issuer, + aud: config.security.jwt.audience, + iat: Date.now(), + exp: (Date.now() + (24*60*60*1000)), + }, config.security.jwt.secret); + + this.tokenCheckBit = tokenCheckBit; + this.save(); + + return token; +}; + +UserSchema.methods.generateLoginToken = function (props = {}) { const { exp, iss } = props; const today = new Date(); @@ -160,33 +180,20 @@ UserSchema.methods.getNomDeBid = function () { return this.nomDeBid || `${this.firstName} ${this.lastName.charAt(0)}`; }; -UserSchema.methods.generateResetToken = function (callback = () => {}) { - const resetCheckBit = crypto.randomBytes(16).toString('hex'); - const token = jwt.sign({ - sub: this.id, - key: resetCheckBit, - iss:config.security.jwt.issuer, - aud: config.security.jwt.audience, - iat: Date.now(), - exp: (Date.now() + (24*60*60*1000)), - }, config.security.jwt.secret); - - this.resetCheckBit = resetCheckBit; - this.save(); - - return token; -}; - UserSchema.methods.isEventManager = function () { return this.isOrganizationEmployee || false; }; +UserSchema.methods.isNomAvailable = function (nom) { + return !!!this.model('User').findOne({ nomDeBid }); +}; + UserSchema.methods.isRegistrationVerified = function () { return this.isVerified || false; }; UserSchema.methods.sendPasswordReset = function () { - const resetToken = this.generateResetToken(); + const resetToken = this.generateAccountToken(); let resetRoute = config.security.resetRoute; resetRoute = resetRoute.replace(':user_id', this.id); @@ -196,6 +203,19 @@ UserSchema.methods.sendPasswordReset = function () { console.log('[sendPasswordReset] resetUrl:', resetUrl); }; +UserSchema.methods.sendConfirmationEmail = function (callback) { + const user = { + email: this.email, + firstName: this.firstName, + token: this.generateAccountToken(), + }; + + callback = typeof callback === 'function' ? callback : + (err, info) => console.log('[UserSchema.methods.sendConfirmationEmail]', { err, info }); + + UserEmails.confirmation(user, callback); +}; + UserSchema.methods.setNomDeBid = function (nomDeBid, callback = () => {}) { const alreadyExists = this.isNomAvailable(nomDeBid); @@ -223,7 +243,7 @@ UserSchema.methods.setPassword = function (password, callback = () => {}) { if (hasLocalStrategy) { this.model('User').findOneAndUpdate( { _id: this._id, 'credentials.method': 'local' }, - { $set: { 'credentials.$': strategy, resetCheckBit: null } }, + { $set: { 'credentials.$': strategy }, $unset: { tokenCheckBit: '' } }, { upsert: true }, callback, ); @@ -231,7 +251,7 @@ UserSchema.methods.setPassword = function (password, callback = () => {}) { if (!hasLocalStrategy) { this.credentials.push(strategy); - this.resetCheckBit = null; + this.tokenCheckBit = undefined; this.save(callback); } }; @@ -242,7 +262,7 @@ UserSchema.methods.toAuthJSON = function () { return { email: this.email, - token: this.generateJWT(), + token: this.generateLoginToken(), user: { id: this._id, nomDeBid: nomDeBid, @@ -354,14 +374,32 @@ UserSchema.statics.findOneAndUpdateOrCreate = function ( }); }; -UserSchema.statics.verifyResetToken = function (token, callback) { +UserSchema.statics.register = function (user, callback = () => {}) { + this.create(user, (err, user) => { + if (err) { + return callback(err, null); + } + + if (user) { + user.sendConfirmationEmail((err, info) => { + if (err) { + return callback(err, null); + } + + callback(null, user); + }); + } + }); +}; + +UserSchema.statics.verifyAccountToken = function (token, callback) { jwt.verify(token, config.security.jwt.secret, (err, decoded) => { if (err) { return callback(err); } const { sub, key } = decoded; - this.findOne({ _id: sub, resetCheckBit: key }, (err, user) => { + this.findOne({ _id: sub, tokenCheckBit: key }, (err, user) => { if (err) { return callback(err); } @@ -376,7 +414,7 @@ UserSchema.statics.verifyResetToken = function (token, callback) { }; UserSchema.statics.verifyTokenAndResetPassword = function (token, password, callback) { - this.verifyResetToken(token, (err, user, info) => { + this.verifyAccountToken(token, (err, user, info) => { if (err) { return callback(err); } @@ -389,6 +427,20 @@ UserSchema.statics.verifyTokenAndResetPassword = function (token, password, call }); }; +UserSchema.statics.verifyTokenAndConfirmRegistration = function (token, callback) { + this.verifyAccountToken(token, (err, user, info) => { + if (err) { + return callback(err); + } + + if (!user) { + return callback(err, false, info); + } + + user.confirmRegistration(callback); + }); +}; + /** * PATH OPERATIONS diff --git a/package.json b/package.json index 06bae47..4e545ef 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "jsonwebtoken": "^8.5.1", "mongoose": "^5.6.0", "mongoose-timestamp": "^0.6.0", + "nodemailer": "^6.3.0", "passport": "^0.4.0", "passport-facebook": "^3.0.0", "passport-google-oauth": "^2.0.0", diff --git a/routes/index.js b/routes/index.js index a8b57c0..1ef6eb8 100644 --- a/routes/index.js +++ b/routes/index.js @@ -8,5 +8,6 @@ module.exports = function(server, auth) { require('./items.js')(server, auth); require('./reset.js')(server, auth); require('./sales.js')(server, auth); + require('./signup.js')(server, auth); require('./users.js')(server, auth); }; diff --git a/routes/reset.js b/routes/reset.js index a92d46c..e930fdc 100644 --- a/routes/reset.js +++ b/routes/reset.js @@ -15,9 +15,9 @@ const routes = { }; module.exports = function (server, auth) { - server.get(routes.getTestToken, auth.secure, function (req, res, next) { + server.get(routes.getTestToken, auth.basic, function (req, res, next) { const { record: user } = req.user; - const resetToken = user.generateResetToken(); + const resetToken = user.generateAccountToken(); const resetUrl = `${url}${route}/${resetToken}`; res.send({ resetToken, resetUrl }); @@ -26,7 +26,7 @@ module.exports = function (server, auth) { server.post(routes.resetWithToken, auth.bypass, function (req, res, next) { const { reset_token } = req.params; - const { password } = req.body; + const { body: { password } = {}} = req; if (!reset_token) { return next( diff --git a/routes/signup.js b/routes/signup.js index b993054..ecff940 100644 --- a/routes/signup.js +++ b/routes/signup.js @@ -5,13 +5,13 @@ const User = require('../models/user'); module.exports = function (server, auth) { const { passport } = auth; - server.post('/signup', (req, res, next) => { + server.post('/signup', auth.basic, (req, res, next) => { const { body: { user = null } = {} } = req; let errors = {}; let errorCount = 0; if (!user) { - errors.user = 'is required - can\'t make something from nothing...''; + errors.user = `is required - can't make something from nothing...`; errorCount++; } @@ -21,13 +21,13 @@ module.exports = function (server, auth) { User.register(user, (err, user, info) => { if (err) { - next(err); + return next(err); } if (info) { res.send(200, { success: false, - nextSteps: 'Please fix the problems indicated and try again.' + nextSteps: 'Please fix the problems indicated and try again.', ...info }); @@ -36,9 +36,44 @@ module.exports = function (server, auth) { res.send(200, { success: true, - nextSteps: 'Check your email for our confirmation email, you will not be able to login without confirming.' + nextSteps: 'Check your email for our confirmation email, you will not be able to login without confirming.', }); + next(); + }); + }); + + server.get('/signup/confirm/:token([A-Za-z0-9_]+\.{3})', (req, res, next) => { + const { token } = req.params; + + if (!token) { + return next( + new errors.InvalidContentError('A confirmation token was not provided.'), + ); + } + + User.verifyTokenAndConfirmRegistration(reset_token, password, (err, user, info) => { + if (err) { + console.error(err); + return next( + new errors.InvalidContentError(err), + ); + } + + if (!user) { + console.error(err); + res.send({ + success: false, + info: `Account registration confirmation failed. ${info}`, + }); + return next(); + } + + res.send({ + success: true, + info: 'New account registration confirmed.', + ...user.toAuthJSON() + }); next(); }); });