2020-08-07 13:17:13 -07:00
import PropTypes from 'prop-types' ;
import QRCode from 'qrcode.react' ;
2022-01-10 14:17:52 -08:00
import React from 'react' ;
import ImmutablePropTypes from 'react-immutable-proptypes' ;
import ImmutablePureComponent from 'react-immutable-pure-component' ;
import { defineMessages , injectIntl , FormattedMessage } from 'react-intl' ;
import { connect } from 'react-redux' ;
2022-01-10 14:25:06 -08:00
2020-09-29 17:10:57 -07:00
import snackbar from 'soapbox/actions/snackbar' ;
2022-01-10 14:17:52 -08:00
import Button from 'soapbox/components/button' ;
import LoadingIndicator from 'soapbox/components/loading_indicator' ;
2021-12-15 06:26:34 -08:00
import ShowablePassword from 'soapbox/components/showable_password' ;
2020-08-07 13:17:13 -07:00
import {
SimpleForm ,
FieldsGroup ,
TextInput ,
} from 'soapbox/features/forms' ;
2022-01-10 14:25:06 -08:00
2020-08-07 13:17:13 -07:00
import {
2022-01-07 12:26:19 -08:00
fetchMfa ,
2020-08-07 13:17:13 -07:00
fetchBackupCodes ,
2022-01-07 12:26:19 -08:00
setupMfa ,
confirmMfa ,
disableMfa ,
2020-08-07 13:17:13 -07:00
} from '../../actions/mfa' ;
2022-01-10 14:17:52 -08:00
import Column from '../ui/components/column' ;
import ColumnSubheading from '../ui/components/column_subheading' ;
2020-08-07 13:17:13 -07:00
/ *
Security settings page for user account
Routed to / auth / mfa
Includes following features :
- Set up Multi - factor Auth
* /
const messages = defineMessages ( {
heading : { id : 'column.security' , defaultMessage : 'Security' } ,
subheading : { id : 'column.mfa' , defaultMessage : 'Multi-Factor Authentication' } ,
mfa _cancel _button : { id : 'column.mfa_cancel' , defaultMessage : 'Cancel' } ,
mfa _setup _button : { id : 'column.mfa_setup' , defaultMessage : 'Proceed to Setup' } ,
mfa _setup _confirm _button : { id : 'column.mfa_confirm_button' , defaultMessage : 'Confirm' } ,
mfa _setup _disable _button : { id : 'column.mfa_disable_button' , defaultMessage : 'Disable' } ,
passwordFieldLabel : { id : 'security.fields.password.label' , defaultMessage : 'Password' } ,
confirmFail : { id : 'security.confirm.fail' , defaultMessage : 'Incorrect code or password. Try again.' } ,
qrFail : { id : 'security.qr.fail' , defaultMessage : 'Failed to fetch setup key' } ,
codesFail : { id : 'security.codes.fail' , defaultMessage : 'Failed to fetch backup codes' } ,
disableFail : { id : 'security.disable.fail' , defaultMessage : 'Incorrect password. Try again.' } ,
2022-01-07 12:26:19 -08:00
mfaDisableSuccess : { id : 'mfa.disable.success_message' , defaultMessage : 'MFA disabled' } ,
mfaConfirmSuccess : { id : 'mfa.confirm.success_message' , defaultMessage : 'MFA confirmed' } ,
2020-08-07 13:17:13 -07:00
} ) ;
const mapStateToProps = state => ( {
backup _codes : state . getIn ( [ 'auth' , 'backup_codes' , 'codes' ] ) ,
2022-01-07 12:26:19 -08:00
mfa : state . getIn ( [ 'security' , 'mfa' ] ) ,
2020-08-07 13:17:13 -07:00
} ) ;
export default @ connect ( mapStateToProps )
@ injectIntl
class MfaForm extends ImmutablePureComponent {
static contextTypes = {
router : PropTypes . object ,
} ;
static propTypes = {
intl : PropTypes . object . isRequired ,
dispatch : PropTypes . func . isRequired ,
2022-01-07 12:26:19 -08:00
mfa : ImmutablePropTypes . map . isRequired ,
2020-08-07 13:17:13 -07:00
} ;
state = {
displayOtpForm : false ,
}
handleSetupProceedClick = e => {
this . setState ( { displayOtpForm : true } ) ;
2022-01-07 12:26:19 -08:00
e . preventDefault ( ) ;
}
componentDidMount ( ) {
this . props . dispatch ( fetchMfa ( ) ) ;
2020-08-07 13:17:13 -07:00
}
render ( ) {
2022-01-07 12:26:19 -08:00
const { intl , mfa } = this . props ;
2020-08-07 13:17:13 -07:00
const { displayOtpForm } = this . state ;
return (
2021-10-14 11:38:16 -07:00
< Column icon = 'lock' heading = { intl . formatMessage ( messages . heading ) } >
2020-08-07 13:17:13 -07:00
< ColumnSubheading text = { intl . formatMessage ( messages . subheading ) } / >
2022-01-07 12:26:19 -08:00
{ mfa . getIn ( [ 'settings' , 'totp' ] ) ? (
< DisableOtpForm / >
) : (
< >
< EnableOtpForm handleSetupProceedClick = { this . handleSetupProceedClick } / >
{ displayOtpForm && < OtpConfirmForm / > }
< / >
) }
2020-08-07 13:17:13 -07:00
< / C o l u m n >
) ;
}
}
@ connect ( )
@ injectIntl
class DisableOtpForm extends ImmutablePureComponent {
static contextTypes = {
router : PropTypes . object ,
} ;
static propTypes = {
intl : PropTypes . object . isRequired ,
dispatch : PropTypes . func . isRequired ,
} ;
state = {
password : '' ,
}
handleInputChange = e => {
this . setState ( { [ e . target . name ] : e . target . value } ) ;
}
handleOtpDisableClick = e => {
const { password } = this . state ;
const { dispatch , intl } = this . props ;
2022-01-07 12:26:19 -08:00
dispatch ( disableMfa ( 'totp' , password ) ) . then ( ( ) => {
dispatch ( snackbar . success ( intl . formatMessage ( messages . mfaDisableSuccess ) ) ) ;
2020-08-07 13:17:13 -07:00
} ) . catch ( error => {
2020-09-29 17:10:57 -07:00
dispatch ( snackbar . error ( intl . formatMessage ( messages . disableFail ) ) ) ;
2020-08-07 13:17:13 -07:00
} ) ;
2022-01-07 12:26:19 -08:00
this . context . router . history . push ( '../auth/edit' ) ;
e . preventDefault ( ) ;
2020-08-07 13:17:13 -07:00
}
render ( ) {
const { intl } = this . props ;
return (
< SimpleForm >
< div className = 'security-settings-panel' >
< h1 className = 'security-settings-panel__setup-otp' >
< FormattedMessage id = 'mfa.otp_enabled_title' defaultMessage = 'OTP Enabled' / >
< / h 1 >
< div > < FormattedMessage id = 'mfa.otp_enabled_description' defaultMessage = 'You have enabled two-factor authentication via OTP.' / > < / d i v >
< div > < FormattedMessage id = 'mfa.mfa_disable_enter_password' defaultMessage = 'Enter your current password to disable two-factor auth:' / > < / d i v >
2021-12-15 06:26:34 -08:00
< ShowablePassword
2020-08-07 13:17:13 -07:00
name = 'password'
onChange = { this . handleInputChange }
/ >
< Button className = 'button button-primary disable' text = { intl . formatMessage ( messages . mfa _setup _disable _button ) } onClick = { this . handleOtpDisableClick } / >
< / d i v >
< / S i m p l e F o r m >
) ;
}
}
@ connect ( )
@ injectIntl
class EnableOtpForm extends ImmutablePureComponent {
static contextTypes = {
router : PropTypes . object ,
} ;
static propTypes = {
intl : PropTypes . object . isRequired ,
dispatch : PropTypes . func . isRequired ,
} ;
state = {
backupCodes : [ ] ,
}
componentDidMount ( ) {
const { dispatch , intl } = this . props ;
2022-01-07 12:26:19 -08:00
dispatch ( fetchBackupCodes ( ) ) . then ( ( { codes : backupCodes } ) => {
this . setState ( { backupCodes } ) ;
2020-08-07 13:17:13 -07:00
} ) . catch ( error => {
2020-09-29 17:10:57 -07:00
dispatch ( snackbar . error ( intl . formatMessage ( messages . codesFail ) ) ) ;
2020-08-07 13:17:13 -07:00
} ) ;
}
handleCancelClick = e => {
this . context . router . history . push ( '../auth/edit' ) ;
}
render ( ) {
const { intl } = this . props ;
const { backupCodes , displayOtpForm } = this . state ;
return (
< SimpleForm >
< div className = 'security-settings-panel' >
< h1 className = 'security-settings-panel__setup-otp' >
< FormattedMessage id = 'mfa.setup_otp_title' defaultMessage = 'OTP Disabled' / >
< / h 1 >
< h2 className = 'security-settings-panel__setup-otp' >
< FormattedMessage id = 'mfa.setup_hint' defaultMessage = 'Follow these steps to set up multi-factor authentication on your account with OTP' / >
< / h 2 >
< div className = 'security-warning' >
< FormattedMessage id = 'mfa.setup_warning' defaultMessage = "Write these codes down or save them somewhere secure - otherwise you won't see them again. If you lose access to your 2FA app and recovery codes you'll be locked out of your account." / >
< / d i v >
< h2 className = 'security-settings-panel__setup-otp' >
< FormattedMessage id = 'mfa.setup_recoverycodes' defaultMessage = 'Recovery codes' / >
< / h 2 >
< div className = 'backup_codes' >
2022-01-07 12:26:19 -08:00
{ backupCodes . length > 0 ? (
2020-08-07 13:17:13 -07:00
< div >
{ backupCodes . map ( ( code , i ) => (
< div key = { i } className = 'backup_code' >
< div className = 'backup_code' > { code } < / d i v >
< / d i v >
) ) }
2022-01-07 12:26:19 -08:00
< / d i v >
) : (
2020-08-07 13:17:13 -07:00
< LoadingIndicator / >
2022-01-07 12:26:19 -08:00
) }
2020-08-07 13:17:13 -07:00
< / d i v >
2022-01-07 12:26:19 -08:00
{ ! displayOtpForm && (
2020-08-07 13:17:13 -07:00
< div className = 'security-settings-panel__setup-otp__buttons' >
< Button className = 'button button-secondary cancel' text = { intl . formatMessage ( messages . mfa _cancel _button ) } onClick = { this . handleCancelClick } / >
2022-01-07 12:26:19 -08:00
{ backupCodes . length > 0 && (
< Button className = 'button button-primary setup' text = { intl . formatMessage ( messages . mfa _setup _button ) } onClick = { this . props . handleSetupProceedClick } / >
) }
2020-08-07 13:17:13 -07:00
< / d i v >
2022-01-07 12:26:19 -08:00
) }
2020-08-07 13:17:13 -07:00
< / d i v >
< / S i m p l e F o r m >
) ;
}
}
@ connect ( )
@ injectIntl
class OtpConfirmForm extends ImmutablePureComponent {
static contextTypes = {
router : PropTypes . object ,
} ;
static propTypes = {
intl : PropTypes . object . isRequired ,
} ;
state = {
password : '' ,
done : false ,
code : '' ,
qrCodeURI : '' ,
confirm _key : '' ,
}
componentDidMount ( ) {
const { dispatch , intl } = this . props ;
2022-01-07 12:26:19 -08:00
dispatch ( setupMfa ( 'totp' ) ) . then ( data => {
this . setState ( { qrCodeURI : data . provisioning _uri , confirm _key : data . key } ) ;
2020-08-07 13:17:13 -07:00
} ) . catch ( error => {
2020-09-29 17:10:57 -07:00
dispatch ( snackbar . error ( intl . formatMessage ( messages . qrFail ) ) ) ;
2020-08-07 13:17:13 -07:00
} ) ;
}
handleInputChange = e => {
this . setState ( { [ e . target . name ] : e . target . value } ) ;
}
handleOtpConfirmClick = e => {
const { code , password } = this . state ;
const { dispatch , intl } = this . props ;
2022-01-07 12:26:19 -08:00
dispatch ( confirmMfa ( 'totp' , code , password ) ) . then ( ( ) => {
dispatch ( snackbar . success ( intl . formatMessage ( messages . mfaConfirmSuccess ) ) ) ;
2020-08-07 13:17:13 -07:00
} ) . catch ( error => {
2020-09-29 17:10:57 -07:00
dispatch ( snackbar . error ( intl . formatMessage ( messages . confirmFail ) ) ) ;
2020-08-07 13:17:13 -07:00
} ) ;
2022-01-07 12:26:19 -08:00
this . context . router . history . push ( '../auth/edit' ) ;
e . preventDefault ( ) ;
2020-08-07 13:17:13 -07:00
}
render ( ) {
const { intl } = this . props ;
const { qrCodeURI , confirm _key } = this . state ;
return (
< SimpleForm >
< div className = 'security-settings-panel' >
< fieldset disabled = { false } >
< FieldsGroup >
< div className = 'security-settings-panel__section-container' >
< h2 > < FormattedMessage id = 'mfa.mfa_setup_scan_title' defaultMessage = 'Scan' / > < / h 2 >
< div > < FormattedMessage id = 'mfa.mfa_setup_scan_description' defaultMessage = 'Using your two-factor app, scan this QR code or enter text key:' / > < / d i v >
< span className = 'security-settings-panel qr-code' >
< QRCode value = { qrCodeURI } / >
< / s p a n >
< div className = 'security-settings-panel confirm-key' > < FormattedMessage id = 'mfa.mfa_setup_scan_key' defaultMessage = 'Key:' / > { confirm _key } < / d i v >
< / d i v >
< div className = 'security-settings-panel__section-container' >
< h2 > < FormattedMessage id = 'mfa.mfa_setup_verify_title' defaultMessage = 'Verify' / > < / h 2 >
< div > < FormattedMessage id = 'mfa.mfa_setup_verify_description' defaultMessage = 'To enable two-factor authentication, enter the code from your two-factor app:' / > < / d i v >
< TextInput
name = 'code'
onChange = { this . handleInputChange }
autoComplete = 'off'
/ >
< div > < FormattedMessage id = 'mfa.mfa_setup_enter_password' defaultMessage = 'Enter your current password to confirm your identity:' / > < / d i v >
2021-12-15 06:26:34 -08:00
< ShowablePassword
2020-08-07 13:17:13 -07:00
name = 'password'
onChange = { this . handleInputChange }
/ >
< / d i v >
< / F i e l d s G r o u p >
< / f i e l d s e t >
< div className = 'security-settings-panel__setup-otp__buttons' >
< Button className = 'button button-secondary cancel' text = { intl . formatMessage ( messages . mfa _cancel _button ) } onClick = { this . handleCancelClick } / >
< Button className = 'button button-primary setup' text = { intl . formatMessage ( messages . mfa _setup _confirm _button ) } onClick = { this . handleOtpConfirmClick } / >
< / d i v >
< / d i v >
< / S i m p l e F o r m >
) ;
}
}