mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 17:05:18 +00:00
Allows users to update their email address (#1232)
This commit is contained in:
parent
1bb1c959ae
commit
cc37cfa828
@ -0,0 +1,45 @@
|
|||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import helpers from 'components/EmailTokenRedirect/helpers';
|
||||||
|
import userInterface from 'interfaces/user';
|
||||||
|
|
||||||
|
export class EmailTokenRedirect extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
token: PropTypes.string.isRequired,
|
||||||
|
user: userInterface,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentWillMount () {
|
||||||
|
const { dispatch, token, user } = this.props;
|
||||||
|
|
||||||
|
return helpers.confirmEmailChange(dispatch, token, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps (nextProps) {
|
||||||
|
const { dispatch, token: newToken, user: newUser } = nextProps;
|
||||||
|
const { token: oldToken, user: oldUser } = this.props;
|
||||||
|
|
||||||
|
const missingProps = !oldToken || !oldUser;
|
||||||
|
|
||||||
|
if (missingProps) {
|
||||||
|
return helpers.confirmEmailChange(dispatch, newToken, newUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { params }) => {
|
||||||
|
const { token } = params;
|
||||||
|
const { user } = state.auth;
|
||||||
|
|
||||||
|
return { token, user };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(EmailTokenRedirect);
|
@ -0,0 +1,67 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import expect, { spyOn, restoreSpies } from 'expect';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
|
|
||||||
|
import { connectedComponent, reduxMockStore } from 'test/helpers';
|
||||||
|
import ConnectedEmailTokenRedirect, { EmailTokenRedirect } from 'components/EmailTokenRedirect/EmailTokenRedirect';
|
||||||
|
import Kolide from 'kolide';
|
||||||
|
import { userStub } from 'test/stubs';
|
||||||
|
|
||||||
|
describe('EmailTokenRedirect - component', () => {
|
||||||
|
afterEach(restoreSpies);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(Kolide.users, 'confirmEmailChange')
|
||||||
|
.andReturn(Promise.resolve({ ...userStub, email: 'new@email.com' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
const authStore = {
|
||||||
|
auth: {
|
||||||
|
user: userStub,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const token = 'KFBR392';
|
||||||
|
const defaultProps = {
|
||||||
|
params: {
|
||||||
|
token,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('componentWillMount', () => {
|
||||||
|
it('calls the API when a token and user are present', () => {
|
||||||
|
const mockStore = reduxMockStore(authStore);
|
||||||
|
|
||||||
|
mount(connectedComponent(ConnectedEmailTokenRedirect, {
|
||||||
|
mockStore,
|
||||||
|
props: defaultProps,
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(Kolide.users.confirmEmailChange).toHaveBeenCalledWith(userStub, token);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not call the API when only a token is present', () => {
|
||||||
|
const mockStore = reduxMockStore({ auth: {} });
|
||||||
|
|
||||||
|
mount(connectedComponent(ConnectedEmailTokenRedirect, {
|
||||||
|
mockStore,
|
||||||
|
props: defaultProps,
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(Kolide.users.confirmEmailChange).toNotHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('componentWillReceiveProps', () => {
|
||||||
|
it('calls the API when a user is received', () => {
|
||||||
|
const mockStore = reduxMockStore();
|
||||||
|
const props = { dispatch: mockStore.dispatch, token };
|
||||||
|
const Component = mount(<EmailTokenRedirect {...props} />);
|
||||||
|
|
||||||
|
expect(Kolide.users.confirmEmailChange).toNotHaveBeenCalled();
|
||||||
|
|
||||||
|
Component.setProps({ user: userStub });
|
||||||
|
|
||||||
|
expect(Kolide.users.confirmEmailChange).toHaveBeenCalledWith(userStub, token);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
25
frontend/components/EmailTokenRedirect/helpers.js
Normal file
25
frontend/components/EmailTokenRedirect/helpers.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import PATHS from 'router/paths';
|
||||||
|
import { push } from 'react-router-redux';
|
||||||
|
import { renderFlash } from 'redux/nodes/notifications/actions';
|
||||||
|
import userActions from 'redux/nodes/entities/users/actions';
|
||||||
|
|
||||||
|
const confirmEmailChange = (dispatch, token, user) => {
|
||||||
|
if (user && token) {
|
||||||
|
return dispatch(userActions.confirmEmailChange(user, token))
|
||||||
|
.then(() => {
|
||||||
|
dispatch(push(PATHS.USER_SETTINGS));
|
||||||
|
dispatch(renderFlash('success', 'Email updated successfully!'));
|
||||||
|
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
dispatch(push(PATHS.LOGIN));
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default { confirmEmailChange };
|
122
frontend/components/EmailTokenRedirect/helpers.tests.js
Normal file
122
frontend/components/EmailTokenRedirect/helpers.tests.js
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import expect, { spyOn, restoreSpies } from 'expect';
|
||||||
|
import { reduxMockStore } from 'test/helpers';
|
||||||
|
|
||||||
|
import helpers from 'components/EmailTokenRedirect/helpers';
|
||||||
|
import Kolide from 'kolide';
|
||||||
|
import { userStub } from 'test/stubs';
|
||||||
|
|
||||||
|
describe('EmailTokenRedirect - helpers', () => {
|
||||||
|
afterEach(restoreSpies);
|
||||||
|
|
||||||
|
describe('#confirmEmailChage', () => {
|
||||||
|
const { confirmEmailChange } = helpers;
|
||||||
|
const token = 'KFBR392';
|
||||||
|
const authStore = {
|
||||||
|
auth: {
|
||||||
|
user: userStub,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('successfully dispatching the confirmEmailChange action', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(Kolide.users, 'confirmEmailChange')
|
||||||
|
.andReturn(Promise.resolve({ ...userStub, email: 'new@email.com' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('pushes the user to the settings page', (done) => {
|
||||||
|
const mockStore = reduxMockStore(authStore);
|
||||||
|
const { dispatch } = mockStore;
|
||||||
|
|
||||||
|
confirmEmailChange(dispatch, userStub, token)
|
||||||
|
.then(() => {
|
||||||
|
const dispatchedActions = mockStore.getActions();
|
||||||
|
|
||||||
|
expect(dispatchedActions).toInclude({
|
||||||
|
type: '@@router/CALL_HISTORY_METHOD',
|
||||||
|
payload: {
|
||||||
|
method: 'push',
|
||||||
|
args: ['/settings'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('unsuccessfully dispatching the confirmEmailChange action', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const errors = [
|
||||||
|
{
|
||||||
|
name: 'base',
|
||||||
|
reason: 'Unable to confirm your email address',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const errorResponse = {
|
||||||
|
status: 422,
|
||||||
|
message: {
|
||||||
|
message: 'Unable to confirm email address',
|
||||||
|
errors,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
spyOn(Kolide.users, 'confirmEmailChange')
|
||||||
|
.andReturn(Promise.reject(errorResponse));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('pushes the user to the login page', (done) => {
|
||||||
|
const mockStore = reduxMockStore(authStore);
|
||||||
|
const { dispatch } = mockStore;
|
||||||
|
|
||||||
|
confirmEmailChange(dispatch, userStub, token)
|
||||||
|
.then(done)
|
||||||
|
.catch(() => {
|
||||||
|
const dispatchedActions = mockStore.getActions();
|
||||||
|
|
||||||
|
expect(dispatchedActions).toInclude({
|
||||||
|
type: '@@router/CALL_HISTORY_METHOD',
|
||||||
|
payload: {
|
||||||
|
method: 'push',
|
||||||
|
args: ['/login'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the user or token are not present', () => {
|
||||||
|
it('does not dispatch any actions when the user is not present', (done) => {
|
||||||
|
const mockStore = reduxMockStore(authStore);
|
||||||
|
const { dispatch } = mockStore;
|
||||||
|
|
||||||
|
confirmEmailChange(dispatch, undefined, token)
|
||||||
|
.then(() => {
|
||||||
|
const dispatchedActions = mockStore.getActions();
|
||||||
|
|
||||||
|
expect(dispatchedActions).toEqual([]);
|
||||||
|
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not dispatch any actions when the token is not present', (done) => {
|
||||||
|
const mockStore = reduxMockStore(authStore);
|
||||||
|
const { dispatch } = mockStore;
|
||||||
|
|
||||||
|
confirmEmailChange(dispatch, userStub, undefined)
|
||||||
|
.then(() => {
|
||||||
|
const dispatchedActions = mockStore.getActions();
|
||||||
|
|
||||||
|
expect(dispatchedActions).toEqual([]);
|
||||||
|
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
1
frontend/components/EmailTokenRedirect/index.js
Normal file
1
frontend/components/EmailTokenRedirect/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default from './EmailTokenRedirect';
|
@ -0,0 +1,53 @@
|
|||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
|
||||||
|
import Button from 'components/buttons/Button';
|
||||||
|
import Form from 'components/forms/Form';
|
||||||
|
import formFieldInterface from 'interfaces/form_field';
|
||||||
|
import InputField from 'components/forms/fields/InputField';
|
||||||
|
|
||||||
|
const baseClass = 'change-email-form';
|
||||||
|
|
||||||
|
class ChangeEmailForm extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
fields: PropTypes.shape({
|
||||||
|
password: formFieldInterface.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
handleSubmit: PropTypes.func.isRequired,
|
||||||
|
onCancel: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { fields, handleSubmit, onCancel } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<InputField
|
||||||
|
{...fields.password}
|
||||||
|
autofocus
|
||||||
|
label="Password"
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
<div className={`${baseClass}__btn-wrap`}>
|
||||||
|
<Button className={`${baseClass}__btn`} type="submit" variant="brand">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onCancel} variant="inverse" className={`${baseClass}__btn`}>Cancel</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Form(ChangeEmailForm, {
|
||||||
|
fields: ['password'],
|
||||||
|
validate: (formData) => {
|
||||||
|
if (!formData.password) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
errors: { password: 'Password must be present' },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true, errors: {} };
|
||||||
|
},
|
||||||
|
});
|
15
frontend/components/forms/ChangeEmailForm/_styles.scss
Normal file
15
frontend/components/forms/ChangeEmailForm/_styles.scss
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
.change-email-form {
|
||||||
|
&__btn-wrap {
|
||||||
|
@include display(flex);
|
||||||
|
@include flex-direction(row-reverse);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__btn {
|
||||||
|
font-size: $small;
|
||||||
|
height: 38px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
margin-left: 15px;
|
||||||
|
padding: 0;
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
}
|
1
frontend/components/forms/ChangeEmailForm/index.js
Normal file
1
frontend/components/forms/ChangeEmailForm/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default from './ChangeEmailForm';
|
@ -18,11 +18,27 @@ class UserSettingsForm extends Component {
|
|||||||
username: formFieldInterface.isRequired,
|
username: formFieldInterface.isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
handleSubmit: PropTypes.func.isRequired,
|
handleSubmit: PropTypes.func.isRequired,
|
||||||
|
pendingEmail: PropTypes.string,
|
||||||
onCancel: PropTypes.func.isRequired,
|
onCancel: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
renderEmailHint = () => {
|
||||||
|
const { pendingEmail } = this.props;
|
||||||
|
|
||||||
|
if (!pendingEmail) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<i className={`${baseClass}__email-hint`}>
|
||||||
|
Pending change to <b>{pendingEmail}</b>
|
||||||
|
</i>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { fields, handleSubmit, onCancel } = this.props;
|
const { fields, handleSubmit, onCancel } = this.props;
|
||||||
|
const { renderEmailHint } = this;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className={baseClass}>
|
<form onSubmit={handleSubmit} className={baseClass}>
|
||||||
@ -34,6 +50,7 @@ class UserSettingsForm extends Component {
|
|||||||
<InputField
|
<InputField
|
||||||
{...fields.email}
|
{...fields.email}
|
||||||
label="Email (required)"
|
label="Email (required)"
|
||||||
|
hint={renderEmailHint()}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
{...fields.name}
|
{...fields.name}
|
||||||
|
@ -3,6 +3,10 @@
|
|||||||
width: 75%;
|
width: 75%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__email-hint {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
&__button-wrap {
|
&__button-wrap {
|
||||||
width: 75%;
|
width: 75%;
|
||||||
border-top: 1px solid $accent-medium;
|
border-top: 1px solid $accent-medium;
|
||||||
|
@ -6,6 +6,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
.input-field {
|
.input-field {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,9 @@ export default {
|
|||||||
CHANGE_PASSWORD: '/v1/kolide/change_password',
|
CHANGE_PASSWORD: '/v1/kolide/change_password',
|
||||||
CONFIG: '/v1/kolide/config',
|
CONFIG: '/v1/kolide/config',
|
||||||
CONFIG_OPTIONS: '/v1/kolide/options',
|
CONFIG_OPTIONS: '/v1/kolide/options',
|
||||||
|
CONFIRM_EMAIL_CHANGE: (token) => {
|
||||||
|
return `/v1/kolide/email/change/${token}`;
|
||||||
|
},
|
||||||
ENABLE_USER: (id) => {
|
ENABLE_USER: (id) => {
|
||||||
return `/v1/kolide/users/${id}/enable`;
|
return `/v1/kolide/users/${id}/enable`;
|
||||||
},
|
},
|
||||||
|
@ -204,6 +204,15 @@ class Kolide extends Base {
|
|||||||
|
|
||||||
return this.authenticatedPost(this.endpoint(CHANGE_PASSWORD), JSON.stringify(passwordParams));
|
return this.authenticatedPost(this.endpoint(CHANGE_PASSWORD), JSON.stringify(passwordParams));
|
||||||
},
|
},
|
||||||
|
confirmEmailChange: (user, token) => {
|
||||||
|
const { CONFIRM_EMAIL_CHANGE } = endpoints;
|
||||||
|
const endpoint = this.endpoint(CONFIRM_EMAIL_CHANGE(token));
|
||||||
|
|
||||||
|
return this.authenticatedGet(endpoint)
|
||||||
|
.then((response) => {
|
||||||
|
return { ...user, email: response.new_email };
|
||||||
|
});
|
||||||
|
},
|
||||||
enable: (user, { enabled }) => {
|
enable: (user, { enabled }) => {
|
||||||
const { ENABLE_USER } = endpoints;
|
const { ENABLE_USER } = endpoints;
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ const {
|
|||||||
invalidForgotPasswordRequest,
|
invalidForgotPasswordRequest,
|
||||||
invalidResetPasswordRequest,
|
invalidResetPasswordRequest,
|
||||||
validChangePasswordRequest,
|
validChangePasswordRequest,
|
||||||
|
validConfirmEmailChangeRequest,
|
||||||
validCreateLabelRequest,
|
validCreateLabelRequest,
|
||||||
validCreateLicenseRequest,
|
validCreateLicenseRequest,
|
||||||
validCreatePackRequest,
|
validCreatePackRequest,
|
||||||
@ -544,6 +545,23 @@ describe('Kolide - API client', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#confirmEmailChange', () => {
|
||||||
|
it('calls the appropriate endpoint with the correct parameters', (done) => {
|
||||||
|
const token = 'KFBR392';
|
||||||
|
const request = validConfirmEmailChangeRequest(bearerToken, token);
|
||||||
|
|
||||||
|
Kolide.setBearerToken(bearerToken);
|
||||||
|
Kolide.users.confirmEmailChange(userStub, token)
|
||||||
|
.then(() => {
|
||||||
|
expect(request.isDone()).toEqual(true);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
throw new Error('Expected request to have been stubbed');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('#enable', () => {
|
describe('#enable', () => {
|
||||||
it('calls the appropriate endpoint with the correct parameters', (done) => {
|
it('calls the appropriate endpoint with the correct parameters', (done) => {
|
||||||
const enableParams = { enabled: true };
|
const enableParams = { enabled: true };
|
||||||
|
@ -5,6 +5,7 @@ import moment from 'moment';
|
|||||||
|
|
||||||
import Avatar from 'components/Avatar';
|
import Avatar from 'components/Avatar';
|
||||||
import Button from 'components/buttons/Button';
|
import Button from 'components/buttons/Button';
|
||||||
|
import ChangeEmailForm from 'components/forms/ChangeEmailForm';
|
||||||
import ChangePasswordForm from 'components/forms/ChangePasswordForm';
|
import ChangePasswordForm from 'components/forms/ChangePasswordForm';
|
||||||
import deepDifference from 'utilities/deep_difference';
|
import deepDifference from 'utilities/deep_difference';
|
||||||
import Icon from 'components/icons/Icon';
|
import Icon from 'components/icons/Icon';
|
||||||
@ -35,7 +36,12 @@ export class UserSettingsPage extends Component {
|
|||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = { showModal: false };
|
this.state = {
|
||||||
|
pendingEmail: undefined,
|
||||||
|
showEmailModal: false,
|
||||||
|
showPasswordModal: false,
|
||||||
|
updatedUser: {},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onCancel = (evt) => {
|
onCancel = (evt) => {
|
||||||
@ -61,17 +67,28 @@ export class UserSettingsPage extends Component {
|
|||||||
onShowModal = (evt) => {
|
onShowModal = (evt) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
|
||||||
this.setState({ showModal: true });
|
this.setState({ showPasswordModal: true });
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onToggleModal = (evt) => {
|
onToggleEmailModal = (updatedUser = {}) => {
|
||||||
|
const { showEmailModal } = this.state;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
showEmailModal: !showEmailModal,
|
||||||
|
updatedUser,
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onTogglePasswordModal = (evt) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
|
||||||
const { showModal } = this.state;
|
const { showPasswordModal } = this.state;
|
||||||
|
|
||||||
this.setState({ showModal: !showModal });
|
this.setState({ showPasswordModal: !showPasswordModal });
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -80,9 +97,19 @@ export class UserSettingsPage extends Component {
|
|||||||
const { dispatch, user } = this.props;
|
const { dispatch, user } = this.props;
|
||||||
const updatedUser = deepDifference(formData, user);
|
const updatedUser = deepDifference(formData, user);
|
||||||
|
|
||||||
|
if (updatedUser.email && !updatedUser.password) {
|
||||||
|
return this.onToggleEmailModal(updatedUser);
|
||||||
|
}
|
||||||
|
|
||||||
return dispatch(updateUser(user, updatedUser))
|
return dispatch(updateUser(user, updatedUser))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return dispatch(renderFlash('success', 'Account updated!'));
|
if (updatedUser.email) {
|
||||||
|
this.setState({ pendingEmail: updatedUser.email });
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(renderFlash('success', 'Account updated!'));
|
||||||
|
|
||||||
|
return true;
|
||||||
})
|
})
|
||||||
.catch(() => false);
|
.catch(() => false);
|
||||||
}
|
}
|
||||||
@ -93,29 +120,60 @@ export class UserSettingsPage extends Component {
|
|||||||
return dispatch(userActions.changePassword(user, formData))
|
return dispatch(userActions.changePassword(user, formData))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
dispatch(renderFlash('success', 'Password changed successfully'));
|
dispatch(renderFlash('success', 'Password changed successfully'));
|
||||||
this.setState({ showModal: false });
|
this.setState({ showPasswordModal: false });
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderModal = () => {
|
renderEmailModal = () => {
|
||||||
const { userErrors } = this.props;
|
const { errors } = this.props;
|
||||||
const { showModal } = this.state;
|
const { updatedUser, showEmailModal } = this.state;
|
||||||
const { handleSubmitPasswordForm, onToggleModal } = this;
|
const { handleSubmit, onToggleEmailModal } = this;
|
||||||
|
|
||||||
if (!showModal) {
|
const emailSubmit = (formData) => {
|
||||||
|
handleSubmit(formData)
|
||||||
|
.then((r) => {
|
||||||
|
return r ? onToggleEmailModal() : false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!showEmailModal) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title="To change your email you must supply your password"
|
||||||
|
onExit={onToggleEmailModal}
|
||||||
|
>
|
||||||
|
<ChangeEmailForm
|
||||||
|
formData={updatedUser}
|
||||||
|
handleSubmit={emailSubmit}
|
||||||
|
onCancel={onToggleEmailModal}
|
||||||
|
serverErrors={errors}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPasswordModal = () => {
|
||||||
|
const { userErrors } = this.props;
|
||||||
|
const { showPasswordModal } = this.state;
|
||||||
|
const { handleSubmitPasswordForm, onTogglePasswordModal } = this;
|
||||||
|
|
||||||
|
if (!showPasswordModal) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title="Change Password"
|
title="Change Password"
|
||||||
onExit={onToggleModal}
|
onExit={onTogglePasswordModal}
|
||||||
>
|
>
|
||||||
<ChangePasswordForm
|
<ChangePasswordForm
|
||||||
handleSubmit={handleSubmitPasswordForm}
|
handleSubmit={handleSubmitPasswordForm}
|
||||||
onCancel={onToggleModal}
|
onCancel={onTogglePasswordModal}
|
||||||
serverErrors={userErrors}
|
serverErrors={userErrors}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
@ -123,8 +181,16 @@ export class UserSettingsPage extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { handleSubmit, onCancel, onLogout, onShowModal, renderModal } = this;
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
onCancel,
|
||||||
|
onLogout,
|
||||||
|
onShowModal,
|
||||||
|
renderEmailModal,
|
||||||
|
renderPasswordModal,
|
||||||
|
} = this;
|
||||||
const { errors, user } = this.props;
|
const { errors, user } = this.props;
|
||||||
|
const { pendingEmail } = this.state;
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return false;
|
return false;
|
||||||
@ -142,6 +208,7 @@ export class UserSettingsPage extends Component {
|
|||||||
formData={user}
|
formData={user}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
|
pendingEmail={pendingEmail}
|
||||||
serverErrors={errors}
|
serverErrors={errors}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -169,7 +236,8 @@ export class UserSettingsPage extends Component {
|
|||||||
LOGOUT
|
LOGOUT
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{renderModal()}
|
{renderEmailModal()}
|
||||||
|
{renderPasswordModal()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -8,18 +8,22 @@ import testHelpers from 'test/helpers';
|
|||||||
import { userStub } from 'test/stubs';
|
import { userStub } from 'test/stubs';
|
||||||
import * as authActions from 'redux/nodes/auth/actions';
|
import * as authActions from 'redux/nodes/auth/actions';
|
||||||
|
|
||||||
const { connectedComponent, reduxMockStore } = testHelpers;
|
const {
|
||||||
|
connectedComponent,
|
||||||
|
fillInFormInput,
|
||||||
|
reduxMockStore,
|
||||||
|
} = testHelpers;
|
||||||
|
|
||||||
describe('UserSettingsPage - component', () => {
|
describe('UserSettingsPage - component', () => {
|
||||||
afterEach(restoreSpies);
|
afterEach(restoreSpies);
|
||||||
|
|
||||||
it('renders a UserSettingsForm component', () => {
|
|
||||||
const store = { auth: { user: userStub }, entities: { users: {} } };
|
const store = { auth: { user: userStub }, entities: { users: {} } };
|
||||||
const mockStore = reduxMockStore(store);
|
const mockStore = reduxMockStore(store);
|
||||||
|
|
||||||
const page = mount(connectedComponent(ConnectedPage, { mockStore }));
|
it('renders a UserSettingsForm component', () => {
|
||||||
|
const Page = mount(connectedComponent(ConnectedPage, { mockStore }));
|
||||||
|
|
||||||
expect(page.find('UserSettingsForm').length).toEqual(1);
|
expect(Page.find('UserSettingsForm').length).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a UserSettingsForm component', () => {
|
it('renders a UserSettingsForm component', () => {
|
||||||
@ -46,4 +50,45 @@ describe('UserSettingsPage - component', () => {
|
|||||||
|
|
||||||
expect(authActions.updateUser).toHaveBeenCalledWith(userStub, updatedAttrs);
|
expect(authActions.updateUser).toHaveBeenCalledWith(userStub, updatedAttrs);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('changing email address', () => {
|
||||||
|
it('renders the ChangeEmailForm when the user changes their email', () => {
|
||||||
|
const Page = mount(connectedComponent(ConnectedPage, { mockStore }));
|
||||||
|
const UserSettingsForm = Page.find('UserSettingsForm');
|
||||||
|
const emailInput = UserSettingsForm.find({ name: 'email' });
|
||||||
|
|
||||||
|
expect(Page.find('ChangeEmailForm').length).toEqual(0, 'Expected the ChangeEmailForm to not render');
|
||||||
|
|
||||||
|
fillInFormInput(emailInput, 'new@email.org');
|
||||||
|
UserSettingsForm.simulate('submit');
|
||||||
|
|
||||||
|
expect(Page.find('ChangeEmailForm').length).toEqual(1, 'Expected the ChangeEmailForm to render');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render the ChangeEmailForm when the user does not change their email', () => {
|
||||||
|
const Page = mount(connectedComponent(ConnectedPage, { mockStore }));
|
||||||
|
const UserSettingsForm = Page.find('UserSettingsForm');
|
||||||
|
const emailInput = UserSettingsForm.find({ name: 'email' });
|
||||||
|
|
||||||
|
expect(Page.find('ChangeEmailForm').length).toEqual(0, 'Expected the ChangeEmailForm to not render');
|
||||||
|
|
||||||
|
fillInFormInput(emailInput, userStub.email);
|
||||||
|
UserSettingsForm.simulate('submit');
|
||||||
|
|
||||||
|
expect(Page.find('ChangeEmailForm').length).toEqual(0, 'Expected the ChangeEmailForm to not render');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays pending email text when the user is pending an email change', () => {
|
||||||
|
const props = { dispatch: noop, user: userStub };
|
||||||
|
const Page = mount(<UserSettingsPage {...props} />);
|
||||||
|
const UserSettingsForm = () => Page.find('UserSettingsForm');
|
||||||
|
const emailHint = () => UserSettingsForm().find('.manage-user__email-hint');
|
||||||
|
|
||||||
|
expect(emailHint().length).toEqual(0, 'Expected the form to not render an email hint');
|
||||||
|
|
||||||
|
Page.setState({ pendingEmail: 'new@email.org' });
|
||||||
|
|
||||||
|
expect(emailHint().length).toEqual(1, 'Expected the form to render an email hint');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -292,7 +292,7 @@ const reduxConfig = ({
|
|||||||
|
|
||||||
dispatch(updateFailure(errorsObject));
|
dispatch(updateFailure(errorsObject));
|
||||||
|
|
||||||
throw errorsObject;
|
throw response;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -2,6 +2,7 @@ import Kolide from 'kolide';
|
|||||||
|
|
||||||
import config from 'redux/nodes/entities/users/config';
|
import config from 'redux/nodes/entities/users/config';
|
||||||
import { formatErrorResponse } from 'redux/nodes/entities/base/helpers';
|
import { formatErrorResponse } from 'redux/nodes/entities/base/helpers';
|
||||||
|
import { logoutUser, updateUserSuccess } from 'redux/nodes/auth/actions';
|
||||||
|
|
||||||
const { extendedActions } = config;
|
const { extendedActions } = config;
|
||||||
|
|
||||||
@ -43,6 +44,29 @@ export const changePassword = (user, { new_password: newPassword, old_password:
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const confirmEmailChange = (user, token) => {
|
||||||
|
const { loadRequest, successAction, updateFailure, updateSuccess } = extendedActions;
|
||||||
|
|
||||||
|
return (dispatch) => {
|
||||||
|
dispatch(loadRequest);
|
||||||
|
|
||||||
|
return Kolide.users.confirmEmailChange(user, token)
|
||||||
|
.then((updatedUser) => {
|
||||||
|
dispatch(successAction(updatedUser, updateSuccess));
|
||||||
|
dispatch(updateUserSuccess(updatedUser));
|
||||||
|
|
||||||
|
return updatedUser;
|
||||||
|
})
|
||||||
|
.catch((response) => {
|
||||||
|
const errorsObject = formatErrorResponse(response);
|
||||||
|
|
||||||
|
dispatch(updateFailure(errorsObject));
|
||||||
|
|
||||||
|
return dispatch(logoutUser());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const enableUser = (user, { enabled }) => {
|
export const enableUser = (user, { enabled }) => {
|
||||||
const { successAction, updateFailure, updateSuccess } = extendedActions;
|
const { successAction, updateFailure, updateSuccess } = extendedActions;
|
||||||
|
|
||||||
@ -96,4 +120,11 @@ export const updateAdmin = (user, { admin }) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default { ...config.actions, changePassword, enableUser, requirePasswordReset, updateAdmin };
|
export default {
|
||||||
|
...config.actions,
|
||||||
|
changePassword,
|
||||||
|
confirmEmailChange,
|
||||||
|
enableUser,
|
||||||
|
requirePasswordReset,
|
||||||
|
updateAdmin,
|
||||||
|
};
|
||||||
|
@ -3,9 +3,11 @@ import expect, { restoreSpies, spyOn } from 'expect';
|
|||||||
import * as Kolide from 'kolide';
|
import * as Kolide from 'kolide';
|
||||||
|
|
||||||
import { reduxMockStore } from 'test/helpers';
|
import { reduxMockStore } from 'test/helpers';
|
||||||
|
import { updateUserSuccess } from 'redux/nodes/auth/actions';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
changePassword,
|
changePassword,
|
||||||
|
confirmEmailChange,
|
||||||
enableUser,
|
enableUser,
|
||||||
requirePasswordReset,
|
requirePasswordReset,
|
||||||
REQUIRE_PASSWORD_RESET_FAILURE,
|
REQUIRE_PASSWORD_RESET_FAILURE,
|
||||||
@ -211,6 +213,117 @@ describe('Users - actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('confirmEmailChange', () => {
|
||||||
|
const token = 'KFBR392';
|
||||||
|
const updatedUser = { ...user, email: 'new@email.com' };
|
||||||
|
|
||||||
|
describe('successful request', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(Kolide.default.users, 'confirmEmailChange').andCall(() => {
|
||||||
|
return Promise.resolve(updatedUser);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(restoreSpies);
|
||||||
|
|
||||||
|
it('calls the API', (done) => {
|
||||||
|
const mockStore = reduxMockStore(store);
|
||||||
|
|
||||||
|
mockStore.dispatch(confirmEmailChange(user, token))
|
||||||
|
.then(() => {
|
||||||
|
expect(Kolide.default.users.confirmEmailChange).toHaveBeenCalledWith(user, token);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispatches the correct actions', (done) => {
|
||||||
|
const mockStore = reduxMockStore(store);
|
||||||
|
|
||||||
|
mockStore.dispatch(confirmEmailChange(user, token))
|
||||||
|
.then(() => {
|
||||||
|
const dispatchedActions = mockStore.getActions();
|
||||||
|
|
||||||
|
expect(dispatchedActions).toEqual([
|
||||||
|
config.extendedActions.loadRequest,
|
||||||
|
config.extendedActions.updateSuccess({
|
||||||
|
users: {
|
||||||
|
[user.id]: updatedUser,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
updateUserSuccess(updatedUser),
|
||||||
|
]);
|
||||||
|
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('unsuccessful request', () => {
|
||||||
|
const errors = [
|
||||||
|
{
|
||||||
|
name: 'base',
|
||||||
|
reason: 'Unable to confirm your email address',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const errorResponse = {
|
||||||
|
status: 422,
|
||||||
|
message: {
|
||||||
|
message: 'Unable to confirm email address',
|
||||||
|
errors,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(Kolide.default.users, 'confirmEmailChange').andCall(() => {
|
||||||
|
return Promise.reject(errorResponse);
|
||||||
|
});
|
||||||
|
spyOn(Kolide.default, 'logout').andCall(() => {
|
||||||
|
return Promise.resolve({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(restoreSpies);
|
||||||
|
|
||||||
|
it('calls the API', (done) => {
|
||||||
|
const mockStore = reduxMockStore(store);
|
||||||
|
|
||||||
|
mockStore.dispatch(confirmEmailChange(user, token))
|
||||||
|
.then(() => {
|
||||||
|
expect(Kolide.default.users.confirmEmailChange).toHaveBeenCalledWith(user, token);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispatches the correct actions', (done) => {
|
||||||
|
const mockStore = reduxMockStore(store);
|
||||||
|
|
||||||
|
mockStore.dispatch(confirmEmailChange(user, token))
|
||||||
|
.then(() => {
|
||||||
|
const dispatchedActions = mockStore.getActions();
|
||||||
|
|
||||||
|
expect(dispatchedActions).toEqual([
|
||||||
|
config.extendedActions.loadRequest,
|
||||||
|
config.extendedActions.updateFailure({ base: 'Unable to confirm your email address', http_status: 422 }),
|
||||||
|
{ type: 'LOGOUT_REQUEST' },
|
||||||
|
{
|
||||||
|
type: '@@router/CALL_HISTORY_METHOD',
|
||||||
|
payload: {
|
||||||
|
method: 'push',
|
||||||
|
args: ['/login'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ type: 'LOGOUT_SUCCESS' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('updateAdmin', () => {
|
describe('updateAdmin', () => {
|
||||||
describe('successful request', () => {
|
describe('successful request', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -13,6 +13,7 @@ import ConfigOptionsPage from 'pages/config/ConfigOptionsPage';
|
|||||||
import ConfirmInvitePage from 'pages/ConfirmInvitePage';
|
import ConfirmInvitePage from 'pages/ConfirmInvitePage';
|
||||||
import CoreLayout from 'layouts/CoreLayout';
|
import CoreLayout from 'layouts/CoreLayout';
|
||||||
import EditPackPage from 'pages/packs/EditPackPage';
|
import EditPackPage from 'pages/packs/EditPackPage';
|
||||||
|
import EmailTokenRedirect from 'components/EmailTokenRedirect';
|
||||||
import LicensePage from 'pages/LicensePage';
|
import LicensePage from 'pages/LicensePage';
|
||||||
import LoginRoutes from 'components/LoginRoutes';
|
import LoginRoutes from 'components/LoginRoutes';
|
||||||
import LogoutPage from 'pages/LogoutPage';
|
import LogoutPage from 'pages/LogoutPage';
|
||||||
@ -42,6 +43,7 @@ const routes = (
|
|||||||
<Route path="reset" />
|
<Route path="reset" />
|
||||||
</Route>
|
</Route>
|
||||||
<Route component={AuthenticatedRoutes}>
|
<Route component={AuthenticatedRoutes}>
|
||||||
|
<Route path="email/change/:token" component={EmailTokenRedirect} />
|
||||||
<Route path="logout" component={LogoutPage} />
|
<Route path="logout" component={LogoutPage} />
|
||||||
<Route component={CoreLayout}>
|
<Route component={CoreLayout}>
|
||||||
<IndexRedirect to="/hosts/manage" />
|
<IndexRedirect to="/hosts/manage" />
|
||||||
|
@ -16,4 +16,5 @@ export default {
|
|||||||
NEW_QUERY: '/queries/new',
|
NEW_QUERY: '/queries/new',
|
||||||
RESET_PASSWORD: '/login/reset',
|
RESET_PASSWORD: '/login/reset',
|
||||||
SETUP: '/setup',
|
SETUP: '/setup',
|
||||||
|
USER_SETTINGS: '/settings',
|
||||||
};
|
};
|
||||||
|
@ -24,6 +24,16 @@ export const validChangePasswordRequest = (bearerToken, params) => {
|
|||||||
.reply(200, {});
|
.reply(200, {});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const validConfirmEmailChangeRequest = (bearerToken, token) => {
|
||||||
|
return nock('http://localhost:8080', {
|
||||||
|
reqHeaders: {
|
||||||
|
Authorization: `Bearer ${bearerToken}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.get(`/api/v1/kolide/email/change/${token}`)
|
||||||
|
.reply(200, { new_email: 'new@email.com' });
|
||||||
|
};
|
||||||
|
|
||||||
export const validCreateLabelRequest = (bearerToken, labelParams) => {
|
export const validCreateLabelRequest = (bearerToken, labelParams) => {
|
||||||
return nock('http://localhost:8080', {
|
return nock('http://localhost:8080', {
|
||||||
reqHeaders: {
|
reqHeaders: {
|
||||||
@ -518,6 +528,7 @@ export default {
|
|||||||
invalidGetQueryRequest,
|
invalidGetQueryRequest,
|
||||||
invalidResetPasswordRequest,
|
invalidResetPasswordRequest,
|
||||||
validChangePasswordRequest,
|
validChangePasswordRequest,
|
||||||
|
validConfirmEmailChangeRequest,
|
||||||
validCreateLabelRequest,
|
validCreateLabelRequest,
|
||||||
validCreateLicenseRequest,
|
validCreateLicenseRequest,
|
||||||
validCreatePackRequest,
|
validCreatePackRequest,
|
||||||
|
Loading…
Reference in New Issue
Block a user