import PropTypes from 'prop-types'; import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { defineMessages, injectIntl, FormattedDate } from 'react-intl'; import { connect } from 'react-redux'; import { changeEmail, changePassword, deleteAccount, } from 'soapbox/actions/security'; import { fetchOAuthTokens, revokeOAuthTokenById } from 'soapbox/actions/security'; import { getSettings } from 'soapbox/actions/settings'; import snackbar from 'soapbox/actions/snackbar'; import Button from 'soapbox/components/button'; import ShowablePassword from 'soapbox/components/showable_password'; import { SimpleForm, FieldsGroup, TextInput, } from 'soapbox/features/forms'; import { fetchMfa } from '../../actions/mfa'; import Column from '../ui/components/column'; /* Security settings page for user account Routed to /auth/edit Includes following features: - Change Email - Change Password - Sessions - Deactivate Account */ const messages = defineMessages({ heading: { id: 'column.security', defaultMessage: 'Security' }, submit: { id: 'security.submit', defaultMessage: 'Save changes' }, updateEmailSuccess: { id: 'security.update_email.success', defaultMessage: 'Email successfully updated.' }, updateEmailFail: { id: 'security.update_email.fail', defaultMessage: 'Update email failed.' }, updatePasswordSuccess: { id: 'security.update_password.success', defaultMessage: 'Password successfully updated.' }, updatePasswordFail: { id: 'security.update_password.fail', defaultMessage: 'Update password failed.' }, emailFieldLabel: { id: 'security.fields.email.label', defaultMessage: 'Email address' }, passwordFieldLabel: { id: 'security.fields.password.label', defaultMessage: 'Password' }, oldPasswordFieldLabel: { id: 'security.fields.old_password.label', defaultMessage: 'Current password' }, newPasswordFieldLabel: { id: 'security.fields.new_password.label', defaultMessage: 'New password' }, confirmationFieldLabel: { id: 'security.fields.password_confirmation.label', defaultMessage: 'New password (again)' }, revoke: { id: 'security.tokens.revoke', defaultMessage: 'Revoke' }, emailHeader: { id: 'security.headers.update_email', defaultMessage: 'Change Email' }, passwordHeader: { id: 'security.headers.update_password', defaultMessage: 'Change Password' }, tokenHeader: { id: 'security.headers.tokens', defaultMessage: 'Sessions' }, deleteHeader: { id: 'security.headers.delete', defaultMessage: 'Delete Account' }, deleteText: { id: 'security.text.delete', defaultMessage: 'To delete your account, enter your password then click Delete Account. This is a permanent action that cannot be undone. Your account will be destroyed from this server, and a deletion request will be sent to other servers. It\'s not guaranteed that all servers will purge your account.' }, deleteSubmit: { id: 'security.submit.delete', defaultMessage: 'Delete Account' }, deleteAccountSuccess: { id: 'security.delete_account.success', defaultMessage: 'Account successfully deleted.' }, deleteAccountFail: { id: 'security.delete_account.fail', defaultMessage: 'Account deletion failed.' }, mfa: { id: 'security.mfa', defaultMessage: 'Set up 2-Factor Auth' }, mfa_setup_hint: { id: 'security.mfa_setup_hint', defaultMessage: 'Configure multi-factor authentication with OTP' }, mfa_enabled: { id: 'security.mfa_enabled', defaultMessage: 'You have multi-factor authentication set up with OTP.' }, disable_mfa: { id: 'security.disable_mfa', defaultMessage: 'Disable' }, mfaHeader: { id: 'security.mfa_header', defaultMessage: 'Authorization Methods' }, }); const mapStateToProps = state => ({ settings: getSettings(state), tokens: state.getIn(['security', 'tokens']), mfa: state.getIn(['security', 'mfa']), }); export default @connect(mapStateToProps) @injectIntl class SecurityForm extends ImmutablePureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; render() { const { intl } = this.props; return ( ); } } @connect() @injectIntl class ChangeEmailForm extends ImmutablePureComponent { static propTypes = { email: PropTypes.string, dispatch: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; state = { email: '', password: '', isLoading: false, } handleInputChange = e => { this.setState({ [e.target.name]: e.target.value }); } handleSubmit = e => { const { email, password } = this.state; const { dispatch, intl } = this.props; this.setState({ isLoading: true }); return dispatch(changeEmail(email, password)).then(() => { this.setState({ email: '', password: '' }); // TODO: Maybe redirect user dispatch(snackbar.success(intl.formatMessage(messages.updateEmailSuccess))); }).catch(error => { this.setState({ password: '' }); dispatch(snackbar.error(intl.formatMessage(messages.updateEmailFail))); }).then(() => { this.setState({ isLoading: false }); }); } render() { const { intl } = this.props; return ( {intl.formatMessage(messages.emailHeader)} {intl.formatMessage(messages.submit)} ); } } @connect() @injectIntl class ChangePasswordForm extends ImmutablePureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; state = { oldPassword: '', newPassword: '', confirmation: '', isLoading: false, } handleInputChange = e => { this.setState({ [e.target.name]: e.target.value }); } clearForm = () => { this.setState({ oldPassword: '', newPassword: '', confirmation: '' }); } handleSubmit = e => { const { oldPassword, newPassword, confirmation } = this.state; const { dispatch, intl } = this.props; this.setState({ isLoading: true }); return dispatch(changePassword(oldPassword, newPassword, confirmation)).then(() => { this.clearForm(); // TODO: Maybe redirect user dispatch(snackbar.success(intl.formatMessage(messages.updatePasswordSuccess))); }).catch(error => { this.clearForm(); dispatch(snackbar.error(intl.formatMessage(messages.updatePasswordFail))); }).then(() => { this.setState({ isLoading: false }); }); } render() { const { intl } = this.props; return ( {intl.formatMessage(messages.passwordHeader)} {intl.formatMessage(messages.submit)} ); } } @connect(mapStateToProps) @injectIntl class SetUpMfa extends ImmutablePureComponent { static contextTypes = { router: PropTypes.object, }; static propTypes = { intl: PropTypes.object.isRequired, mfa: ImmutablePropTypes.map.isRequired, }; handleMfaClick = e => { this.context.router.history.push('../auth/mfa'); } componentDidMount() { this.props.dispatch(fetchMfa()); } render() { const { intl, mfa } = this.props; return ( {intl.formatMessage(messages.mfaHeader)} {!mfa.getIn(['settings', 'totp']) ? {intl.formatMessage(messages.mfa_setup_hint)} : {intl.formatMessage(messages.mfa_enabled)} } ); } } @connect(mapStateToProps) @injectIntl class AuthTokenList extends ImmutablePureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, tokens: ImmutablePropTypes.list, }; handleRevoke = id => { return e => { this.props.dispatch(revokeOAuthTokenById(id)); }; } componentDidMount() { this.props.dispatch(fetchOAuthTokens()); } render() { const { tokens, intl } = this.props; if (tokens.isEmpty()) return null; return ( {intl.formatMessage(messages.tokenHeader)} {tokens.reverse().map((token, i) => ( {token.get('app_name')} {this.props.intl.formatMessage(messages.revoke)} ))} ); } } @connect(mapStateToProps) @injectIntl class DeactivateAccount extends ImmutablePureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; state = { password: '', isLoading: false, } handleInputChange = e => { this.setState({ [e.target.name]: e.target.value }); } handleSubmit = e => { const { password } = this.state; const { dispatch, intl } = this.props; this.setState({ isLoading: true }); return dispatch(deleteAccount(intl, password)).then(() => { //this.setState({ email: '', password: '' }); // TODO: Maybe redirect user dispatch(snackbar.success(intl.formatMessage(messages.deleteAccountSuccess))); }).catch(error => { this.setState({ password: '' }); dispatch(snackbar.error(intl.formatMessage(messages.deleteAccountFail))); }).then(() => { this.setState({ isLoading: false }); }); } render() { const { intl } = this.props; return ( {intl.formatMessage(messages.deleteHeader)} {intl.formatMessage(messages.deleteText)} {intl.formatMessage(messages.deleteSubmit)} ); } }
{intl.formatMessage(messages.mfa_setup_hint)}
{intl.formatMessage(messages.mfa_enabled)}
{intl.formatMessage(messages.deleteText)}