diff --git a/config.js b/config.js index cbb7f08..b4f37db 100644 --- a/config.js +++ b/config.js @@ -11,15 +11,19 @@ module.exports = { }, version: '0.1.0', assetStoreUrl: 'https://www.google.com/', - services: { - apple: {}, - facebook: { - appId: '2359355590971136', - appSecret: 'a5703f7d0af8e694aec5bd4175a85d6b', + mail: { + smtp: { + host: 'mail.fitz.guru', + port: 587, + secure: false, + auth: { + user: 'noreply@eventment.io', + pass: '3ventment.eieio', + }, }, - google: { - appId: '442412638360-p0idffou0qlpgor7agideudb1dh10mpf.apps.googleusercontent.com', - appSecret: 'a7fmS7Wc9Ssycr21WXdQ4TYl', + from: { + address: 'donotreply@eventment.io', + name: 'Eventment.io Support', }, }, security: { @@ -33,5 +37,20 @@ module.exports = { route: '/reset', tokenPlaceholder: ':reset_token', }, + confirm: { + route: '/confirm', + tokenPlaceholder: ':confirm_token', + }, + }, + services: { + apple: {}, + facebook: { + appId: '2359355590971136', + appSecret: 'a5703f7d0af8e694aec5bd4175a85d6b', + }, + google: { + appId: '442412638360-p0idffou0qlpgor7agideudb1dh10mpf.apps.googleusercontent.com', + appSecret: 'a7fmS7Wc9Ssycr21WXdQ4TYl', + }, }, }; diff --git a/emails/user.js b/emails/user.js new file mode 100644 index 0000000..84a79cf --- /dev/null +++ b/emails/user.js @@ -0,0 +1,28 @@ +const config = require('../config'); +const helpers = require('../lib/helpers'); +const Mailer = require('../lib/Mailer'); + +module.exports = { + + confirmation: ({ email, firstName, token }, callback = helpers.emptyFunction) => { + const link = `${config.api.url}${config.security.confirm.route}/${encodeURI(token)}`; + const mail = { + subject: 'Please confirm your account', + text: `Please confirm your account\r\r\r\r${firstName},\r\rAn account was recently created on the Eventment giving platform for this email (${email}).\r\rTo complete your registration, please open this link in your browser: ${link}\r\rIf you did not create this account, please disregard this email.`, + html: `

Please confirm your account

${firstName},

An account was recently created on the Eventment giving platform for this email (${email}).

To complete your registration, please click here.

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: `

Password Reset Request

${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(); }); });