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' } ,
2022-01-12 10:03:24 -08:00
codePlaceholder : { id : 'mfa.mfa_setup.code_placeholder' , defaultMessage : 'Code' } ,
passwordPlaceholder : { id : 'mfa.mfa_setup.password_placeholder' , defaultMessage : 'Password' } ,
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 : '' ,
2022-01-12 09:22:46 -08:00
isLoading : false ,
2020-08-07 13:17:13 -07:00
}
handleInputChange = e => {
this . setState ( { [ e . target . name ] : e . target . value } ) ;
}
2022-01-12 09:22:46 -08:00
handleSubmit = e => {
2020-08-07 13:17:13 -07:00
const { password } = this . state ;
const { dispatch , intl } = this . props ;
2022-01-07 12:26:19 -08:00
2022-01-12 09:22:46 -08:00
this . setState ( { isLoading : true } ) ;
2022-01-07 12:26:19 -08:00
dispatch ( disableMfa ( 'totp' , password ) ) . then ( ( ) => {
dispatch ( snackbar . success ( intl . formatMessage ( messages . mfaDisableSuccess ) ) ) ;
2022-01-12 09:22:46 -08:00
this . context . router . history . push ( '../auth/edit' ) ;
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 ) ) ) ;
2022-01-12 09:22:46 -08:00
this . setState ( { isLoading : false } ) ;
2020-08-07 13:17:13 -07:00
} ) ;
2022-01-07 12:26:19 -08:00
e . preventDefault ( ) ;
2020-08-07 13:17:13 -07:00
}
render ( ) {
const { intl } = this . props ;
2022-01-12 09:22:46 -08:00
const { isLoading , password } = this . state ;
2020-08-07 13:17:13 -07:00
return (
2022-01-12 10:03:24 -08:00
< div className = 'security-settings-panel' >
< SimpleForm onSubmit = { this . handleSubmit } disabled = { isLoading } >
2020-08-07 13:17:13 -07:00
< h1 className = 'security-settings-panel__setup-otp' >
< FormattedMessage id = 'mfa.otp_enabled_title' defaultMessage = 'OTP Enabled' / >
< / h 1 >
2022-01-12 10:03:24 -08:00
< h2 className = 'security-settings-panel__setup-otp' >
< FormattedMessage id = 'mfa.otp_enabled_description' defaultMessage = 'You have enabled two-factor authentication via OTP.' / >
< / h 2 >
2021-12-15 06:26:34 -08:00
< ShowablePassword
2022-01-12 10:03:24 -08:00
label = { intl . formatMessage ( messages . passwordPlaceholder ) }
placeholder = { intl . formatMessage ( messages . passwordPlaceholder ) }
hint = { < FormattedMessage id = 'mfa.mfa_disable_enter_password' defaultMessage = 'Enter your current password to disable two-factor auth.' / > }
2022-01-12 09:22:46 -08:00
disabled = { isLoading }
2020-08-07 13:17:13 -07:00
name = 'password'
onChange = { this . handleInputChange }
2022-01-12 09:22:46 -08:00
value = { password }
2022-01-12 10:03:24 -08:00
required
2022-01-12 09:22:46 -08:00
/ >
2022-01-12 10:03:24 -08:00
< div className = 'security-settings-panel__setup-otp__buttons' >
< Button
disabled = { isLoading }
className = 'button button-primary disable'
text = { intl . formatMessage ( messages . mfa _setup _disable _button ) }
/ >
< / d i v >
< / S i m p l e F o r m >
< / d i v >
2020-08-07 13:17:13 -07:00
) ;
}
}
@ 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' ) ;
2022-01-12 10:03:24 -08:00
e . preventDefault ( ) ;
2020-08-07 13:17:13 -07:00
}
render ( ) {
const { intl } = this . props ;
const { backupCodes , displayOtpForm } = this . state ;
return (
2022-01-12 10:03:24 -08:00
< div className = 'security-settings-panel' >
< SimpleForm >
2020-08-07 13:17:13 -07:00
< 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
) }
2022-01-12 10:03:24 -08:00
< / S i m p l e F o r m >
< / d i v >
2020-08-07 13:17:13 -07:00
) ;
}
}
@ connect ( )
@ injectIntl
class OtpConfirmForm extends ImmutablePureComponent {
static contextTypes = {
router : PropTypes . object ,
} ;
static propTypes = {
intl : PropTypes . object . isRequired ,
} ;
state = {
password : '' ,
2022-01-12 09:22:46 -08:00
isLoading : false ,
2020-08-07 13:17:13 -07:00
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 } ) ;
}
2022-01-12 10:03:24 -08:00
handleCancelClick = e => {
this . context . router . history . push ( '../auth/edit' ) ;
e . preventDefault ( ) ;
}
2022-01-12 09:22:46 -08:00
handleSubmit = e => {
2020-08-07 13:17:13 -07:00
const { dispatch , intl } = this . props ;
2022-01-12 09:22:46 -08:00
const { code , password } = this . state ;
this . setState ( { isLoading : true } ) ;
2022-01-07 12:26:19 -08:00
dispatch ( confirmMfa ( 'totp' , code , password ) ) . then ( ( ) => {
dispatch ( snackbar . success ( intl . formatMessage ( messages . mfaConfirmSuccess ) ) ) ;
2022-01-12 09:22:46 -08:00
this . context . router . history . push ( '../auth/edit' ) ;
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 ) ) ) ;
2022-01-12 09:22:46 -08:00
this . setState ( { isLoading : false } ) ;
2020-08-07 13:17:13 -07:00
} ) ;
2022-01-07 12:26:19 -08:00
e . preventDefault ( ) ;
2020-08-07 13:17:13 -07:00
}
render ( ) {
const { intl } = this . props ;
2022-01-12 09:22:46 -08:00
const { isLoading , qrCodeURI , confirm _key , password , code } = this . state ;
2020-08-07 13:17:13 -07:00
return (
2022-01-12 10:03:24 -08:00
< div className = 'security-settings-panel' >
< SimpleForm onSubmit = { this . handleSubmit } disabled = { isLoading } >
2020-08-07 13:17:13 -07:00
< fieldset disabled = { false } >
< FieldsGroup >
< div className = 'security-settings-panel__section-container' >
< h2 > < FormattedMessage id = 'mfa.mfa_setup_scan_title' defaultMessage = 'Scan' / > < / h 2 >
2022-01-12 10:03:24 -08:00
< div > < FormattedMessage id = 'mfa.mfa_setup_scan_description' defaultMessage = 'Using your two-factor app, scan this QR code or enter the text key.' / > < / d i v >
2020-08-07 13:17:13 -07:00
2022-01-12 10:03:24 -08:00
< div className = 'security-settings-panel__qr-code' >
2020-08-07 13:17:13 -07:00
< QRCode value = { qrCodeURI } / >
2022-01-12 10:03:24 -08:00
< div className = 'security-settings-panel__confirm-key' >
{ confirm _key }
< / d i v >
< / d i v >
2020-08-07 13:17:13 -07:00
< / d i v >
< div className = 'security-settings-panel__section-container' >
< h2 > < FormattedMessage id = 'mfa.mfa_setup_verify_title' defaultMessage = 'Verify' / > < / h 2 >
< TextInput
name = 'code'
2022-01-12 10:03:24 -08:00
label = { intl . formatMessage ( messages . codePlaceholder ) }
hint = { < FormattedMessage id = 'mfa.mfa_setup.code_hint' defaultMessage = 'Enter the code from your two-factor app.' / > }
placeholder = { intl . formatMessage ( messages . codePlaceholder ) }
2020-08-07 13:17:13 -07:00
onChange = { this . handleInputChange }
autoComplete = 'off'
2022-01-12 09:22:46 -08:00
value = { code }
disabled = { isLoading }
2022-01-12 10:03:24 -08:00
required
2020-08-07 13:17:13 -07:00
/ >
2021-12-15 06:26:34 -08:00
< ShowablePassword
2020-08-07 13:17:13 -07:00
name = 'password'
2022-01-12 10:03:24 -08:00
label = { intl . formatMessage ( messages . passwordPlaceholder ) }
hint = { < FormattedMessage id = 'mfa.mfa_setup.password_hint' defaultMessage = 'Enter your current password to confirm your identity.' / > }
placeholder = { intl . formatMessage ( messages . passwordPlaceholder ) }
2020-08-07 13:17:13 -07:00
onChange = { this . handleInputChange }
2022-01-12 09:22:46 -08:00
value = { password }
disabled = { isLoading }
2022-01-12 10:03:24 -08:00
required
2020-08-07 13:17:13 -07:00
/ >
< / 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' >
2022-01-12 09:22:46 -08:00
< Button
type = 'button'
className = 'button button-secondary cancel'
text = { intl . formatMessage ( messages . mfa _cancel _button ) }
onClick = { this . handleCancelClick }
disabled = { isLoading }
/ >
< Button
type = 'submit'
className = 'button button-primary setup'
text = { intl . formatMessage ( messages . mfa _setup _confirm _button ) }
disabled = { isLoading }
/ >
2020-08-07 13:17:13 -07:00
< / d i v >
2022-01-12 10:03:24 -08:00
< / S i m p l e F o r m >
< / d i v >
2020-08-07 13:17:13 -07:00
) ;
}
}