diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 46c69fa5645b..cefbcf224ac4 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -48,10 +48,19 @@ "Accounts_denyUnverifiedEmail": "Deny unverified email", "Accounts_EmailVerification": "Email Verification", "Accounts_EmailVerification_Description": "Make sure you have correct SMTP settings to use this feature", + "Accounts_Email_Approved": "[name]

Your account was approved.

", + "Accounts_Email_Activated": "[name]

Your account was activated.

", + "Accounts_Email_Deactivated": "[name]

Your account was deactivated.

", + "Accounts_Email_Approved_Subject": "Account approved", + "Accounts_Email_Activated_Subject": "Account activated", + "Accounts_Email_Deactivated_Subject": "Account deactivated", "Accounts_Enrollment_Email": "Enrollment Email", "Accounts_Enrollment_Email_Default": "

Welcome to

[Site_Name]

Go to [Site_URL] and try the best open source chat solution available today!

", "Accounts_Enrollment_Email_Description": "You may use the following placeholders:
", "Accounts_Enrollment_Email_Subject_Default": "Welcome to [Site_Name]", + "Accounts_Admin_Email_Approval_Needed_Default": "

The user [name] ([email]) has been registered.

Please check \"Administration -> Users\" to activate or delete it.

", + "Accounts_Admin_Email_Approval_Needed_With_Reason_Default": "

The user [name] ([email]) has been registered.

Reason: [reason]

Please check \"Administration -> Users\" to activate or delete it.

", + "Accounts_Admin_Email_Approval_Needed_Subject_Default": "A new user registered and needs approval", "Accounts_ForgetUserSessionOnWindowClose": "Forget User Session on Window Close", "Accounts_Iframe_api_method": "Api Method", "Accounts_Iframe_api_url": "API URL", @@ -938,6 +947,7 @@ "Invalid_name": "The name must not be empty", "Invalid_notification_setting_s": "Invalid notification setting: %s", "Invalid_pass": "The password must not be empty", + "Invalid_reason": "The reason to join must not be empty", "Invalid_room_name": "%s is not a valid room name", "Invalid_secret_URL_message": "The URL provided is invalid.", "Invalid_setting_s": "Invalid setting: %s", @@ -1528,6 +1538,7 @@ "Read_only_changed_successfully": "Read only changed successfully", "Read_only_channel": "Read Only Channel", "Read_only_group": "Read Only Group", + "Reason_To_Join": "Reason to Join", "RealName_Change_Disabled": "Your Rocket.Chat administrator has disabled the changing of names", "Receive_alerts": "Receive alerts", "Record": "Record", diff --git a/packages/rocketchat-lib/lib/placeholders.js b/packages/rocketchat-lib/lib/placeholders.js index 8c3907b567fa..45f996e86173 100644 --- a/packages/rocketchat-lib/lib/placeholders.js +++ b/packages/rocketchat-lib/lib/placeholders.js @@ -16,6 +16,7 @@ RocketChat.placeholders.replace = function(str, data) { str = str.replace(/\[lname\]/g, s.strRightBack(data.name, ' ') || ''); str = str.replace(/\[email\]/g, data.email || ''); str = str.replace(/\[password\]/g, data.password || ''); + str = str.replace(/\[reason\]/g, data.reason || ''); str = str.replace(/\[User\]/g, data.user || ''); str = str.replace(/\[Room\]/g, data.room || ''); diff --git a/packages/rocketchat-lib/server/functions/getFullUserData.js b/packages/rocketchat-lib/server/functions/getFullUserData.js index 7bc9c9893fbf..056c4643d90f 100644 --- a/packages/rocketchat-lib/server/functions/getFullUserData.js +++ b/packages/rocketchat-lib/server/functions/getFullUserData.js @@ -9,7 +9,8 @@ RocketChat.getFullUserData = function({userId, filter, limit}) { status: 1, utcOffset: 1, type: 1, - active: 1 + active: 1, + reason: 1 }; if (RocketChat.authz.hasPermission(userId, 'view-full-other-user-info')) { diff --git a/packages/rocketchat-lib/server/models/Users.js b/packages/rocketchat-lib/server/models/Users.js index 5381c8f7ce4d..e81af4599ff6 100644 --- a/packages/rocketchat-lib/server/models/Users.js +++ b/packages/rocketchat-lib/server/models/Users.js @@ -507,6 +507,26 @@ class ModelUsers extends RocketChat.models._Base { return this.update({ _id }, update); } + setReason(_id, reason) { + const update = { + $set: { + reason + } + }; + + return this.update(_id, update); + } + + unsetReason(_id) { + const update = { + $unset: { + reason: true + } + }; + + return this.update(_id, update); + } + // INSERT create(data) { const user = { diff --git a/packages/rocketchat-lib/server/startup/settings.js b/packages/rocketchat-lib/server/startup/settings.js index 8e21ccd78594..dbda0e31d271 100644 --- a/packages/rocketchat-lib/server/startup/settings.js +++ b/packages/rocketchat-lib/server/startup/settings.js @@ -107,6 +107,7 @@ RocketChat.settings.addGroup('Accounts', function() { } }); this.add('Accounts_ManuallyApproveNewUsers', false, { + 'public': true, type: 'boolean' }); this.add('Accounts_AllowedDomainsList', '', { diff --git a/packages/rocketchat-ui-flextab/client/tabs/userInfo.html b/packages/rocketchat-ui-flextab/client/tabs/userInfo.html index 7a2a4378e3a0..f7237d1d399e 100644 --- a/packages/rocketchat-ui-flextab/client/tabs/userInfo.html +++ b/packages/rocketchat-ui-flextab/client/tabs/userInfo.html @@ -65,7 +65,7 @@

