mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-11-03 20:45:58 +01:00
closes #3271
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
"password-reset-requested": "Password Reset Requested - %1!",
|
"password-reset-requested": "Password Reset Requested - %1!",
|
||||||
"welcome-to": "Welcome to %1",
|
"welcome-to": "Welcome to %1",
|
||||||
|
|
||||||
|
"invite": "Invitation from %1",
|
||||||
|
|
||||||
"greeting_no_name": "Hello",
|
"greeting_no_name": "Hello",
|
||||||
"greeting_with_name": "Hello %1",
|
"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.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",
|
"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.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.text2": "To continue with the password reset, please click on the following link:",
|
||||||
"reset.cta": "Click here to reset your password",
|
"reset.cta": "Click here to reset your password",
|
||||||
|
|||||||
@@ -8,5 +8,7 @@
|
|||||||
"users-found-search-took": "%1 user(s) found! Search took %2 seconds.",
|
"users-found-search-took": "%1 user(s) found! Search took %2 seconds.",
|
||||||
"filter-by": "Filter By",
|
"filter-by": "Filter By",
|
||||||
"online-only": "Online only",
|
"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"
|
||||||
}
|
}
|
||||||
@@ -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
|
// Update the "others can mention you via" text
|
||||||
username.on('keyup', function() {
|
username.on('keyup', function() {
|
||||||
$('#yourUsername').text(this.value.length > 0 ? utils.slugify(this.value) : 'username');
|
$('#yourUsername').text(this.value.length > 0 ? utils.slugify(this.value) : 'username');
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ define('forum/users', ['translator'], function(translator) {
|
|||||||
|
|
||||||
handleSearch();
|
handleSearch();
|
||||||
|
|
||||||
|
handleInvite();
|
||||||
|
|
||||||
socket.removeListener('event:user_status_change', onUserStatusChange);
|
socket.removeListener('event:user_status_change', onUserStatusChange);
|
||||||
socket.on('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];
|
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;
|
return Users;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,6 +33,14 @@ authenticationController.register = function(req, res, next) {
|
|||||||
var uid;
|
var uid;
|
||||||
async.waterfall([
|
async.waterfall([
|
||||||
function(next) {
|
function(next) {
|
||||||
|
if (registrationType === 'invite-only') {
|
||||||
|
user.verifyInvitation(userData, next);
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function(next) {
|
||||||
|
console.log(userData);
|
||||||
if (!userData.email) {
|
if (!userData.email) {
|
||||||
return next(new Error('[[error:invalid-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);
|
plugins.fireHook('filter:register.check', {req: req, res: res, userData: userData}, next);
|
||||||
},
|
},
|
||||||
function(data, next) {
|
function(data, next) {
|
||||||
if (registrationType === 'normal') {
|
if (registrationType === 'normal' || registrationType === 'invite-only') {
|
||||||
registerAndLoginUser(req, res, userData, next);
|
registerAndLoginUser(req, res, userData, next);
|
||||||
} else if (registrationType === 'admin-approval') {
|
} else if (registrationType === 'admin-approval') {
|
||||||
addToApprovalQueue(req, res, userData, next);
|
addToApprovalQueue(req, res, userData, next);
|
||||||
@@ -83,6 +91,8 @@ function registerAndLoginUser(req, res, userData, callback) {
|
|||||||
function(next) {
|
function(next) {
|
||||||
user.logIP(uid, req.ip);
|
user.logIP(uid, req.ip);
|
||||||
|
|
||||||
|
user.deleteInvitation(userData.email);
|
||||||
|
|
||||||
user.notifications.sendWelcomeNotification(uid);
|
user.notifications.sendWelcomeNotification(uid);
|
||||||
|
|
||||||
plugins.fireHook('filter:register.complete', {uid: uid, referrer: req.body.referrer || nconf.get('relative_path') + '/'}, next);
|
plugins.fireHook('filter:register.complete', {uid: uid, referrer: req.body.referrer || nconf.get('relative_path') + '/'}, next);
|
||||||
|
|||||||
@@ -98,20 +98,20 @@ Controllers.register = function(req, res, next) {
|
|||||||
return helpers.notFound(req, res);
|
return helpers.notFound(req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
var data = {},
|
async.waterfall([
|
||||||
loginStrategies = require('../routes/authentication').getLoginStrategies();
|
function(next) {
|
||||||
|
if (registrationType === 'invite-only') {
|
||||||
if (loginStrategies.length === 0) {
|
user.verifyInvitation(req.query, next);
|
||||||
data = {
|
|
||||||
'register_window:spansize': 'col-md-12',
|
|
||||||
'alternate_logins': false
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
data = {
|
next();
|
||||||
'register_window:spansize': 'col-md-6',
|
|
||||||
'alternate_logins': true
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
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.authentication = loginStrategies;
|
||||||
|
|
||||||
@@ -123,9 +123,10 @@ Controllers.register = function(req, res, next) {
|
|||||||
data.regFormEntry = [];
|
data.regFormEntry = [];
|
||||||
data.error = req.flash('error')[0];
|
data.error = req.flash('error')[0];
|
||||||
|
|
||||||
plugins.fireHook('filter:register.build', {req: req, res: res, templateData: data}, function(err, data) {
|
plugins.fireHook('filter:register.build', {req: req, res: res, templateData: data}, next);
|
||||||
if (err && global.env === 'development') {
|
}
|
||||||
winston.warn(JSON.stringify(err));
|
], function(err, data) {
|
||||||
|
if (err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
res.render('register', data.templateData);
|
res.render('register', data.templateData);
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ function render(req, res, data, next) {
|
|||||||
if (err) {
|
if (err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
|
data.templateData.inviteOnly = meta.config.registrationType === 'invite-only';
|
||||||
res.render('users', data.templateData);
|
res.render('users', data.templateData);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,48 +20,62 @@ var fs = require('fs'),
|
|||||||
};
|
};
|
||||||
|
|
||||||
Emailer.send = function(template, uid, params, callback) {
|
Emailer.send = function(template, uid, params, callback) {
|
||||||
if (!callback) { callback = function() {}; }
|
callback = callback || function() {};
|
||||||
if (!app) {
|
if (!app) {
|
||||||
winston.warn('[emailer] App not ready!');
|
winston.warn('[emailer] App not ready!');
|
||||||
return callback();
|
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({
|
async.parallel({
|
||||||
html: function(next) {
|
html: function(next) {
|
||||||
app.render('emails/' + template, params, next);
|
app.render('emails/' + template, params, next);
|
||||||
},
|
},
|
||||||
plaintext: function(next) {
|
plaintext: function(next) {
|
||||||
app.render('emails/' + template + '_plaintext', params, next);
|
app.render('emails/' + template + '_plaintext', params, next);
|
||||||
},
|
}
|
||||||
email: async.apply(User.getUserField, uid, 'email'),
|
|
||||||
settings: async.apply(User.getSettings, uid)
|
|
||||||
}, function(err, results) {
|
}, function(err, results) {
|
||||||
if (err) {
|
if (err) {
|
||||||
winston.error('[emailer] Error sending digest : ' + err.stack);
|
winston.error('[emailer] Error sending digest : ' + err.stack);
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
async.map([results.html, results.plaintext, params.subject], function(raw, next) {
|
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);
|
next(undefined, translated);
|
||||||
});
|
});
|
||||||
}, function(err, translated) {
|
}, function(err, translated) {
|
||||||
if (err) {
|
if (err) {
|
||||||
winston.error(err.message);
|
|
||||||
return callback(err);
|
return callback(err);
|
||||||
} else if (!results.email) {
|
|
||||||
winston.warn('uid : ' + uid + ' has no email, not sending.');
|
|
||||||
return callback();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Plugins.hasListeners('action:email.send')) {
|
if (Plugins.hasListeners('action:email.send')) {
|
||||||
Plugins.fireHook('action:email.send', {
|
Plugins.fireHook('action:email.send', {
|
||||||
to: results.email,
|
to: email,
|
||||||
from: meta.config['email:from'] || 'no-reply@localhost.lan',
|
from: meta.config['email:from'] || 'no-reply@localhost.lan',
|
||||||
subject: translated[2],
|
subject: translated[2],
|
||||||
html: translated[0],
|
html: translated[0],
|
||||||
plaintext: translated[1],
|
plaintext: translated[1],
|
||||||
template: template,
|
template: template,
|
||||||
uid: uid,
|
uid: params.uid,
|
||||||
pid: params.pid,
|
pid: params.pid,
|
||||||
fromUid: params.fromUid
|
fromUid: params.fromUid
|
||||||
});
|
});
|
||||||
@@ -72,6 +86,10 @@ var fs = require('fs'),
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}(module.exports));
|
}(module.exports));
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
module.exports = SocketUser;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ var async = require('async'),
|
|||||||
require('./user/jobs')(User);
|
require('./user/jobs')(User);
|
||||||
require('./user/picture')(User);
|
require('./user/picture')(User);
|
||||||
require('./user/approval')(User);
|
require('./user/approval')(User);
|
||||||
|
require('./user/invite')(User);
|
||||||
|
|
||||||
User.getUserField = function(uid, field, callback) {
|
User.getUserField = function(uid, field, callback) {
|
||||||
User.getUserFields(uid, [field], function(err, user) {
|
User.getUserFields(uid, [field], function(err, user) {
|
||||||
@@ -509,3 +510,4 @@ var async = require('async'),
|
|||||||
|
|
||||||
|
|
||||||
}(exports));
|
}(exports));
|
||||||
|
|
||||||
|
|||||||
81
src/user/invite.js
Normal file
81
src/user/invite.js
Normal file
@@ -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);
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
<select class="form-control" data-field="registrationType">
|
<select class="form-control" data-field="registrationType">
|
||||||
<option value="normal">Normal</option>
|
<option value="normal">Normal</option>
|
||||||
<option value="admin-approval">Admin Approval</option>
|
<option value="admin-approval">Admin Approval</option>
|
||||||
|
<option value="invite-only">Invite Only</option>
|
||||||
<option value="disabled">No registration</option>
|
<option value="disabled">No registration</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
14
src/views/emails/invitation.tpl
Normal file
14
src/views/emails/invitation.tpl
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<p>[[email:greeting_no_name]],</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>[[email:invitation.text1, {username}, {site_title}]]</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<blockquote>
|
||||||
|
<a href="{registerLink}">[[email:invitation.ctr]]</a>
|
||||||
|
</blockquote>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
[[email:closing]]<br />
|
||||||
|
<strong>{site_title}</strong>
|
||||||
|
</p>
|
||||||
11
src/views/emails/invitation_plaintext.tpl
Normal file
11
src/views/emails/invitation_plaintext.tpl
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[[email:greeting_no_name]],
|
||||||
|
|
||||||
|
[[email:invitation.text1, {username}, {site_title}]]
|
||||||
|
|
||||||
|
[[email:invitation.ctr]]
|
||||||
|
|
||||||
|
{registerLink}
|
||||||
|
|
||||||
|
[[email:closing]]
|
||||||
|
{site_title}
|
||||||
|
|
||||||
Reference in New Issue
Block a user