diff --git a/accounts/src/pages/VerifyBackupCode.js b/accounts/src/pages/VerifyBackupCode.js
index 1eae6f5808..2077485af8 100644
--- a/accounts/src/pages/VerifyBackupCode.js
+++ b/accounts/src/pages/VerifyBackupCode.js
@@ -107,6 +107,15 @@ export class VerifyBackupCode extends Component {
+
diff --git a/admin-dashboard/package-lock.json b/admin-dashboard/package-lock.json
index b829488d81..e9b58162f3 100644
--- a/admin-dashboard/package-lock.json
+++ b/admin-dashboard/package-lock.json
@@ -15501,6 +15501,21 @@
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
"integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc="
},
+ "qr.js": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz",
+ "integrity": "sha1-ys6GOG9ZoNuAUPqQ2baw6IoeNk8="
+ },
+ "qrcode.react": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-1.0.0.tgz",
+ "integrity": "sha512-jBXleohRTwvGBe1ngV+62QvEZ/9IZqQivdwzo9pJM4LQMoCM2VnvNBnKdjvGnKyDZ/l0nCDgsPod19RzlPvm/Q==",
+ "requires": {
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.6.0",
+ "qr.js": "0.0.0"
+ }
+ },
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
diff --git a/admin-dashboard/package.json b/admin-dashboard/package.json
index 2f47a6ab5b..2b0071635d 100644
--- a/admin-dashboard/package.json
+++ b/admin-dashboard/package.json
@@ -22,6 +22,7 @@
"prop-types": "^15.6.1",
"puppeteer": "^2.1.1",
"puppeteer-cluster": "^0.19.0",
+ "qrcode.react": "^1.0.0",
"react": "^16.5.2",
"react-click-outside": "github:tj/react-click-outside",
"react-dom": "^16.5.2",
diff --git a/admin-dashboard/src/actions/user.js b/admin-dashboard/src/actions/user.js
index 1065fcb9db..7aab5c77a2 100644
--- a/admin-dashboard/src/actions/user.js
+++ b/admin-dashboard/src/actions/user.js
@@ -548,3 +548,139 @@ export const searchUsers = (filter, skip, limit) => async dispatch => {
dispatch(searchUsersError(errors(errorMsg)));
}
};
+
+// Update user twoFactorAuthToken
+export function twoFactorAuthTokenRequest() {
+ return {
+ type: types.UPDATE_TWO_FACTOR_AUTH_REQUEST,
+ };
+}
+
+export function twoFactorAuthTokenSuccess(payload) {
+ return {
+ type: types.UPDATE_TWO_FACTOR_AUTH_SUCCESS,
+ payload: payload,
+ };
+}
+
+export function twoFactorAuthTokenError(error) {
+ return {
+ type: types.UPDATE_TWO_FACTOR_AUTH_FAILURE,
+ payload: error,
+ };
+}
+
+export function updateTwoFactorAuthToken(data) {
+ return function(dispatch) {
+ const promise = putApi('user/profile', data);
+ dispatch(twoFactorAuthTokenRequest());
+ promise.then(
+ function(response) {
+ const payload = response.data;
+ dispatch(twoFactorAuthTokenSuccess(payload));
+ return payload;
+ },
+ function(error) {
+ if (error && error.response && error.response.data)
+ error = error.response.data;
+ if (error && error.data) {
+ error = error.data;
+ }
+ if (error && error.message) {
+ error = error.message;
+ } else {
+ error = 'Network Error';
+ }
+ dispatch(twoFactorAuthTokenError(errors(error)));
+ }
+ );
+
+ return promise;
+ };
+}
+
+export function setTwoFactorAuth(enabled) {
+ return {
+ type: types.SET_TWO_FACTOR_AUTH,
+ payload: enabled,
+ };
+}
+
+export function verifyTwoFactorAuthToken(values) {
+ return function(dispatch) {
+ const promise = postApi('user/totp/verifyToken', values);
+ dispatch(twoFactorAuthTokenRequest());
+ promise.then(
+ function(response) {
+ const payload = response.data;
+ dispatch(twoFactorAuthTokenSuccess(payload));
+ return payload;
+ },
+ function(error) {
+ if (error && error.response && error.response.data)
+ error = error.response.data;
+ if (error && error.data) {
+ error = error.data;
+ }
+ if (error && error.message) {
+ error = error.message;
+ } else {
+ error = 'Network Error';
+ }
+ dispatch(twoFactorAuthTokenError(errors(error)));
+ }
+ );
+
+ return promise;
+ };
+}
+
+// Generate user's QR code
+export function generateTwoFactorQRCodeRequest() {
+ return {
+ type: types.GENERATE_TWO_FACTOR_QR_REQUEST,
+ };
+}
+
+export function generateTwoFactorQRCodeSuccess(payload) {
+ return {
+ type: types.GENERATE_TWO_FACTOR_QR_SUCCESS,
+ payload: payload,
+ };
+}
+
+export function generateTwoFactorQRCodeError(error) {
+ return {
+ type: types.GENERATE_TWO_FACTOR_QR_FAILURE,
+ payload: error,
+ };
+}
+
+export function generateTwoFactorQRCode(userId) {
+ return function(dispatch) {
+ const promise = postApi(`user/totp/token/${userId}`);
+ dispatch(generateTwoFactorQRCodeRequest());
+ promise.then(
+ function(response) {
+ const payload = response.data;
+ dispatch(generateTwoFactorQRCodeSuccess(payload));
+ return payload;
+ },
+ function(error) {
+ if (error && error.response && error.response.data)
+ error = error.response.data;
+ if (error && error.data) {
+ error = error.data;
+ }
+ if (error && error.message) {
+ error = error.message;
+ } else {
+ error = 'Network Error';
+ }
+ dispatch(generateTwoFactorQRCodeError(errors(error)));
+ }
+ );
+
+ return promise;
+ };
+}
diff --git a/admin-dashboard/src/components/user/TwoFactorAuth.js b/admin-dashboard/src/components/user/TwoFactorAuth.js
new file mode 100644
index 0000000000..54f0162bd7
--- /dev/null
+++ b/admin-dashboard/src/components/user/TwoFactorAuth.js
@@ -0,0 +1,337 @@
+import React, { Component } from 'react';
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import { closeModal } from '../../actions/modal';
+import ShouldRender from '../basic/ShouldRender';
+import { reduxForm, Field, SubmissionError } from 'redux-form';
+import { Spinner } from '../basic/Loader';
+import QRCode from 'qrcode.react';
+import { RenderField } from '../basic/RenderField';
+import { ListLoader } from '../basic/Loader.js';
+import {
+ setTwoFactorAuth,
+ verifyTwoFactorAuthToken,
+ generateTwoFactorQRCode,
+ twoFactorAuthTokenError,
+} from '../../actions/user';
+
+function validate() {
+ return undefined;
+}
+
+class TwoFactorAuthModal extends Component {
+ state = { next: false };
+
+ async componentDidMount() {
+ const {
+ user: { user },
+ generateTwoFactorQRCode,
+ } = this.props;
+ generateTwoFactorQRCode(user._id);
+ }
+
+ handleKeyBoard = e => {
+ const { twoFactorAuthId, closeModal } = this.props;
+ switch (e.key) {
+ case 'Escape':
+ return closeModal({
+ id: twoFactorAuthId,
+ });
+ default:
+ return false;
+ }
+ };
+
+ nextHandler = event => {
+ event.preventDefault();
+ this.setState({ next: true });
+ };
+
+ submitForm = values => {
+ if (!values.token) {
+ throw new SubmissionError({ token: 'Auth token is required.' });
+ }
+
+ const { twoFactorAuthId, closeModal } = this.props;
+ if (values.token) {
+ const {
+ setTwoFactorAuth,
+ verifyTwoFactorAuthToken,
+ user: { user },
+ } = this.props;
+ values.userId = user._id;
+ verifyTwoFactorAuthToken(values).then(response => {
+ setTwoFactorAuth(response.data.twoFactorAuthEnabled);
+ return closeModal({
+ id: twoFactorAuthId,
+ });
+ });
+ }
+ };
+
+ render() {
+ const { handleSubmit, qrCode, twoFactorAuthSetting } = this.props;
+ const { next } = this.state;
+
+ return (
+
+ );
+ }
+}
+
+TwoFactorAuthModal.displayName = 'TwoFactorAuthModal';
+
+const TwoFactorAuthForm = reduxForm({
+ form: 'TwoFactorAuthForm',
+ enableReinitialize: true,
+ validate,
+})(TwoFactorAuthModal);
+
+TwoFactorAuthModal.propTypes = {
+ handleSubmit: PropTypes.func.isRequired,
+ closeModal: PropTypes.func.isRequired,
+ generateTwoFactorQRCode: PropTypes.func,
+ setTwoFactorAuth: PropTypes.func,
+ user: PropTypes.object,
+ qrCode: PropTypes.object,
+ twoFactorAuthId: PropTypes.string,
+ twoFactorAuthSetting: PropTypes.object,
+ verifyTwoFactorAuthToken: PropTypes.func,
+ twoFactorAuthTokenError: PropTypes.func,
+};
+
+const mapStateToProps = state => {
+ return {
+ user: state.user.user,
+ qrCode: state.user.qrCode,
+ twoFactorAuthSetting: state.user.twoFactorAuthSetting,
+ };
+};
+
+const mapDispatchToProps = dispatch =>
+ bindActionCreators(
+ {
+ closeModal,
+ setTwoFactorAuth,
+ verifyTwoFactorAuthToken,
+ generateTwoFactorQRCode,
+ twoFactorAuthTokenError,
+ },
+ dispatch
+ );
+
+export default connect(mapStateToProps, mapDispatchToProps)(TwoFactorAuthForm);
diff --git a/admin-dashboard/src/components/user/UserBlockBox.js b/admin-dashboard/src/components/user/UserBlockBox.js
index 2a04809500..6a259f89fe 100644
--- a/admin-dashboard/src/components/user/UserBlockBox.js
+++ b/admin-dashboard/src/components/user/UserBlockBox.js
@@ -112,7 +112,7 @@ UserBlockBox.propTypes = {
blockUser: PropTypes.func.isRequired,
closeModal: PropTypes.func,
openModal: PropTypes.func.isRequired,
- userId: PropTypes.string.isRequired,
+ userId: PropTypes.string,
};
UserBlockBox.contextTypes = {
diff --git a/admin-dashboard/src/components/user/UserProject.js b/admin-dashboard/src/components/user/UserProject.js
index 786b42a340..ebc25ca844 100644
--- a/admin-dashboard/src/components/user/UserProject.js
+++ b/admin-dashboard/src/components/user/UserProject.js
@@ -69,7 +69,7 @@ const mapStateToProps = state => {
UserProject.propTypes = {
fetchUserProjects: PropTypes.func.isRequired,
userId: PropTypes.string,
- projects: PropTypes.array,
+ projects: PropTypes.object,
};
export default connect(mapStateToProps, mapDispatchToProps)(UserProject);
diff --git a/admin-dashboard/src/components/user/UserSetting.js b/admin-dashboard/src/components/user/UserSetting.js
index d7af51f84a..6733a1e341 100644
--- a/admin-dashboard/src/components/user/UserSetting.js
+++ b/admin-dashboard/src/components/user/UserSetting.js
@@ -3,6 +3,10 @@ import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import ShouldRender from '../basic/ShouldRender';
import PropTypes from 'prop-types';
+import DataPathHoC from '../DataPathHoC';
+import TwoFactorAuthModal from './TwoFactorAuth';
+import { updateTwoFactorAuthToken, setTwoFactorAuth } from '../../actions/user';
+import { openModal } from '../../actions/modal';
export class UserSetting extends Component {
constructor(props) {
@@ -16,7 +20,35 @@ export class UserSetting extends Component {
}
}
+ handleChange = () => {
+ const {
+ user,
+ updateTwoFactorAuthToken,
+ openModal,
+ setTwoFactorAuth,
+ } = this.props;
+ if (user.twoFactorAuthEnabled) {
+ updateTwoFactorAuthToken({
+ twoFactorAuthEnabled: false,
+ email: user.email,
+ }).then(() => {
+ setTwoFactorAuth(!user.twoFactorAuthEnabled);
+ });
+ } else {
+ openModal({
+ twoFactorAuthId: user.id,
+ onClose: () => '',
+ content: DataPathHoC(TwoFactorAuthModal, {}),
+ });
+ }
+ };
+
render() {
+ let { twoFactorAuthEnabled } = this.props.user;
+ if (twoFactorAuthEnabled === undefined) {
+ twoFactorAuthEnabled = false;
+ }
+
return (
@@ -121,6 +153,34 @@ export class UserSetting extends Component {
+
+
+
+
+
+
@@ -156,7 +216,10 @@ export class UserSetting extends Component {
UserSetting.displayName = 'UserSetting';
const mapDispatchToProps = dispatch => {
- return bindActionCreators({}, dispatch);
+ return bindActionCreators(
+ { updateTwoFactorAuthToken, openModal, setTwoFactorAuth },
+ dispatch
+ );
};
function mapStateToProps(state) {
@@ -169,6 +232,9 @@ function mapStateToProps(state) {
UserSetting.propTypes = {
user: PropTypes.object.isRequired,
+ updateTwoFactorAuthToken: PropTypes.func,
+ openModal: PropTypes.func,
+ setTwoFactorAuth: PropTypes.func,
};
UserSetting.contextTypes = {
diff --git a/admin-dashboard/src/constants/user.js b/admin-dashboard/src/constants/user.js
index e7e71cc069..ed835a129e 100644
--- a/admin-dashboard/src/constants/user.js
+++ b/admin-dashboard/src/constants/user.js
@@ -61,3 +61,14 @@ export const SEARCH_USERS_REQUEST = 'SEARCH_USERS_REQUEST';
export const SEARCH_USERS_RESET = 'SEARCH_USERS_RESET';
export const SEARCH_USERS_SUCCESS = 'SEARCH_USERS_SUCCESS';
export const SEARCH_USERS_FAILURE = 'SEARCH_USERS_FAILURE';
+
+//update user's two factor auth settings
+export const UPDATE_TWO_FACTOR_AUTH_REQUEST = 'UPDATE_TWO_FACTOR_AUTH_REQUEST';
+export const UPDATE_TWO_FACTOR_AUTH_SUCCESS = 'UPDATE_TWO_FACTOR_AUTH_SUCCESS';
+export const UPDATE_TWO_FACTOR_AUTH_FAILURE = 'UPDATE_TWO_FACTOR_AUTH_FAILURE';
+export const SET_TWO_FACTOR_AUTH = 'SET_TWO_FACTOR_AUTH';
+
+// Generate QR code
+export const GENERATE_TWO_FACTOR_QR_REQUEST = 'GENERATE_TWO_FACTOR_QR_REQUEST';
+export const GENERATE_TWO_FACTOR_QR_SUCCESS = 'GENERATE_TWO_FACTOR_QR_SUCCESS';
+export const GENERATE_TWO_FACTOR_QR_FAILURE = 'GENERATE_TWO_FACTOR_QR_FAILURE';
diff --git a/admin-dashboard/src/reducers/user.js b/admin-dashboard/src/reducers/user.js
index 7240ae4c4e..715c368330 100644
--- a/admin-dashboard/src/reducers/user.js
+++ b/admin-dashboard/src/reducers/user.js
@@ -39,6 +39,13 @@ import {
SEARCH_USERS_RESET,
SEARCH_USERS_SUCCESS,
SEARCH_USERS_FAILURE,
+ UPDATE_TWO_FACTOR_AUTH_REQUEST,
+ UPDATE_TWO_FACTOR_AUTH_SUCCESS,
+ UPDATE_TWO_FACTOR_AUTH_FAILURE,
+ SET_TWO_FACTOR_AUTH,
+ GENERATE_TWO_FACTOR_QR_REQUEST,
+ GENERATE_TWO_FACTOR_QR_SUCCESS,
+ GENERATE_TWO_FACTOR_QR_FAILURE,
} from '../constants/user';
const INITIAL_STATE = {
@@ -98,6 +105,18 @@ const INITIAL_STATE = {
error: null,
success: false,
},
+ twoFactorAuthSetting: {
+ error: null,
+ requesting: false,
+ success: false,
+ data: {},
+ },
+ qrCode: {
+ error: null,
+ requesting: false,
+ success: false,
+ data: {},
+ },
};
export default function user(state = INITIAL_STATE, action) {
@@ -524,6 +543,80 @@ export default function user(state = INITIAL_STATE, action) {
...INITIAL_STATE,
});
+ //update user's two factor auth settings
+ case UPDATE_TWO_FACTOR_AUTH_REQUEST:
+ return Object.assign({}, state, {
+ twoFactorAuthSetting: {
+ requesting: true,
+ error: null,
+ success: false,
+ data: state.twoFactorAuthSetting.data,
+ },
+ });
+
+ case UPDATE_TWO_FACTOR_AUTH_SUCCESS:
+ return Object.assign({}, state, {
+ user: {
+ ...INITIAL_STATE.user,
+ user: action.payload,
+ },
+ twoFactorAuthSetting: {
+ requesting: false,
+ error: null,
+ success: false,
+ data: state.twoFactorAuthSetting.data,
+ },
+ });
+
+ case UPDATE_TWO_FACTOR_AUTH_FAILURE:
+ return Object.assign({}, state, {
+ twoFactorAuthSetting: {
+ requesting: false,
+ error: action.payload,
+ success: false,
+ data: state.twoFactorAuthSetting.data,
+ },
+ });
+
+ case SET_TWO_FACTOR_AUTH:
+ return Object.assign({}, state, {
+ user: {
+ ...state.user,
+ twoFactorAuthEnabled: action.payload,
+ },
+ });
+
+ //generate user's QR code
+ case GENERATE_TWO_FACTOR_QR_REQUEST:
+ return Object.assign({}, state, {
+ qrCode: {
+ requesting: true,
+ error: null,
+ success: false,
+ data: state.qrCode.data,
+ },
+ });
+
+ case GENERATE_TWO_FACTOR_QR_SUCCESS:
+ return Object.assign({}, state, {
+ qrCode: {
+ requesting: false,
+ error: null,
+ success: false,
+ data: action.payload,
+ },
+ });
+
+ case GENERATE_TWO_FACTOR_QR_FAILURE:
+ return Object.assign({}, state, {
+ qrCode: {
+ requesting: false,
+ error: action.payload,
+ success: false,
+ data: state.qrCode.data,
+ },
+ });
+
default:
return state;
}
diff --git a/admin-dashboard/src/test/Users.test.enterprise.js b/admin-dashboard/src/test/Users.test.enterprise.js
index 8d30368efc..9e05d8404f 100644
--- a/admin-dashboard/src/test/Users.test.enterprise.js
+++ b/admin-dashboard/src/test/Users.test.enterprise.js
@@ -91,4 +91,82 @@ describe('Users Component (IS_SAAS_SERVICE=false)', () => {
},
operationTimeOut
);
+
+ test(
+ 'Should not activate google authenticator if the verification code field is empty',
+ async () => {
+ return await cluster.execute(null, async ({ page }) => {
+ // visit the dashboard
+ await page.goto(utils.ADMIN_DASHBOARD_URL, {
+ waitUntil: 'networkidle0',
+ });
+ await page.waitForSelector(
+ '.bs-ObjectList-rows > a:nth-child(2)'
+ );
+ await page.click('.bs-ObjectList-rows > a:nth-child(2)');
+ await page.waitFor(5000);
+
+ // toggle the google authenticator
+ await page.$eval('input[name=twoFactorAuthEnabled]', e =>
+ e.click()
+ );
+
+ // click on the next button
+ await page.waitForSelector('#nextFormButton');
+ await page.click('#nextFormButton');
+
+ // click the verification button
+ await page.waitForSelector('#enableTwoFactorAuthButton');
+ await page.click('#enableTwoFactorAuthButton');
+
+ // verify there is an error message
+ let spanElement = await page.waitForSelector('.field-error');
+ spanElement = await spanElement.getProperty('innerText');
+ spanElement = await spanElement.jsonValue();
+ spanElement.should.be.exactly('Auth token is required.');
+ });
+ },
+ operationTimeOut
+ );
+
+ test(
+ 'Should not activate google authenticator if the verification code is invalid',
+ async () => {
+ return await cluster.execute(null, async ({ page }) => {
+ // visit the dashboard
+ await page.goto(utils.ADMIN_DASHBOARD_URL, {
+ waitUntil: 'networkidle0',
+ });
+ await page.waitForSelector(
+ '.bs-ObjectList-rows > a:nth-child(2)'
+ );
+ await page.click('.bs-ObjectList-rows > a:nth-child(2)');
+ await page.waitFor(5000);
+
+ // toggle the google authenticator
+ await page.$eval('input[name=twoFactorAuthEnabled]', e =>
+ e.click()
+ );
+
+ // click on the next button
+ await page.waitForSelector('#nextFormButton');
+ await page.click('#nextFormButton');
+
+ // enter a random verification code
+ await page.waitForSelector('input[name=token]');
+ await page.type('input[name=token]', '021196');
+
+ // click the verification button
+ await page.waitForSelector('#enableTwoFactorAuthButton');
+ await page.click('#enableTwoFactorAuthButton');
+
+ // verify there is an error message
+ let spanElement = await page.waitForSelector('#modal-message');
+ spanElement = await spanElement.getProperty('innerText');
+ spanElement = await spanElement.jsonValue();
+ spanElement.should.be.exactly('Invalid token.');
+ });
+ },
+ operationTimeOut
+ );
});