{{phoneNumber}} {{/each}} {{/if}} - {{#if lastLogin}} + {{#if createdAt}}
@@ -75,6 +75,12 @@
{{/if}} + {{#if shouldDisplayReason}} +
+ + +
+ {{/if}} {{/if}} {{#if utc}}
diff --git a/packages/rocketchat-ui-flextab/client/tabs/userInfo.js b/packages/rocketchat-ui-flextab/client/tabs/userInfo.js index 92f1bbcd023b..e53b75943255 100644 --- a/packages/rocketchat-ui-flextab/client/tabs/userInfo.js +++ b/packages/rocketchat-ui-flextab/client/tabs/userInfo.js @@ -155,6 +155,11 @@ Template.userInfo.helpers({ const roomRoles = RoomRoles.findOne({'u._id': user._id, rid: Session.get('openedRoom') }) || {}; const roles = _.union(userRoles.roles || [], roomRoles.roles || []); return roles.length && RocketChat.models.Roles.find({ _id: { $in: roles }, description: { $exists: 1 } }, { fields: { description: 1 } }); + }, + + shouldDisplayReason() { + const user = Template.instance().user.get(); + return RocketChat.settings.get('Accounts_ManuallyApproveNewUsers') && user.active === false && user.reason; } }); /* globals isRtl popover */ diff --git a/packages/rocketchat-ui-login/client/login/form.html b/packages/rocketchat-ui-login/client/login/form.html index 6ac86a292ea4..ef5277bf675c 100644 --- a/packages/rocketchat-ui-login/client/login/form.html +++ b/packages/rocketchat-ui-login/client/login/form.html @@ -28,11 +28,11 @@

{{{_ "Registration_Succeeded"}}}

autocapitalize="off" autocorrect="off" placeholder="{{emailOrUsernamePlaceholder}}" autofocus>
-
- - {{#if hasOnePassword}} -
- {{/if}} + + + {{#if hasOnePassword}} +
+ {{/if}}
- +
+ + {{/if}} {{#if state 'register'}} @@ -58,10 +58,10 @@

{{{_ "Registration_Succeeded"}}}

+
+
+ {{#if requirePasswordConfirmation}}
@@ -86,10 +86,22 @@

{{{_ "Registration_Succeeded"}}}

+
+
+ + + {{/if}} + {{#if manuallyApproveNewUsers}} +
+ -
+ {{/if}} {{/if}} {{#if state 'forgot-password' 'email-verification'}} diff --git a/packages/rocketchat-ui-login/client/login/form.js b/packages/rocketchat-ui-login/client/login/form.js index 5a97a2a8dd6d..96eb5b70fdc7 100644 --- a/packages/rocketchat-ui-login/client/login/form.js +++ b/packages/rocketchat-ui-login/client/login/form.js @@ -60,6 +60,9 @@ Template.loginForm.helpers({ }, hasOnePassword() { return typeof OnePassword !== 'undefined' && OnePassword.findLoginForUrl && typeof device !== 'undefined' && device.platform && device.platform.toLocaleLowerCase() === 'ios'; + }, + manuallyApproveNewUsers() { + return RocketChat.settings.get('Accounts_ManuallyApproveNewUsers'); } }); @@ -252,6 +255,9 @@ Template.loginForm.onCreated(function() { if (RocketChat.settings.get('Accounts_RequirePasswordConfirmation') && formObj['confirm-pass'] !== formObj['pass']) { validationObj['confirm-pass'] = t('Invalid_confirm_pass'); } + if (RocketChat.settings.get('Accounts_ManuallyApproveNewUsers') && !formObj['reason']) { + validationObj['reason'] = t('Invalid_reason'); + } validateCustomFields(formObj, validationObj); } $('#login-card h2').removeClass('error'); diff --git a/server/lib/accounts.js b/server/lib/accounts.js index 40d433672c1c..946b0b0f67c1 100644 --- a/server/lib/accounts.js +++ b/server/lib/accounts.js @@ -12,6 +12,54 @@ Accounts.emailTemplates.siteName = RocketChat.settings.get('Site_Name'); Accounts.emailTemplates.from = `${ RocketChat.settings.get('Site_Name') } <${ RocketChat.settings.get('From_Email') }>`; +Accounts.emailTemplates.userToActivate = { + subject() { + const subject = TAPi18n.__('Accounts_Admin_Email_Approval_Needed_Subject_Default'); + const siteName = RocketChat.settings.get('Site_Name'); + + return `[${ siteName }] ${ subject }`; + }, + + html(options = {}) { + const header = RocketChat.placeholders.replace(RocketChat.settings.get('Email_Header') || ''); + const footer = RocketChat.placeholders.replace(RocketChat.settings.get('Email_Footer') || ''); + + const email = options.reason ? 'Accounts_Admin_Email_Approval_Needed_With_Reason_Default' : 'Accounts_Admin_Email_Approval_Needed_Default'; + + const html = RocketChat.placeholders.replace(TAPi18n.__(email), { + name: options.name, + email: options.email, + reason: options.reason + }); + + return header + html + footer; + } +}; + +Accounts.emailTemplates.userActivated = { + subject({active, username}) { + const action = active ? (username ? 'Activated' : 'Approved') : 'Deactivated'; + const subject = `Accounts_Email_${ action }_Subject`; + const siteName = RocketChat.settings.get('Site_Name'); + + return `[${ siteName }] ${ TAPi18n.__(subject) }`; + }, + + html({active, name, username}) { + const header = RocketChat.placeholders.replace(RocketChat.settings.get('Email_Header') || ''); + const footer = RocketChat.placeholders.replace(RocketChat.settings.get('Email_Footer') || ''); + + const action = active ? (username ? 'Activated' : 'Approved') : 'Deactivated'; + + const html = RocketChat.placeholders.replace(TAPi18n.__(`Accounts_Email_${ action }`), { + name + }); + + return header + html + footer; + } +}; + + const verifyEmailHtml = Accounts.emailTemplates.verifyEmail.text; Accounts.emailTemplates.verifyEmail.html = function(user, url) { @@ -94,6 +142,27 @@ Accounts.onCreateUser(function(options, user = {}) { } } + if (!user.active) { + const destinations = []; + + RocketChat.models.Roles.findUsersInRole('admin').forEach(adminUser => { + if (Array.isArray(adminUser.emails)) { + adminUser.emails.forEach(email => { + destinations.push(`${ adminUser.name }<${ email.address }>`); + }); + } + }); + + const email = { + to: destinations, + from: RocketChat.settings.get('From_Email'), + subject: Accounts.emailTemplates.userToActivate.subject(), + html: Accounts.emailTemplates.userToActivate.html(options) + }; + + Meteor.defer(() => Email.send(email)); + } + return user; }); diff --git a/server/methods/registerUser.js b/server/methods/registerUser.js index e2371beaeed9..392a2f794a5f 100644 --- a/server/methods/registerUser.js +++ b/server/methods/registerUser.js @@ -4,6 +4,7 @@ Meteor.methods({ registerUser(formData) { const AllowAnonymousRead = RocketChat.settings.get('Accounts_AllowAnonymousRead'); const AllowAnonymousWrite = RocketChat.settings.get('Accounts_AllowAnonymousWrite'); + const manuallyApproveNewUsers = RocketChat.settings.get('Accounts_ManuallyApproveNewUsers'); if (AllowAnonymousRead === true && AllowAnonymousWrite === true && formData.email == null) { const userId = Accounts.insertUserDoc({}, { globalRoles: [ @@ -19,7 +20,8 @@ Meteor.methods({ email: String, pass: String, name: String, - secretURL: Match.Optional(String) + secretURL: Match.Optional(String), + reason: Match.Optional(String) })); } @@ -33,7 +35,9 @@ Meteor.methods({ const userData = { email: s.trim(formData.email.toLowerCase()), - password: formData.pass + password: formData.pass, + name: formData.name, + reason: formData.reason }; // Check if user has already been imported and never logged in. If so, set password and let it through @@ -48,6 +52,11 @@ Meteor.methods({ RocketChat.models.Users.setName(userId, s.trim(formData.name)); + const reason = s.trim(formData.reason); + if (manuallyApproveNewUsers && reason) { + RocketChat.models.Users.setReason(userId, reason); + } + RocketChat.saveCustomFields(userId, formData); try { diff --git a/server/methods/setUserActiveStatus.js b/server/methods/setUserActiveStatus.js index f4a2eea9d1ea..58607a653468 100644 --- a/server/methods/setUserActiveStatus.js +++ b/server/methods/setUserActiveStatus.js @@ -26,6 +26,21 @@ Meteor.methods({ if (active === false) { RocketChat.models.Users.unsetLoginTokens(userId); + } else { + RocketChat.models.Users.unsetReason(userId); + } + + const destinations = Array.isArray(user.emails) && user.emails.map(email => `${ user.name || user.username }<${ email.address }>`); + + if (destinations) { + const email = { + to: destinations, + from: RocketChat.settings.get('From_Email'), + subject: Accounts.emailTemplates.userActivated.subject({active}), + html: Accounts.emailTemplates.userActivated.html({active, name: user.name, username: user.username}) + }; + + Meteor.defer(() => Email.send(email)); } return true; diff --git a/tests/data/user.js b/tests/data/user.js index 92294f99dfcf..f9d51e8ba46b 100644 --- a/tests/data/user.js +++ b/tests/data/user.js @@ -1,6 +1,7 @@ export const username = `user.test.${ Date.now() }`; export const email = `${ username }@rocket.chat`; export const password = 'rocket.chat'; +export const reason = 'rocket.chat.reason'; export const adminUsername = 'rocketchat.internal.admin.test'; export const adminEmail = `${ adminUsername }@rocket.chat`; diff --git a/tests/end-to-end/ui/12-settings.js b/tests/end-to-end/ui/12-settings.js index d54756e6b8b6..759cd13019b7 100644 --- a/tests/end-to-end/ui/12-settings.js +++ b/tests/end-to-end/ui/12-settings.js @@ -16,7 +16,7 @@ import admin from '../../pageobjects/administration.page'; import {checkIfUserIsValid, checkIfUserIsAdmin} from '../../data/checks'; import {targetUser, imgURL} from '../../data/interactions.js'; -import {adminUsername, adminEmail, adminPassword, username, email, password} from '../../data/user.js'; +import {adminUsername, adminEmail, adminPassword, username, email, password, reason} from '../../data/user.js'; function api(path) { return prefix + path; @@ -454,6 +454,7 @@ describe('[Api Settings Change]', () => { }); it('register the user', () => { + browser.refresh(); loginPage.registerButton.waitForVisible(5000); loginPage.registerButton.click(); loginPage.nameField.waitForVisible(5000); @@ -461,6 +462,8 @@ describe('[Api Settings Change]', () => { loginPage.emailField.setValue(`setting${ email }`); loginPage.passwordField.setValue(password); loginPage.confirmPasswordField.setValue(password); + loginPage.reasonField.waitForVisible(5000); + loginPage.reasonField.setValue(reason); loginPage.submit(); diff --git a/tests/pageobjects/login.page.js b/tests/pageobjects/login.page.js index 318970150b14..1084a0cbc109 100644 --- a/tests/pageobjects/login.page.js +++ b/tests/pageobjects/login.page.js @@ -12,6 +12,7 @@ class LoginPage extends Page { get emailField() { return browser.element('[name=email]'); } get passwordField() { return browser.element('[name=pass]'); } get confirmPasswordField() { return browser.element('[name=confirm-pass]'); } + get reasonField() { return browser.element('[name=reason]'); } get inputUsername() { return browser.element('form#login-card input#username'); } get emailOrUsernameInvalidText() { return browser.element('[name=emailOrUsername]~.input-error'); }