diff --git a/install/data/defaults.json b/install/data/defaults.json index 4e5ebc035e..c8bcdb9008 100644 --- a/install/data/defaults.json +++ b/install/data/defaults.json @@ -9,7 +9,7 @@ "maximumPostLength": 32767, "allowGuestSearching": 0, "allowTopicsThumbnail": 0, - "allowRegistration": 1, + "registrationType": "normal", "allowLocalLogin": 1, "allowAccountDelete": 1, "allowFileUploads": 0, diff --git a/public/language/en_GB/email.json b/public/language/en_GB/email.json index 33fd28377b..87baa1854b 100644 --- a/public/language/en_GB/email.json +++ b/public/language/en_GB/email.json @@ -7,6 +7,7 @@ "welcome.text1": "Thank you for registering with %1!", "welcome.text2": "To fully activate your account, we need to verify that you own the email address you registered with.", + "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", "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.", diff --git a/public/language/en_GB/notifications.json b/public/language/en_GB/notifications.json index 19de8e2c28..1c256bebb4 100644 --- a/public/language/en_GB/notifications.json +++ b/public/language/en_GB/notifications.json @@ -22,6 +22,7 @@ "user_posted_topic": "%1 has posted a new topic: %2", "user_mentioned_you_in": "%1 mentioned you in %2", "user_started_following_you": "%1 started following you.", + "new_register": "%1 sent a registration request.", "email-confirmed": "Email Confirmed", "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.", diff --git a/public/language/en_GB/register.json b/public/language/en_GB/register.json index 26196c765d..dcbd4bb03a 100644 --- a/public/language/en_GB/register.json +++ b/public/language/en_GB/register.json @@ -14,5 +14,6 @@ "register_now_button": "Register Now", "alternative_registration": "Alternative Registration", "terms_of_use": "Terms of Use", - "agree_to_terms_of_use": "I agree to the Terms of Use" + "agree_to_terms_of_use": "I agree to the Terms of Use", + "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator." } \ No newline at end of file diff --git a/public/src/admin/manage/registration.js b/public/src/admin/manage/registration.js new file mode 100644 index 0000000000..0592fc02a9 --- /dev/null +++ b/public/src/admin/manage/registration.js @@ -0,0 +1,28 @@ +"use strict"; + +/* global config, socket, define, templates, bootbox, app, ajaxify, */ + +define('admin/manage/registration', function() { + var Registration = {}; + + Registration.init = function() { + + $('.users-list').on('click', '[data-action]', function(ev) { + var $this = this; + var parent = $(this).parents('[data-username]'); + var action = $(this).attr('data-action'); + var username = parent.attr('data-username'); + var method = action === 'accept' ? 'admin.user.acceptRegistration' : 'admin.user.rejectRegistration'; + + socket.emit(method, {username: username}, function(err) { + if (err) { + return app.alertError(err.message); + } + parent.remove(); + }); + return false; + }); + }; + + return Registration; +}); diff --git a/public/src/client/register.js b/public/src/client/register.js index 6955fcb989..a1c6955b5c 100644 --- a/public/src/client/register.js +++ b/public/src/client/register.js @@ -69,7 +69,15 @@ define('forum/register', ['csrf', 'translator'], function(csrf, translator) { 'x-csrf-token': csrf.get() }, success: function(data, status) { - window.location.href = data; + registerBtn.removeClass('disabled'); + if (!data) { + return; + } + if (data.referrer) { + window.location.href = data.referrer; + } else if (data.message) { + app.alert({message: data.message, timeout: 20000}); + } }, error: function(data, status) { var errorEl = $('#register-error-notify'); @@ -84,7 +92,7 @@ define('forum/register', ['csrf', 'translator'], function(csrf, translator) { }); }); - if(agreeTerms.length) { + if (agreeTerms.length) { agreeTerms.on('click', function() { if ($(this).prop('checked')) { register.removeAttr('disabled'); diff --git a/src/controllers/admin/users.js b/src/controllers/admin/users.js index 7f884b79ce..6c10694ba6 100644 --- a/src/controllers/admin/users.js +++ b/src/controllers/admin/users.js @@ -30,6 +30,15 @@ usersController.banned = function(req, res, next) { getUsers('users:banned', req, res, next); }; +usersController.registrationQueue = function(req, res, next) { + user.getRegistrationQueue(0, -1, function(err, data) { + if (err) { + return next(err); + } + res.render('admin/manage/registration', {users: data}); + }) +}; + function getUsers(set, req, res, next) { user.getUsersFromSet(set, req.uid, 0, 49, function(err, users) { if (err) { diff --git a/src/controllers/authentication.js b/src/controllers/authentication.js index a663d0e6c8..b7f03d89b7 100644 --- a/src/controllers/authentication.js +++ b/src/controllers/authentication.js @@ -16,7 +16,9 @@ var async = require('async'), authenticationController = {}; authenticationController.register = function(req, res, next) { - if (parseInt(meta.config.allowRegistration, 10) === 0) { + var registrationType = meta.config.registrationType || 'normal'; + + if (registrationType === 'disabled') { return res.sendStatus(403); } @@ -53,7 +55,26 @@ authenticationController.register = function(req, res, next) { plugins.fireHook('filter:register.check', {req: req, res: res, userData: userData}, next); }, function(data, next) { - user.create(data.userData, next); + if (registrationType === 'normal') { + registerAndLoginUser(req, res, userData, next); + } else if (registrationType === 'admin-approval') { + addToApprovalQueue(req, res, userData, next); + } + } + ], function(err, data) { + if (err) { + return res.status(400).send(err.message); + } + + res.json(data); + }); +}; + +function registerAndLoginUser(req, res, userData, callback) { + var uid; + async.waterfall([ + function(next) { + user.create(userData, next); }, function(_uid, next) { uid = _uid; @@ -64,16 +85,22 @@ authenticationController.register = function(req, res, next) { user.notifications.sendWelcomeNotification(uid); - plugins.fireHook('filter:register.complete', {uid: uid, referrer: req.body.referrer}, next); - } - ], function(err, data) { - if (err) { - return res.status(400).send(err.message); + plugins.fireHook('filter:register.complete', {uid: uid, referrer: req.body.referrer || nconf.get('relative_path') + '/'}, next); } + ], callback); +} - res.status(200).send(data.referrer ? data.referrer : nconf.get('relative_path') + '/'); - }); -}; +function addToApprovalQueue(req, res, userData, callback) { + async.waterfall([ + function(next) { + userData.ip = req.ip; + user.addToApprovalQueue(userData, next); + }, + function(next) { + next(null, {message: '[[register:registration-added-to-queue]]'}); + } + ], callback); +} authenticationController.login = function(req, res, next) { // Handle returnTo data diff --git a/src/controllers/index.js b/src/controllers/index.js index dd3e3ecd7d..4d24ab0558 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -77,11 +77,13 @@ Controllers.login = function(req, res, next) { loginStrategies = require('../routes/authentication').getLoginStrategies(), emailersPresent = plugins.hasListeners('action:email.send'); + var registrationType = meta.config.registrationType || 'normal'; + data.alternate_logins = loginStrategies.length > 0; data.authentication = loginStrategies; data.showResetLink = emailersPresent; data.allowLocalLogin = parseInt(meta.config.allowLocalLogin, 10) === 1 || parseInt(req.query.local, 10) === 1; - data.allowRegistration = parseInt(meta.config.allowRegistration, 10) === 1; + data.allowRegistration = registrationType === 'normal' || registrationType === 'admin-approval'; data.allowLoginWith = '[[login:' + (meta.config.allowLoginWith || 'username-email') + ']]'; data.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[global:login]]'}]); data.error = req.flash('error')[0]; @@ -90,7 +92,9 @@ Controllers.login = function(req, res, next) { }; Controllers.register = function(req, res, next) { - if (parseInt(meta.config.allowRegistration, 10) === 0) { + var registrationType = meta.config.registrationType || 'normal'; + + if (registrationType === 'disabled') { return helpers.notFound(req, res); } diff --git a/src/middleware/middleware.js b/src/middleware/middleware.js index 6943b2ca83..10d2385eb7 100644 --- a/src/middleware/middleware.js +++ b/src/middleware/middleware.js @@ -80,7 +80,7 @@ middleware.redirectToLoginIfGuest = function(req, res, next) { if (!req.user || parseInt(req.user.uid, 10) === 0) { return redirectToLogin(req, res); } - + next(); }; @@ -191,16 +191,17 @@ middleware.buildHeader = function(req, res, next) { }; middleware.renderHeader = function(req, res, callback) { + var registrationType = meta.config.registrationType || 'normal' var templateValues = { - bootswatchCSS: meta.config['theme:src'], - title: meta.config.title || '', - description: meta.config.description || '', - 'cache-buster': meta.config['cache-buster'] ? 'v=' + meta.config['cache-buster'] : '', - 'brand:logo': meta.config['brand:logo'] || '', - 'brand:logo:display': meta.config['brand:logo']?'':'hide', - allowRegistration: meta.config.allowRegistration === undefined || parseInt(meta.config.allowRegistration, 10) === 1, - searchEnabled: plugins.hasListeners('filter:search.query') - }; + bootswatchCSS: meta.config['theme:src'], + title: meta.config.title || '', + description: meta.config.description || '', + 'cache-buster': meta.config['cache-buster'] ? 'v=' + meta.config['cache-buster'] : '', + 'brand:logo': meta.config['brand:logo'] || '', + 'brand:logo:display': meta.config['brand:logo']?'':'hide', + allowRegistration: registrationType === 'normal' || registrationType === 'admin-approval', + searchEnabled: plugins.hasListeners('filter:search.query') + }; for (var key in res.locals.config) { if (res.locals.config.hasOwnProperty(key)) { diff --git a/src/routes/admin.js b/src/routes/admin.js index 91090c2b69..40bb725e89 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -58,6 +58,7 @@ function addRoutes(router, middleware, controllers) { router.get('/manage/users/sort-posts', controllers.admin.users.sortByPosts); router.get('/manage/users/sort-reputation', controllers.admin.users.sortByReputation); router.get('/manage/users/banned', controllers.admin.users.banned); + router.get('/manage/users/registration', controllers.admin.users.registrationQueue); router.get('/manage/groups', controllers.admin.groups.list); router.get('/manage/groups/:name', controllers.admin.groups.get); diff --git a/src/socket.io/admin/user.js b/src/socket.io/admin/user.js index 4625226c2b..b48746b0a1 100644 --- a/src/socket.io/admin/user.js +++ b/src/socket.io/admin/user.js @@ -241,4 +241,13 @@ User.search = function(socket, data, callback) { }); }; +User.acceptRegistration = function(socket, data, callback) { + user.acceptRegistration(data.username, callback); +}; + +User.rejectRegistration = function(socket, data, callback) { + user.rejectRegistration(data.username, callback); +}; + + module.exports = User; \ No newline at end of file diff --git a/src/user.js b/src/user.js index 6c9ce24ad0..df5bccd66c 100644 --- a/src/user.js +++ b/src/user.js @@ -30,6 +30,7 @@ var async = require('async'), require('./user/search')(User); require('./user/jobs')(User); require('./user/picture')(User); + require('./user/approval')(User); User.getUserField = function(uid, field, callback) { User.getUserFields(uid, [field], function(err, user) { diff --git a/src/user/approval.js b/src/user/approval.js new file mode 100644 index 0000000000..5c571ed6c3 --- /dev/null +++ b/src/user/approval.js @@ -0,0 +1,139 @@ + +'use strict'; + +var async = require('async'), + nconf = require('nconf'), + db = require('./../database'), + meta = require('../meta'), + emailer = require('../emailer'), + notifications = require('../notifications'), + translator = require('../../public/src/modules/translator'), + utils = require('../../public/src/utils'); + + +module.exports = function(User) { + + User.addToApprovalQueue = function(userData, callback) { + userData.userslug = utils.slugify(userData.username); + async.waterfall([ + function(next) { + User.isDataValid(userData, next); + }, + function(next) { + User.hashPassword(userData.password, next); + }, + function(hashedPassword, next) { + var data = { + username: userData.username, + email: userData.email, + ip: userData.ip, + hashedPassword: hashedPassword + }; + + db.setObject('registration:queue:name:' + userData.username, data, next); + }, + function(next) { + db.sortedSetAdd('registration:queue', Date.now(), userData.username, next); + }, + function(next) { + sendNotificationToAdmins(userData.username, next); + } + ], callback); + }; + + function sendNotificationToAdmins(username, callback) { + notifications.create({ + bodyShort: '[[notifications:new_register, ' + username + ']]', + nid: 'new_register' + username, + path: '/admin/manage/users/registration' + }, function(err, notification) { + if (err) { + return callback(err); + } + if (notification) { + notifications.pushGroup(notification, 'administrators', callback); + } else { + callback(); + } + }); + } + + User.acceptRegistration = function(username, callback) { + var uid; + var userData; + async.waterfall([ + function(next) { + db.getObject('registration:queue:name:' + username, next); + }, + function(_userData, next) { + if (!_userData) { + return callback(new Error('[[error:invalid-data]]')); + } + userData = _userData; + User.create(userData, next); + }, + function(_uid, next) { + uid = _uid; + User.setUserField(uid, 'password', userData.hashedPassword, next); + }, + function(next) { + var title = meta.config.title || meta.config.browserTitle || 'NodeBB'; + translator.translate('[[email:welcome-to, ' + title + ']]', meta.config.defaultLang, function(subject) { + var data = { + site_title: title, + username: username, + subject: subject, + template: 'registration_accepted', + uid: uid + }; + + emailer.send('registration_accepted', uid, data, next); + }); + }, + function(next) { + User.notifications.sendWelcomeNotification(uid, next); + }, + function(next) { + removeFromQueue(username, next); + } + ], callback); + }; + + User.rejectRegistration = function(username, callback) { + removeFromQueue(username, callback); + }; + + function removeFromQueue(username, callback) { + async.parallel([ + async.apply(db.sortedSetRemove, 'registration:queue', username), + async.apply(db.delete, 'registration:queue:name:' + username) + ], callback); + } + + User.getRegistrationQueue = function(start, stop, callback) { + var data; + async.waterfall([ + function(next) { + db.getSortedSetRevRangeWithScores('registration:queue', start, stop, next); + }, + function(_data, next) { + data = _data; + var keys = data.filter(Boolean).map(function(user) { + return 'registration:queue:name:' + user.value; + }); + db.getObjects(keys, next); + }, + function(users, next) { + users.forEach(function(user, index) { + if (user) { + user.timestamp = utils.toISOString(data[index].score); + } + }); + + next(null, users); + } + ], callback); + }; + + +}; diff --git a/src/user/create.js b/src/user/create.js index a9d22ac3c6..db09c11b97 100644 --- a/src/user/create.js +++ b/src/user/create.js @@ -20,7 +20,7 @@ module.exports = function(User) { data.email = validator.escape(data.email.trim()); } - isDataValid(data, function(err) { + User.isDataValid(data, function(err) { if (err) { return callback(err); } @@ -155,7 +155,7 @@ module.exports = function(User) { }); }; - function isDataValid(userData, callback) { + User.isDataValid = function(userData, callback) { async.parallel({ emailValid: function(next) { if (userData.email) { @@ -186,8 +186,10 @@ module.exports = function(User) { next(); } } - }, callback); - } + }, function(err, results) { + callback(err); + }); + }; function renameUsername(userData, callback) { meta.userOrGroupExists(userData.userslug, function(err, exists) { diff --git a/src/user/notifications.js b/src/user/notifications.js index b92df0ce68..9ee63b3087 100644 --- a/src/user/notifications.js +++ b/src/user/notifications.js @@ -289,9 +289,10 @@ var async = require('async'), }); }; - UserNotifications.sendWelcomeNotification = function(uid) { + UserNotifications.sendWelcomeNotification = function(uid, callback) { + callback = callback || function() {}; if (!meta.config.welcomeNotification) { - return; + return callback(); } var path = meta.config.welcomeLink ? meta.config.welcomeLink : '#'; @@ -301,8 +302,13 @@ var async = require('async'), path: path, nid: 'welcome_' + uid }, function(err, notification) { - if (!err && notification) { - notifications.push(notification, [uid]); + if (err) { + return callback(err); + } + if (notification) { + notifications.push(notification, [uid], callback); + } else { + callback(); } }); }; diff --git a/src/views/admin/manage/registration.tpl b/src/views/admin/manage/registration.tpl new file mode 100644 index 0000000000..4cb08b8681 --- /dev/null +++ b/src/views/admin/manage/registration.tpl @@ -0,0 +1,40 @@ +
+
+
+
Registration Queue
+
+ + + + + + + + + + + + + + + + + +
NameEmailIPTime
+ {users.username} + + {users.email} + + {users.ip} + + + +
+ + +
+
+
+
+
+
\ No newline at end of file diff --git a/src/views/admin/manage/users.tpl b/src/views/admin/manage/users.tpl index e8dda5de23..8b60cd2b0a 100644 --- a/src/views/admin/manage/users.tpl +++ b/src/views/admin/manage/users.tpl @@ -8,6 +8,7 @@
  • Top Posters
  • Most Reputation
  • Banned
  • +
  • Registration Queue
  • User Search
  • diff --git a/src/views/admin/settings/user.tpl b/src/views/admin/settings/user.tpl index 94c3b536fc..6a9804eb0e 100644 --- a/src/views/admin/settings/user.tpl +++ b/src/views/admin/settings/user.tpl @@ -4,11 +4,6 @@
    User List
    -
    - -
    + +
    + + +
    diff --git a/src/views/emails/registration_accepted.tpl b/src/views/emails/registration_accepted.tpl new file mode 100644 index 0000000000..5c978aa22a --- /dev/null +++ b/src/views/emails/registration_accepted.tpl @@ -0,0 +1,12 @@ +

    [[email:greeting_with_name, {username}]],

    + +

    + [[email:welcome.text1, {site_title}]] +

    + +

    [[email:welcome.text3]]

    + +

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

    diff --git a/src/views/emails/registration_accepted_plaintext.tpl b/src/views/emails/registration_accepted_plaintext.tpl new file mode 100644 index 0000000000..a1bacf0f9f --- /dev/null +++ b/src/views/emails/registration_accepted_plaintext.tpl @@ -0,0 +1,9 @@ +[[email:greeting_with_name, {username}]], + +[[email:welcome.text1, {site_title}]] + +[[email:welcome.text3]] + + +[[email:closing]] +{site_title} \ No newline at end of file