diff --git a/.eslintrc b/.eslintrc index 3c9e29446b60..bbae28a07d60 100644 --- a/.eslintrc +++ b/.eslintrc @@ -72,7 +72,7 @@ "curly": [2, "all"], "eqeqeq": [2, "allow-null"], "new-cap": [2, { - "capIsNewExceptions": ["Match.Optional", "Match.Maybe", "Match.ObjectIncluding", "Push.Configure"] + "capIsNewExceptions": ["Match.Optional", "Match.Maybe", "Match.ObjectIncluding", "Push.Configure", "SHA256"] }], "use-isnan": 2, "valid-typeof": 2, diff --git a/.meteor/packages b/.meteor/packages index 4863b12e029e..905b829d46f2 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -38,6 +38,7 @@ standard-minifier-css@1.3.3 standard-minifier-js@1.2.2 tracker@1.1.2 +rocketchat:2fa rocketchat:action-links rocketchat:analytics rocketchat:api diff --git a/.meteor/versions b/.meteor/versions index da42836cd55d..d6517147238f 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -119,6 +119,7 @@ reactive-dict@1.1.8 reactive-var@1.0.11 reload@1.1.11 retry@1.0.9 +rocketchat:2fa@0.0.1 rocketchat:action-links@0.0.1 rocketchat:analytics@0.0.2 rocketchat:api@0.0.1 diff --git a/HISTORY.md b/HISTORY.md index d1dbaa0b2f32..f9d83f5e4040 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -4,6 +4,7 @@ - [NEW] Permission `join-without-join-code` assigned to admins and bots by default (#6139) - [NEW] Integrations, both incoming and outgoing, now have access to the models. Example: `Users.findOneById(id)` (#6336) +- [NEW] Option to enable `Two Factor Authentication` in user's account preference - [FIX] Incoming integrations would break when trying to use the `Store` feature. ## 0.54.2 - 2017-Mar-24 diff --git a/packages/rocketchat-2fa/.npm/package/.gitignore b/packages/rocketchat-2fa/.npm/package/.gitignore new file mode 100644 index 000000000000..3c3629e647f5 --- /dev/null +++ b/packages/rocketchat-2fa/.npm/package/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/packages/rocketchat-2fa/.npm/package/README b/packages/rocketchat-2fa/.npm/package/README new file mode 100644 index 000000000000..3d492553a438 --- /dev/null +++ b/packages/rocketchat-2fa/.npm/package/README @@ -0,0 +1,7 @@ +This directory and the files immediately inside it are automatically generated +when you change this package's NPM dependencies. Commit the files in this +directory (npm-shrinkwrap.json, .gitignore, and this README) to source control +so that others run the same versions of sub-dependencies. + +You should NOT check in the node_modules directory that Meteor automatically +creates; if you are using git, the .gitignore file tells git to ignore it. diff --git a/packages/rocketchat-2fa/.npm/package/npm-shrinkwrap.json b/packages/rocketchat-2fa/.npm/package/npm-shrinkwrap.json new file mode 100644 index 000000000000..df69ce869e8b --- /dev/null +++ b/packages/rocketchat-2fa/.npm/package/npm-shrinkwrap.json @@ -0,0 +1,19 @@ +{ + "dependencies": { + "base32.js": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.0.1.tgz", + "from": "base32.js@0.0.1" + }, + "speakeasy": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/speakeasy/-/speakeasy-2.0.0.tgz", + "from": "speakeasy@2.0.0" + }, + "yaqrcode": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/yaqrcode/-/yaqrcode-0.2.1.tgz", + "from": "yaqrcode@0.2.1" + } + } +} diff --git a/packages/rocketchat-2fa/README.md b/packages/rocketchat-2fa/README.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/rocketchat-2fa/client/TOTPPassword.js b/packages/rocketchat-2fa/client/TOTPPassword.js new file mode 100644 index 000000000000..2fb6fa0e171e --- /dev/null +++ b/packages/rocketchat-2fa/client/TOTPPassword.js @@ -0,0 +1,72 @@ +import toastr from 'toastr'; + +function reportError(error, callback) { + if (callback) { + callback(error); + } else { + throw error; + } +} + +Meteor.loginWithPasswordAndTOTP = function(selector, password, code, callback) { + if (typeof selector === 'string') { + if (selector.indexOf('@') === -1) { + selector = {username: selector}; + } else { + selector = {email: selector}; + } + } + + Accounts.callLoginMethod({ + methodArguments: [{ + totp: { + login: { + user: selector, + password: Accounts._hashPassword(password) + }, + code + } + }], + userCallback(error) { + if (error) { + reportError(error, callback); + } else { + callback && callback(); + } + } + }); +}; + +const loginWithPassword = Meteor.loginWithPassword; + +Meteor.loginWithPassword = function(email, password, cb) { + loginWithPassword(email, password, (error) => { + if (!error || error.error !== 'totp-required') { + return cb(error); + } + + swal({ + title: t('Two-factor_authentication'), + text: t('Open_your_authentication_app_and_enter_the_code'), + type: 'input', + inputType: 'text', + showCancelButton: true, + closeOnConfirm: true, + confirmButtonText: t('Verify'), + cancelButtonText: t('Cancel') + }, (code) => { + if (code === false) { + return cb(); + } + + Meteor.loginWithPasswordAndTOTP(email, password, code, (error) => { + if (error && error.error === 'totp-invalid') { + toastr.error(t('Invalid_two_factor_code')); + cb(); + } else { + cb(error); + } + }); + }); + }); +}; diff --git a/packages/rocketchat-2fa/client/accountSecurity.html b/packages/rocketchat-2fa/client/accountSecurity.html new file mode 100644 index 000000000000..9ed51e09d061 --- /dev/null +++ b/packages/rocketchat-2fa/client/accountSecurity.html @@ -0,0 +1,57 @@ + diff --git a/packages/rocketchat-2fa/client/accountSecurity.js b/packages/rocketchat-2fa/client/accountSecurity.js new file mode 100644 index 000000000000..40543df58297 --- /dev/null +++ b/packages/rocketchat-2fa/client/accountSecurity.js @@ -0,0 +1,145 @@ +import toastr from 'toastr'; +import qrcode from 'yaqrcode'; + +window.qrcode = qrcode; + +Template.accountSecurity.helpers({ + showImage() { + return Template.instance().showImage.get(); + }, + imageData() { + return Template.instance().imageData.get(); + }, + isEnabled() { + const user = Meteor.user(); + return user && user.services && user.services.totp && user.services.totp.enabled; + }, + isRegistering() { + return Template.instance().state.get() === 'registering'; + }, + codesRemaining() { + if (Template.instance().codesRemaining.get()) { + return t('You_have_n_codes_remaining', { number: Template.instance().codesRemaining.get() }); + } + } +}); + +Template.accountSecurity.events({ + 'click .enable-2fa'(event, instance) { + Meteor.call('2fa:enable', (error, result) => { + instance.imageData.set(qrcode(result.url, { size: 200 })); + + instance.state.set('registering'); + + Meteor.defer(() => { + instance.find('#testCode').focus(); + }); + }); + }, + + 'click .disable-2fa'() { + swal({ + title: t('Two-factor_authentication'), + text: t('Open_your_authentication_app_and_enter_the_code'), + type: 'input', + inputType: 'text', + showCancelButton: true, + closeOnConfirm: true, + confirmButtonText: t('Verify'), + cancelButtonText: t('Cancel') + }, (code) => { + if (code === false) { + return; + } + + Meteor.call('2fa:disable', code, (error, result) => { + if (error) { + return toastr.error(t(error.error)); + } + + if (result) { + toastr.success(t('Two-factor_authentication_disabled')); + } else { + return toastr.error(t('Invalid_two_factor_code')); + } + }); + }); + }, + + 'submit .verify-code'(event, instance) { + event.preventDefault(); + + Meteor.call('2fa:validateTempToken', instance.find('#testCode').value, (error, result) => { + if (result) { + instance.showBackupCodes(result.codes); + + instance.find('#testCode').value = ''; + instance.state.set(); + toastr.success(t('Two-factor_authentication_enabled')); + } else { + toastr.error(t('Invalid_two_factor_code')); + } + }); + }, + + 'click .regenerate-codes'(event, instance) { + swal({ + title: t('Two-factor_authentication'), + text: t('Open_your_authentication_app_and_enter_the_code'), + type: 'input', + inputType: 'text', + showCancelButton: true, + closeOnConfirm: false, + confirmButtonText: t('Verify'), + cancelButtonText: t('Cancel') + }, (code) => { + if (code === false) { + return; + } + + Meteor.call('2fa:regenerateCodes', code, (error, result) => { + if (error) { + return toastr.error(t(error.error)); + } + + if (result) { + instance.showBackupCodes(result.codes); + } else { + return toastr.error(t('Invalid_two_factor_code')); + } + }); + }); + } +}); + +Template.accountSecurity.onCreated(function() { + this.showImage = new ReactiveVar(false); + this.imageData = new ReactiveVar(); + + this.state = new ReactiveVar(); + + this.codesRemaining = new ReactiveVar(); + + this.showBackupCodes = (userCodes) => { + const backupCodes = userCodes.map((value, index) => { + return (index + 1) % 4 === 0 && index < 11 ? `${ value }\n` : `${ value } `; + }).join(''); + const codes = `${ backupCodes }`; + swal({ + title: t('Backup_codes'), + text: `${ t('Make_sure_you_have_a_copy_of_your_codes', { codes }) }`, + html: true + }); + }; + + this.autorun(() => { + const user = Meteor.user(); + if (user && user.services && user.services.totp && user.services.totp.enabled) { + Meteor.call('2fa:checkCodesRemaining', (error, result) => { + if (result) { + this.codesRemaining.set(result.remaining); + } + }); + } + }); +}); diff --git a/packages/rocketchat-2fa/package.js b/packages/rocketchat-2fa/package.js new file mode 100644 index 000000000000..3a8bdc696b82 --- /dev/null +++ b/packages/rocketchat-2fa/package.js @@ -0,0 +1,39 @@ +Package.describe({ + name: 'rocketchat:2fa', + version: '0.0.1', + summary: '', + git: '', + documentation: 'README.md' +}); + +Npm.depends({ + speakeasy: '2.0.0', + yaqrcode: '0.2.1' +}); + +Package.onUse(function(api) { + api.use([ + 'accounts-base', + 'ecmascript', + 'templating', + 'rocketchat:lib', + 'sha', + 'random' + ]); + + api.addFiles('client/accountSecurity.html', 'client'); + api.addFiles('client/accountSecurity.js', 'client'); + api.addFiles('client/TOTPPassword.js', 'client'); + + api.addFiles('server/lib/totp.js', 'server'); + + api.addFiles('server/methods/checkCodesRemaining.js', 'server'); + api.addFiles('server/methods/disable.js', 'server'); + api.addFiles('server/methods/enable.js', 'server'); + api.addFiles('server/methods/regenerateCodes.js', 'server'); + api.addFiles('server/methods/validateTempToken.js', 'server'); + + api.addFiles('server/models/users.js', 'server'); + + api.addFiles('server/loginHandler.js', 'server'); +}); diff --git a/packages/rocketchat-2fa/server/lib/totp.js b/packages/rocketchat-2fa/server/lib/totp.js new file mode 100644 index 000000000000..bf81dd043cd0 --- /dev/null +++ b/packages/rocketchat-2fa/server/lib/totp.js @@ -0,0 +1,54 @@ +import speakeasy from 'speakeasy'; + +RocketChat.TOTP = { + generateSecret() { + return speakeasy.generateSecret(); + }, + + generateOtpauthURL(secret, username) { + return speakeasy.otpauthURL({ + secret: secret.ascii, + label: `Rocket.Chat:${ username }` + }); + }, + + verify({ secret, token, backupTokens, userId }) { + let verified; + + // validates a backup code + if (token.length === 8 && backupTokens) { + const hashedCode = SHA256(token); + const usedCode = backupTokens.indexOf(hashedCode); + + if (usedCode !== -1) { + verified = true; + + backupTokens.splice(usedCode, 1); + + // mark the code as used (remove it from the list) + RocketChat.models.Users.update2FABackupCodesByUserId(userId, backupTokens); + } + } else { + verified = speakeasy.totp.verify({ + secret, + encoding: 'base32', + token + }); + } + + return verified; + }, + + generateCodes() { + // generate 12 backup codes + const codes = []; + const hashedCodes = []; + for (let i = 0; i < 12; i++) { + const code = Random.id(8); + codes.push(code); + hashedCodes.push(SHA256(code)); + } + + return { codes, hashedCodes }; + } +}; diff --git a/packages/rocketchat-2fa/server/loginHandler.js b/packages/rocketchat-2fa/server/loginHandler.js new file mode 100644 index 000000000000..650b1f9d6df5 --- /dev/null +++ b/packages/rocketchat-2fa/server/loginHandler.js @@ -0,0 +1,28 @@ +Accounts.registerLoginHandler('totp', function(options) { + if (!options.totp || !options.totp.code) { + return; + } + + return Accounts._runLoginHandlers(this, options.totp.login); +}); + +RocketChat.callbacks.add('onValidateLogin', (login) => { + if (login.type === 'password' && login.user.services && login.user.services.totp && login.user.services.totp.enabled === true) { + const { totp } = login.methodArguments[0]; + + if (!totp || !totp.code) { + throw new Meteor.Error('totp-required', 'TOTP Required'); + } + + const verified = RocketChat.TOTP.verify({ + secret: login.user.services.totp.secret, + token: totp.code, + userId: login.user._id, + backupTokens: login.user.services.totp.hashedBackup + }); + + if (verified !== true) { + throw new Meteor.Error('totp-invalid', 'TOTP Invalid'); + } + } +}); diff --git a/packages/rocketchat-2fa/server/methods/checkCodesRemaining.js b/packages/rocketchat-2fa/server/methods/checkCodesRemaining.js new file mode 100644 index 000000000000..8d2224d61726 --- /dev/null +++ b/packages/rocketchat-2fa/server/methods/checkCodesRemaining.js @@ -0,0 +1,17 @@ +Meteor.methods({ + '2fa:checkCodesRemaining'() { + if (!Meteor.userId()) { + throw new Meteor.Error('not-authorized'); + } + + const user = Meteor.user(); + + if (!user.services || !user.services.totp || !user.services.totp.enabled) { + throw new Meteor.Error('invalid-totp'); + } + + return { + remaining: user.services.totp.hashedBackup.length + }; + } +}); diff --git a/packages/rocketchat-2fa/server/methods/disable.js b/packages/rocketchat-2fa/server/methods/disable.js new file mode 100644 index 000000000000..f8cb63d3dbef --- /dev/null +++ b/packages/rocketchat-2fa/server/methods/disable.js @@ -0,0 +1,22 @@ +Meteor.methods({ + '2fa:disable'(code) { + if (!Meteor.userId()) { + throw new Meteor.Error('not-authorized'); + } + + const user = Meteor.user(); + + const verified = RocketChat.TOTP.verify({ + secret: user.services.totp.secret, + token: code, + userId: Meteor.userId(), + backupTokens: user.services.totp.hashedBackup + }); + + if (!verified) { + return false; + } + + return RocketChat.models.Users.disable2FAByUserId(Meteor.userId()); + } +}); diff --git a/packages/rocketchat-2fa/server/methods/enable.js b/packages/rocketchat-2fa/server/methods/enable.js new file mode 100644 index 000000000000..3c690089b7cf --- /dev/null +++ b/packages/rocketchat-2fa/server/methods/enable.js @@ -0,0 +1,17 @@ +Meteor.methods({ + '2fa:enable'() { + if (!Meteor.userId()) { + throw new Meteor.Error('not-authorized'); + } + + const user = Meteor.user(); + + const secret = RocketChat.TOTP.generateSecret(); + + RocketChat.models.Users.disable2FAAndSetTempSecretByUserId(Meteor.userId(), secret.base32); + + return { + url: RocketChat.TOTP.generateOtpauthURL(secret, user.username) + }; + } +}); diff --git a/packages/rocketchat-2fa/server/methods/regenerateCodes.js b/packages/rocketchat-2fa/server/methods/regenerateCodes.js new file mode 100644 index 000000000000..24f9a4e5973f --- /dev/null +++ b/packages/rocketchat-2fa/server/methods/regenerateCodes.js @@ -0,0 +1,27 @@ +Meteor.methods({ + '2fa:regenerateCodes'(userToken) { + if (!Meteor.userId()) { + throw new Meteor.Error('not-authorized'); + } + + const user = Meteor.user(); + + if (!user.services || !user.services.totp || !user.services.totp.enabled) { + throw new Meteor.Error('invalid-totp'); + } + + const verified = RocketChat.TOTP.verify({ + secret: user.services.totp.secret, + token: userToken, + userId: Meteor.userId(), + backupTokens: user.services.totp.hashedBackup + }); + + if (verified) { + const { codes, hashedCodes } = RocketChat.TOTP.generateCodes(); + + RocketChat.models.Users.update2FABackupCodesByUserId(Meteor.userId(), hashedCodes); + return { codes }; + } + } +}); diff --git a/packages/rocketchat-2fa/server/methods/validateTempToken.js b/packages/rocketchat-2fa/server/methods/validateTempToken.js new file mode 100644 index 000000000000..482ccfde0098 --- /dev/null +++ b/packages/rocketchat-2fa/server/methods/validateTempToken.js @@ -0,0 +1,25 @@ +Meteor.methods({ + '2fa:validateTempToken'(userToken) { + if (!Meteor.userId()) { + throw new Meteor.Error('not-authorized'); + } + + const user = Meteor.user(); + + if (!user.services || !user.services.totp || !user.services.totp.tempSecret) { + throw new Meteor.Error('invalid-totp'); + } + + const verified = RocketChat.TOTP.verify({ + secret: user.services.totp.tempSecret, + token: userToken + }); + + if (verified) { + const { codes, hashedCodes } = RocketChat.TOTP.generateCodes(); + + RocketChat.models.Users.enable2FAAndSetSecretAndCodesByUserId(Meteor.userId(), user.services.totp.tempSecret, hashedCodes); + return { codes }; + } + } +}); diff --git a/packages/rocketchat-2fa/server/models/users.js b/packages/rocketchat-2fa/server/models/users.js new file mode 100644 index 000000000000..ba90d2673861 --- /dev/null +++ b/packages/rocketchat-2fa/server/models/users.js @@ -0,0 +1,49 @@ +RocketChat.models.Users.disable2FAAndSetTempSecretByUserId = function(userId, tempToken) { + return this.update({ + _id: userId + }, { + $set: { + 'services.totp': { + enabled: false, + tempSecret: tempToken + } + } + }); +}; + +RocketChat.models.Users.enable2FAAndSetSecretAndCodesByUserId = function(userId, secret, backupCodes) { + return this.update({ + _id: userId + }, { + $set: { + 'services.totp.enabled': true, + 'services.totp.secret': secret, + 'services.totp.hashedBackup': backupCodes + }, + $unset: { + 'services.totp.tempSecret': 1 + } + }); +}; + +RocketChat.models.Users.disable2FAByUserId = function(userId) { + return this.update({ + _id: userId + }, { + $set: { + 'services.totp': { + enabled: false + } + } + }); +}; + +RocketChat.models.Users.update2FABackupCodesByUserId = function(userId, backupCodes) { + return this.update({ + _id: userId + }, { + $set: { + 'services.totp.hashedBackup': backupCodes + } + }); +}; diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 12cba57bcbdc..6057e02758c1 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -245,6 +245,7 @@ "Back_to_integration_detail": "Back to the integration detail", "Back_to_login": "Back to login", "Back_to_permissions": "Back to permissions", + "Backup_codes": "Backup codes", "Beta_feature_Depends_on_Video_Conference_to_be_enabled": "Beta feature. Depends on Video Conference to be enabled.", "Block_User": "Block User", "Body": "Body", @@ -405,6 +406,7 @@ "Desktop_Notifications_Enabled": "Desktop Notifications are Enabled", "Direct_message_someone": "Direct message someone", "Direct_Messages": "Direct Messages", + "Disable_two-factor_authentication": "Disable two-factor authentication", "Display_offline_form": "Display offline form", "Displays_action_text": "Displays action text", "Do_you_want_to_change_to_s_question": "Do you want to change to %s?", @@ -447,10 +449,12 @@ "Empty_title": "Empty title", "Enable": "Enable", "Enable_Desktop_Notifications": "Enable Desktop Notifications", + "Enable_two-factor_authentication": "Enable two-factor authentication", "Enabled": "Enabled", "Enable_Svg_Favicon": "Enable SVG favicon", "Encrypted_message": "Encrypted message", "End_OTR": "End OTR", + "Enter_authentication_code": "Enter authentication code", "Enter_Alternative": "Alternative mode (send with Enter + Ctrl/Alt/Shift/CMD)", "Enter_a_regex": "Enter a regex", "Enter_a_room_name": "Enter a room name", @@ -740,6 +744,7 @@ "Invalid_room_name": "%s is not a valid room name,
use only letters, numbers, hyphens and underscores", "Invalid_secret_URL_message": "The URL provided is invalid.", "Invalid_setting_s": "Invalid setting: %s", + "Invalid_two_factor_code": "Invalid two factor code", "invisible": "invisible", "Invisible": "Invisible", "Invitation": "Invitation", @@ -924,6 +929,7 @@ "Mailer_body_tags": "You must use [unsubscribe] for the unsubscription link.
You may use [name], [fname], [lname] for the user's full name, first name or last name, respectively.
You may use [email] for the user's email.", "Mailing": "Mailing", "Make_Admin": "Make Admin", + "Make_sure_you_have_a_copy_of_your_codes": "Make sure you have a copy of your codes: __codes__ If you lose access to your authenticator app, you can use one of these codes to log in.", "Manager_added": "Manager added", "Manager_removed": "Manager removed", "Managing_assets": "Managing assets", @@ -1095,6 +1101,7 @@ "optional": "optional", "Use_minor_colors": "Use minor color palette (defaults inherit major colors)", "or": "or", + "Open_your_authentication_app_and_enter_the_code": "Open your authentication app and enter the code. You can also use one of your backup codes.", "Order": "Order", "OS_Arch": "OS Arch", "OS_Cpus": "OS CPU Count", @@ -1196,6 +1203,7 @@ "Refresh_oauth_services": "Refresh OAuth Services", "Refresh_keys": "Refresh keys", "Refresh_your_page_after_install_to_enable_screen_sharing": "Refresh your page after install to enable screen sharing", + "Regenerate_codes": "Regenerate codes", "Register": "Register a new account", "Registration": "Registration", "Registration_Succeeded": "Registration Succeeded", @@ -1276,6 +1284,7 @@ "Save_to_enable_this_action": "Save to enable this action", "Saved": "Saved", "Saving": "Saving", + "Scan_QR_code": "Using an authenticator app like Google Authenticator, Authy or Duo, scan the QR code. It will display a 6 digit code which you need to enter below.", "Scope": "Scope", "Screen_Share": "Screen Share", "Script_Enabled": "Script Enabled", @@ -1285,6 +1294,7 @@ "Search_Private_Groups": "Search Private Groups", "seconds": "seconds", "Secret_token": "Secret token", + "Security": "Security", "Select_a_department": "Select a department", "Select_a_user": "Select a user", "Select_an_avatar": "Select an avatar", @@ -1478,6 +1488,10 @@ "This_is_a_push_test_messsage": "This is a push test message", "This_room_has_been_archived_by__username_": "This room has been archived by __username__", "This_room_has_been_unarchived_by__username_": "This room has been unarchived by __username__", + "Two-factor_authentication": "Two-factor authentication", + "Two-factor_authentication_disabled": "Two-factor authentication disabled", + "Two-factor_authentication_enabled": "Two-factor authentication enabled", + "Two-factor_authentication_is_currently_disabled": "Two-factor authentication is currently disabled", "Thursday": "Thursday", "Time_in_seconds": "Time in seconds", "Title": "Title", @@ -1599,6 +1613,7 @@ "Verification_Email_Subject": "[Site_Name] - Verify your account", "Verification_Email": "Click here to verify your account.", "Verified": "Verified", + "Verify": "Verify", "Version": "Version", "Video_Chat_Window": "Video Chat", "Video_Conference": "Video Conference", @@ -1653,6 +1668,7 @@ "You_can_use_webhooks_to_easily_integrate_livechat_with_your_CRM": "You can use webhooks to easily integrate livechat with your CRM.", "You_cant_leave_a_livechat_room_Please_use_the_close_button": "You can't leave a livechat room. Please, use the close button.", "You_have_been_muted": "You have been muted and cannot speak in this room", + "You_have_n_codes_remaining": "You have __number__ codes remaining.", "You_have_not_verified_your_email": "You have not verified your email.", "You_have_successfully_unsubscribed": "You have successfully unsubscribed from our Mailling List.", "You_must_join_to_view_messages_in_this_channel": "You must join to view messages in this channel", diff --git a/packages/rocketchat-theme/client/imports/base.less b/packages/rocketchat-theme/client/imports/base.less index b5a45929b789..334cd74107da 100644 --- a/packages/rocketchat-theme/client/imports/base.less +++ b/packages/rocketchat-theme/client/imports/base.less @@ -4803,8 +4803,12 @@ body:not(.is-cordova) { } } -.sweet-alert .sa-input-error { - top: 19px; +.sweet-alert { + margin-left: -239px !important; + + .sa-input-error { + top: 19px; + } } .one-passsword { diff --git a/packages/rocketchat-ui-account/client/accountFlex.html b/packages/rocketchat-ui-account/client/accountFlex.html index 6c3e46ddcd6f..e8f433806c48 100644 --- a/packages/rocketchat-ui-account/client/accountFlex.html +++ b/packages/rocketchat-ui-account/client/accountFlex.html @@ -10,15 +10,18 @@

{{_ "My_Account"}}

  • {{_ "Preferences"}}
  • + {{#if allowUserProfileChange}}
  • - {{#if allowUserProfileChange}} - {{_ "Profile"}} - {{/if}} + {{_ "Profile"}}
  • + {{/if}} + {{#if allowUserAvatarChange}}
  • - {{#if allowUserAvatarChange}} - {{_ "Avatar"}} - {{/if}} + {{_ "Avatar"}} +
  • + {{/if}} +
  • + {{_ "Security"}}
  • diff --git a/packages/rocketchat-ui-login/package.js b/packages/rocketchat-ui-login/package.js index bd0efa48cbc1..4c8f8647f17b 100644 --- a/packages/rocketchat-ui-login/package.js +++ b/packages/rocketchat-ui-login/package.js @@ -17,7 +17,8 @@ Package.onUse(function(api) { 'coffeescript', 'underscore', 'rocketchat:lib', - 'rocketchat:assets' + 'rocketchat:assets', + 'rocketchat:2fa' ]); api.use('kadira:flow-router', 'client'); diff --git a/server/lib/accounts.js b/server/lib/accounts.js index a9675794ba9c..dd31698a69fd 100644 --- a/server/lib/accounts.js +++ b/server/lib/accounts.js @@ -164,6 +164,8 @@ Accounts.validateLoginAttempt(function(login) { } } + login = RocketChat.callbacks.run('onValidateLogin', login); + RocketChat.models.Users.updateLastLoginById(login.user._id); Meteor.defer(function() { return RocketChat.callbacks.run('afterValidateLogin', login); diff --git a/server/publications/userData.js b/server/publications/userData.js index 2d12096f0210..cec86d01cb3c 100644 --- a/server/publications/userData.js +++ b/server/publications/userData.js @@ -23,6 +23,7 @@ Meteor.publish('userData', function() { requirePasswordChange: 1, requirePasswordChangeReason: 1, 'services.password.bcrypt': 1, + 'services.totp.enabled': 1, statusLivechat: 1 } });