From 5ee1951245db8487606a849dc9cfc1b0b5f7d4df Mon Sep 17 00:00:00 2001 From: barisusakli Date: Sun, 28 Jun 2015 21:54:21 -0400 Subject: [PATCH] closes #3271 --- public/language/en_GB/email.json | 5 ++ public/language/en_GB/users.json | 4 +- public/src/client/register.js | 6 ++ public/src/client/users.js | 19 ++++++ src/controllers/authentication.js | 12 +++- src/controllers/index.js | 57 ++++++++-------- src/controllers/users.js | 1 + src/emailer.js | 40 ++++++++--- src/socket.io/user.js | 13 +++- src/user.js | 2 + src/user/invite.js | 81 +++++++++++++++++++++++ src/views/admin/settings/user.tpl | 1 + src/views/emails/invitation.tpl | 14 ++++ src/views/emails/invitation_plaintext.tpl | 11 +++ 14 files changed, 224 insertions(+), 42 deletions(-) create mode 100644 src/user/invite.js create mode 100644 src/views/emails/invitation.tpl create mode 100644 src/views/emails/invitation_plaintext.tpl diff --git a/public/language/en_GB/email.json b/public/language/en_GB/email.json index 87baa1854b..1aa66835b8 100644 --- a/public/language/en_GB/email.json +++ b/public/language/en_GB/email.json @@ -2,6 +2,8 @@ "password-reset-requested": "Password Reset Requested - %1!", "welcome-to": "Welcome to %1", + "invite": "Invitation from %1", + "greeting_no_name": "Hello", "greeting_with_name": "Hello %1", @@ -10,6 +12,9 @@ "welcome.text3": "An administrator has accepted your registration application. You can login with your username/password now.", "welcome.cta": "Click here to confirm your email address", + "invitation.text1": "%1 has invited you to join %2", + "invitation.ctr": "Click here to create your account.", + "reset.text1": "We received a request to reset your password, possibly because you have forgotten it. If this is not the case, please ignore this email.", "reset.text2": "To continue with the password reset, please click on the following link:", "reset.cta": "Click here to reset your password", diff --git a/public/language/en_GB/users.json b/public/language/en_GB/users.json index 0f3687c9ed..4b6cb8b1c3 100644 --- a/public/language/en_GB/users.json +++ b/public/language/en_GB/users.json @@ -8,5 +8,7 @@ "users-found-search-took": "%1 user(s) found! Search took %2 seconds.", "filter-by": "Filter By", "online-only": "Online only", - "picture-only": "Picture only" + "picture-only": "Picture only", + "invite": "Invite", + "invitation-email-sent": "An invitation email has been sent to %1" } \ No newline at end of file diff --git a/public/src/client/register.js b/public/src/client/register.js index a1c6955b5c..cca8617972 100644 --- a/public/src/client/register.js +++ b/public/src/client/register.js @@ -24,6 +24,12 @@ define('forum/register', ['csrf', 'translator'], function(csrf, translator) { } }); + var query = utils.params(); + if (query.email && query.token) { + email.val(query.email); + $('#token').val(query.token); + } + // Update the "others can mention you via" text username.on('keyup', function() { $('#yourUsername').text(this.value.length > 0 ? utils.slugify(this.value) : 'username'); diff --git a/public/src/client/users.js b/public/src/client/users.js index 9f440470b6..35e56dc30a 100644 --- a/public/src/client/users.js +++ b/public/src/client/users.js @@ -22,6 +22,8 @@ define('forum/users', ['translator'], function(translator) { handleSearch(); + handleInvite(); + socket.removeListener('event:user_status_change', onUserStatusChange); socket.on('event:user_status_change', onUserStatusChange); @@ -200,5 +202,22 @@ define('forum/users', ['translator'], function(translator) { return parts[parts.length - 1]; } + function handleInvite() { + $('[component="user/invite"]').on('click', function() { + bootbox.prompt('Email: ', function(email) { + if (!email) { + return; + } + + socket.emit('user.invite', email, function(err) { + if (err) { + return app.alertError(err.message); + } + app.alertSuccess('[[users:invitation-email-sent, ' + email + ']]'); + }); + }); + }); + } + return Users; }); diff --git a/src/controllers/authentication.js b/src/controllers/authentication.js index b7f03d89b7..c303fb825d 100644 --- a/src/controllers/authentication.js +++ b/src/controllers/authentication.js @@ -33,6 +33,14 @@ authenticationController.register = function(req, res, next) { var uid; async.waterfall([ function(next) { + if (registrationType === 'invite-only') { + user.verifyInvitation(userData, next); + } else { + next(); + } + }, + function(next) { + console.log(userData); if (!userData.email) { return next(new Error('[[error:invalid-email]]')); } @@ -55,7 +63,7 @@ authenticationController.register = function(req, res, next) { plugins.fireHook('filter:register.check', {req: req, res: res, userData: userData}, next); }, function(data, next) { - if (registrationType === 'normal') { + if (registrationType === 'normal' || registrationType === 'invite-only') { registerAndLoginUser(req, res, userData, next); } else if (registrationType === 'admin-approval') { addToApprovalQueue(req, res, userData, next); @@ -83,6 +91,8 @@ function registerAndLoginUser(req, res, userData, callback) { function(next) { user.logIP(uid, req.ip); + user.deleteInvitation(userData.email); + user.notifications.sendWelcomeNotification(uid); plugins.fireHook('filter:register.complete', {uid: uid, referrer: req.body.referrer || nconf.get('relative_path') + '/'}, next); diff --git a/src/controllers/index.js b/src/controllers/index.js index 4d24ab0558..ae6cb5c497 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -98,34 +98,35 @@ Controllers.register = function(req, res, next) { return helpers.notFound(req, res); } - var data = {}, - loginStrategies = require('../routes/authentication').getLoginStrategies(); - - if (loginStrategies.length === 0) { - data = { - 'register_window:spansize': 'col-md-12', - 'alternate_logins': false - }; - } else { - data = { - 'register_window:spansize': 'col-md-6', - 'alternate_logins': true - }; - } - - data.authentication = loginStrategies; - - data.minimumUsernameLength = meta.config.minimumUsernameLength; - data.maximumUsernameLength = meta.config.maximumUsernameLength; - data.minimumPasswordLength = meta.config.minimumPasswordLength; - data.termsOfUse = meta.config.termsOfUse; - data.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[register:register]]'}]); - data.regFormEntry = []; - data.error = req.flash('error')[0]; - - plugins.fireHook('filter:register.build', {req: req, res: res, templateData: data}, function(err, data) { - if (err && global.env === 'development') { - winston.warn(JSON.stringify(err)); + async.waterfall([ + function(next) { + if (registrationType === 'invite-only') { + user.verifyInvitation(req.query, next); + } else { + next(); + } + }, + function(next) { + var loginStrategies = require('../routes/authentication').getLoginStrategies(); + var data = { + 'register_window:spansize': loginStrategies.length ? 'col-md-6' : 'col-md-12', + 'alternate_logins': !!loginStrategies.length + }; + + data.authentication = loginStrategies; + + data.minimumUsernameLength = meta.config.minimumUsernameLength; + data.maximumUsernameLength = meta.config.maximumUsernameLength; + data.minimumPasswordLength = meta.config.minimumPasswordLength; + data.termsOfUse = meta.config.termsOfUse; + data.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[register:register]]'}]); + data.regFormEntry = []; + data.error = req.flash('error')[0]; + + plugins.fireHook('filter:register.build', {req: req, res: res, templateData: data}, next); + } + ], function(err, data) { + if (err) { return next(err); } res.render('register', data.templateData); diff --git a/src/controllers/users.js b/src/controllers/users.js index 1c15da5d49..e2298ade29 100644 --- a/src/controllers/users.js +++ b/src/controllers/users.js @@ -122,6 +122,7 @@ function render(req, res, data, next) { if (err) { return next(err); } + data.templateData.inviteOnly = meta.config.registrationType === 'invite-only'; res.render('users', data.templateData); }); } diff --git a/src/emailer.js b/src/emailer.js index a7a0b67302..a4f2c407bd 100644 --- a/src/emailer.js +++ b/src/emailer.js @@ -20,48 +20,62 @@ var fs = require('fs'), }; Emailer.send = function(template, uid, params, callback) { - if (!callback) { callback = function() {}; } + callback = callback || function() {}; if (!app) { winston.warn('[emailer] App not ready!'); return callback(); } + async.waterfall([ + function(next) { + async.parallel({ + email: async.apply(User.getUserField, uid, 'email'), + settings: async.apply(User.getSettings, uid) + }, next); + }, + function(results, next) { + if (!results.email) { + winston.warn('uid : ' + uid + ' has no email, not sending.'); + return next(); + } + params.uid = uid; + Emailer.sendToEmail(template, results.email, results.settings.userLang, params, next); + } + ], callback); + }; + + Emailer.sendToEmail = function(template, email, language, params, callback) { + callback = callback || function() {}; async.parallel({ html: function(next) { app.render('emails/' + template, params, next); }, plaintext: function(next) { app.render('emails/' + template + '_plaintext', params, next); - }, - email: async.apply(User.getUserField, uid, 'email'), - settings: async.apply(User.getSettings, uid) + } }, function(err, results) { if (err) { winston.error('[emailer] Error sending digest : ' + err.stack); return callback(err); } async.map([results.html, results.plaintext, params.subject], function(raw, next) { - translator.translate(raw, results.settings.userLang || meta.config.defaultLang || 'en_GB', function(translated) { + translator.translate(raw, language || meta.config.defaultLang || 'en_GB', function(translated) { next(undefined, translated); }); }, function(err, translated) { if (err) { - winston.error(err.message); return callback(err); - } else if (!results.email) { - winston.warn('uid : ' + uid + ' has no email, not sending.'); - return callback(); } if (Plugins.hasListeners('action:email.send')) { Plugins.fireHook('action:email.send', { - to: results.email, + to: email, from: meta.config['email:from'] || 'no-reply@localhost.lan', subject: translated[2], html: translated[0], plaintext: translated[1], template: template, - uid: uid, + uid: params.uid, pid: params.pid, fromUid: params.fromUid }); @@ -72,6 +86,10 @@ var fs = require('fs'), } }); }); + }; + + + }(module.exports)); diff --git a/src/socket.io/user.js b/src/socket.io/user.js index 4077685e79..6621c50242 100644 --- a/src/socket.io/user.js +++ b/src/socket.io/user.js @@ -495,6 +495,17 @@ SocketUser.setStatus = function(socket, status, callback) { }); }; -/* Exports */ +SocketUser.invite = function(socket, email, callback) { + if (!email || !socket.uid) { + return callback(new Error('[[error:invald-data]]')); + } + + if (meta.config.registrationType !== 'invite-only') { + return callback(new Error('[[error:forum-not-invite-only]]')); + } + + user.sendInvitationEmail(socket.uid, email, callback); +}; + module.exports = SocketUser; diff --git a/src/user.js b/src/user.js index df5bccd66c..7661146a14 100644 --- a/src/user.js +++ b/src/user.js @@ -31,6 +31,7 @@ var async = require('async'), require('./user/jobs')(User); require('./user/picture')(User); require('./user/approval')(User); + require('./user/invite')(User); User.getUserField = function(uid, field, callback) { User.getUserFields(uid, [field], function(err, user) { @@ -509,3 +510,4 @@ var async = require('async'), }(exports)); + diff --git a/src/user/invite.js b/src/user/invite.js new file mode 100644 index 0000000000..9ea271b397 --- /dev/null +++ b/src/user/invite.js @@ -0,0 +1,81 @@ + +'use strict'; + +var async = require('async'), + nconf = require('nconf'), + winston = require('winston'), + db = require('./../database'), + + meta = require('../meta'), + emailer = require('../emailer'), + + plugins = require('../plugins'), + translator = require('../../public/src/modules/translator'), + utils = require('../../public/src/utils'); + + +module.exports = function(User) { + + User.sendInvitationEmail = function(uid, email, callback) { + callback = callback || function() {}; + var token = utils.generateUUID(); + var registerLink = nconf.get('url') + '/register?token=' + token + '&email=' + email; + + var oneDay = 86400000; + async.waterfall([ + function(next) { + db.set('invitation:email:' + email, token, next); + }, + function(next) { + db.pexpireAt('invitation:email:' + email, Date.now() + oneDay, next); + }, + function(next) { + User.getUserField(uid, 'username', next); + }, + function(username, next) { + var title = meta.config.title || meta.config.browserTitle || 'NodeBB'; + translator.translate('[[email:invite, ' + title + ']]', meta.config.defaultLang, function(subject) { + var data = { + site_title: title, + registerLink: registerLink, + subject: subject, + username: username, + template: 'invitation' + }; + + if (plugins.hasListeners('action:email.send')) { + emailer.sendToEmail('invitation', email, meta.config.defaultLang, data, next); + } else { + winston.warn('No emailer to send verification email!'); + next(); + } + }); + } + ], callback); + }; + + User.verifyInvitation = function(query, callback) { + if (!query.token || !query.email) { + return callback(new Error('[[error:invalid-data]]')); + } + + async.waterfall([ + function(next) { + db.get('invitation:email:' + query.email, next); + }, + function(token, next) { + if (!token || token !== query.token) { + return next(new Error('[[error:invalid-token]]')); + } + + next(); + } + ], callback); + }; + + User.deleteInvitation = function(email, callback) { + callback = callback || function() {}; + db.delete('invitation:email:' + email, callback); + }; + +}; \ No newline at end of file diff --git a/src/views/admin/settings/user.tpl b/src/views/admin/settings/user.tpl index 6a9804eb0e..f03c84d838 100644 --- a/src/views/admin/settings/user.tpl +++ b/src/views/admin/settings/user.tpl @@ -39,6 +39,7 @@ diff --git a/src/views/emails/invitation.tpl b/src/views/emails/invitation.tpl new file mode 100644 index 0000000000..a19543d084 --- /dev/null +++ b/src/views/emails/invitation.tpl @@ -0,0 +1,14 @@ +

[[email:greeting_no_name]],

+ +

+ [[email:invitation.text1, {username}, {site_title}]] +

+ +
+ [[email:invitation.ctr]] +
+ +

+ [[email:closing]]
+ {site_title} +

diff --git a/src/views/emails/invitation_plaintext.tpl b/src/views/emails/invitation_plaintext.tpl new file mode 100644 index 0000000000..64bb6c73e1 --- /dev/null +++ b/src/views/emails/invitation_plaintext.tpl @@ -0,0 +1,11 @@ +[[email:greeting_no_name]], + +[[email:invitation.text1, {username}, {site_title}]] + +[[email:invitation.ctr]] + + {registerLink} + +[[email:closing]] +{site_title} +