Merge branch 'fix-sort-users-by-last-active' into 'master'

adds a link back to enter auth token form

See merge request fyipe-project/app!826
This commit is contained in:
Nawaz Dhandala 2020-09-11 08:53:52 +00:00
commit 8eedf4c67f
11 changed files with 749 additions and 3 deletions

View File

@ -107,6 +107,15 @@ export class VerifyBackupCode extends Component {
</form>
</div>
</div>
<div className="below-box">
<p>
Have a google app authenticator?{' '}
<Link to="/accounts/user-auth/token">
Enter auth token
</Link>
.
</p>
</div>
<div id="footer_spacer" />
<div id="bottom">
<ul>

View File

@ -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",

View File

@ -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",

View File

@ -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;
};
}

View File

@ -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 (
<form onSubmit={handleSubmit(this.submitForm)}>
<div
onKeyDown={this.handleKeyBoard}
className="ModalLayer-wash Box-root Flex-flex Flex-alignItems--flexStart Flex-justifyContent--center"
>
<div
className="ModalLayer-contents"
tabIndex={-1}
style={{ marginTop: 40 }}
>
<div className="bs-BIM">
<div className="bs-Modal bs-Modal--medium">
<div className="bs-Modal-header">
<div className="bs-Modal-header-copy">
<span className="Text-color--inherit Text-display--inline Text-fontSize--20 Text-fontWeight--medium Text-lineHeight--24 Text-typeface--base Text-wrap--wrap">
<span>
Two Factor Authentication
</span>
</span>
</div>
<div className="bs-Modal-messages">
<ShouldRender
if={twoFactorAuthSetting.error}
>
<p
className="bs-Modal-message"
id="modal-message"
>
{twoFactorAuthSetting.error}
</p>
</ShouldRender>
</div>
</div>
<div>
<div className="bs-Fieldset-wrapper Box-root Margin-bottom--2">
<div className="bs-u-paddingless">
<div className="bs-Modal-block">
<div>
{next ? (
<div>
<div
className="bs-Fieldset-wrapper Box-root"
style={{
width:
'90%',
margin:
'1px 0 6px 2%',
}}
>
<p>
Input a
token from
your mobile
device to
complete
setup
</p>
</div>
<div className="bs-Modal-body">
<Field
className="bs-TextInput"
component={
RenderField
}
name="token"
id="token"
placeholder="Verification token"
disabled={
twoFactorAuthSetting.requesting
}
style={{
width:
'90%',
margin:
'5px 0 10px 2%',
}}
/>
</div>
</div>
) : (
<div className="bs-Fieldset-wrapper Box-root">
<div
className="bs-Fieldset-wrapper Box-root"
style={{
marginBottom:
'10px',
marginTop:
'-5px',
}}
>
<p>
Download the
Google
Authenticator
Mobile app
on your
mobile
device
<span>
{' '}
(
<a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en">
Android
</a>
,
<a href="https://apps.apple.com/us/app/google-authenticator/id388497605">
{' '}
IOS
</a>
)
</span>{' '}
and then
scan the QR
code below
to set up
Two-factor
Authentication
with an
Authenticator
app.
</p>
</div>
{qrCode.data
.otpauth_url ? (
<QRCode
size={230}
value={`${qrCode.data.otpauth_url}`}
style={{
display:
'block',
margin:
'0 auto',
}}
/>
) : (
<ListLoader />
)}
</div>
)}
</div>
</div>
</div>
</div>
</div>
<div className="bs-Modal-footer">
<div className="bs-Modal-footer-actions">
<button
className={`bs-Button bs-DeprecatedButton ${twoFactorAuthSetting.requesting &&
'bs-is-disabled'}`}
type="button"
onClick={() => {
this.props.twoFactorAuthTokenError(
''
);
this.props.closeModal({
id: this.props
.twoFactorAuthId,
});
}}
disabled={
twoFactorAuthSetting.requesting
}
>
<span>Cancel</span>
</button>
{!next ? (
<button
id="nextFormButton"
className={`bs-Button bs-DeprecatedButton bs-Button--blue ${twoFactorAuthSetting.requesting &&
'bs-is-disabled'}`}
disabled={
twoFactorAuthSetting.requesting
}
onClick={event =>
this.nextHandler(event)
}
type="button"
>
<ShouldRender
if={
twoFactorAuthSetting.requesting
}
>
<Spinner />
</ShouldRender>
<span>Next</span>
</button>
) : (
<button
id="enableTwoFactorAuthButton"
className={`bs-Button bs-DeprecatedButton bs-Button--blue ${twoFactorAuthSetting.requesting &&
'bs-is-disabled'}`}
type="submit"
disabled={
twoFactorAuthSetting.requesting
}
>
<ShouldRender
if={
twoFactorAuthSetting.requesting
}
>
<Spinner />
</ShouldRender>
<span>Verify</span>
</button>
)}
</div>
</div>
</div>
</div>
</div>
</div>
</form>
);
}
}
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);

View File

@ -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 = {

View File

@ -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);

View File

@ -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 (
<div className="bs-ContentSection Card-root Card-shadow--medium">
<div className="Box-root">
@ -121,6 +153,34 @@ export class UserSetting extends Component {
</span>
</div>
</div>
<div className="bs-Fieldset-row">
<label className="bs-Fieldset-label">
Two Factor Authentication <br />{' '}
by Google Authenticator
</label>
<div className="bs-Fieldset-fields">
<label
className="Toggler-wrap"
style={{
marginTop: '10px',
}}
>
<input
className="btn-toggler"
type="checkbox"
onChange={
this.handleChange
}
name="twoFactorAuthEnabled"
id="twoFactorAuthEnabled"
checked={
twoFactorAuthEnabled
}
/>
<span className="TogglerBtn-slider round"></span>
</label>
</div>
</div>
</div>
</fieldset>
</div>
@ -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 = {

View File

@ -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';

View File

@ -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;
}

View File

@ -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
);
});