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-03-17 18:17:28 -07:00
import { withRouter } from 'react-router-dom' ;
2022-01-10 14:25:06 -08:00
2020-09-29 17:10:57 -07:00
import snackbar from 'soapbox/actions/snackbar' ;
2022-03-21 11:09:01 -07:00
import { Spinner } from 'soapbox/components/ui' ;
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-03-21 11:09:01 -07:00
import { Button , Card , CardBody , CardHeader , CardTitle , Column , Form , FormActions , FormGroup , Input , Stack , Text } from '../../components/ui' ;
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 ( {
2022-03-21 11:09:01 -07:00
heading : { id : 'column.mfa' , defaultMessage : 'Multi-Factor Authentication' } ,
2020-08-07 13:17:13 -07:00
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
2022-03-17 18:17:28 -07:00
@ withRouter
2020-08-07 13:17:13 -07:00
class MfaForm extends ImmutablePureComponent {
static propTypes = {
intl : PropTypes . object . isRequired ,
dispatch : PropTypes . func . isRequired ,
2022-01-07 12:26:19 -08:00
mfa : ImmutablePropTypes . map . isRequired ,
2022-03-17 18:17:28 -07:00
history : PropTypes . object ,
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 (
2022-03-21 11:09:01 -07:00
< Column label = { intl . formatMessage ( messages . heading ) } transparent withHeader = { false } >
< Card variant = 'rounded' >
< CardHeader backHref = '/settings' >
< CardTitle title = { intl . formatMessage ( messages . heading ) } / >
< / C a r d H e a d e r >
< CardBody >
{ mfa . getIn ( [ 'settings' , 'totp' ] ) ? (
< DisableOtpForm / >
) : (
< >
< EnableOtpForm displayOtpForm = { displayOtpForm } handleSetupProceedClick = { this . handleSetupProceedClick } / >
{ displayOtpForm && < OtpConfirmForm / > }
< / >
) }
< / C a r d B o d y >
< / C a r d >
2020-08-07 13:17:13 -07:00
< / C o l u m n >
) ;
}
}
@ connect ( )
@ injectIntl
2022-03-17 18:17:28 -07:00
@ withRouter
2020-08-07 13:17:13 -07:00
class DisableOtpForm extends ImmutablePureComponent {
static propTypes = {
intl : PropTypes . object . isRequired ,
dispatch : PropTypes . func . isRequired ,
2022-03-17 18:17:28 -07:00
history : PropTypes . object ,
2020-08-07 13:17:13 -07:00
} ;
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-03-17 18:17:28 -07:00
this . props . 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-03-21 11:09:01 -07:00
< Form onSubmit = { this . handleSubmit } disabled = { isLoading } >
< Stack >
< Text weight = 'medium' >
2020-08-07 13:17:13 -07:00
< FormattedMessage id = 'mfa.otp_enabled_title' defaultMessage = 'OTP Enabled' / >
2022-03-21 11:09:01 -07:00
< / T e x t >
< Text theme = 'muted' >
2022-01-12 10:03:24 -08:00
< FormattedMessage id = 'mfa.otp_enabled_description' defaultMessage = 'You have enabled two-factor authentication via OTP.' / >
2022-03-21 11:09:01 -07:00
< / T e x t >
< / S t a c k >
< FormGroup
labelText = { intl . formatMessage ( messages . passwordPlaceholder ) }
hintText = { < FormattedMessage id = 'mfa.mfa_disable_enter_password' defaultMessage = 'Enter your current password to disable two-factor auth.' / > }
>
< Input
type = 'password'
2022-01-12 10:03:24 -08:00
placeholder = { intl . formatMessage ( messages . passwordPlaceholder ) }
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-03-21 11:09:01 -07:00
< / F o r m G r o u p >
< FormActions >
< Button
disabled = { isLoading }
theme = 'danger'
text = { intl . formatMessage ( messages . mfa _setup _disable _button ) }
/ >
< / F o r m A c t i o n s >
< / F o r m >
2020-08-07 13:17:13 -07:00
) ;
}
}
@ connect ( )
@ injectIntl
2022-03-17 18:17:28 -07:00
@ withRouter
2020-08-07 13:17:13 -07:00
class EnableOtpForm extends ImmutablePureComponent {
static propTypes = {
intl : PropTypes . object . isRequired ,
dispatch : PropTypes . func . isRequired ,
2022-03-17 18:17:28 -07:00
history : PropTypes . object ,
2020-08-07 13:17:13 -07:00
} ;
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 => {
2022-03-17 18:17:28 -07:00
this . props . history . push ( '../auth/edit' ) ;
2022-01-12 10:03:24 -08:00
e . preventDefault ( ) ;
2020-08-07 13:17:13 -07:00
}
render ( ) {
2022-03-21 11:09:01 -07:00
const { intl , displayOtpForm } = this . props ;
const { backupCodes } = this . state ;
2020-08-07 13:17:13 -07:00
return (
2022-03-21 11:09:01 -07:00
< Stack space = { 4 } >
{ /* Removing for now -- seems redundant. */ }
{ / * < p c l a s s N a m e = ' t e x t - m u t e d m b - 1 0 ' >
< FormattedMessage id = 'mfa.setup_hint' defaultMessage = 'Follow these steps to set up multi-factor authentication on your account with OTP.' / >
< /p> */ }
< Stack space = { 2 } >
< Text theme = 'muted' >
2020-08-07 13:17:13 -07:00
< 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." / >
2022-03-21 11:09:01 -07:00
< / T e x t >
2022-03-30 09:43:39 -07:00
< div className = 'bg-gray-100 dark:bg-slate-700 rounded-lg p-4' >
2022-03-21 11:09:01 -07:00
< Stack space = { 3 } >
< Text weight = 'medium' align = 'center' >
< FormattedMessage id = 'mfa.setup_recoverycodes' defaultMessage = 'Recovery codes' / >
< / T e x t >
{ backupCodes . length > 0 ? (
< div className = 'grid gap-3 grid-cols-2 rounded-lg text-center' >
{ backupCodes . map ( ( code , i ) => (
< Text key = { i } theme = 'muted' size = 'sm' >
{ code }
< / T e x t >
) ) }
< / d i v >
) : (
< Spinner / >
) }
< / S t a c k >
2020-08-07 13:17:13 -07:00
< / d i v >
2022-03-21 11:09:01 -07:00
< / S t a c k >
{ ! displayOtpForm && (
< FormActions >
< Button
theme = 'ghost'
text = { intl . formatMessage ( messages . mfa _cancel _button ) }
onClick = { this . handleCancelClick }
/ >
{ backupCodes . length > 0 && (
< Button
theme = 'primary'
text = { intl . formatMessage ( messages . mfa _setup _button ) }
onClick = { this . props . handleSetupProceedClick }
/ >
2022-01-07 12:26:19 -08:00
) }
2022-03-21 11:09:01 -07:00
< / F o r m A c t i o n s >
) }
< / S t a c k >
2020-08-07 13:17:13 -07:00
) ;
}
}
@ connect ( )
@ injectIntl
2022-03-17 18:17:28 -07:00
@ withRouter
2020-08-07 13:17:13 -07:00
class OtpConfirmForm extends ImmutablePureComponent {
static propTypes = {
intl : PropTypes . object . isRequired ,
2022-03-17 18:17:28 -07:00
history : PropTypes . object ,
2020-08-07 13:17:13 -07:00
} ;
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 => {
2022-03-17 18:17:28 -07:00
this . props . history . push ( '../auth/edit' ) ;
2022-01-12 10:03:24 -08:00
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-03-17 18:17:28 -07:00
this . props . 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-03-21 11:09:01 -07:00
< Stack space = { 4 } >
< hr className = 'mt-4' / >
< Form onSubmit = { this . handleSubmit } disabled = { isLoading } >
< Stack >
< Text weight = 'semibold' size = 'lg' >
1. < FormattedMessage id = 'mfa.mfa_setup_scan_title' defaultMessage = 'Scan' / >
< / T e x t >
< Text theme = 'muted' >
< FormattedMessage id = 'mfa.mfa_setup_scan_description' defaultMessage = 'Using your two-factor app, scan this QR code or enter the text key.' / >
< / T e x t >
< / S t a c k >
< QRCode value = { qrCodeURI } / >
{ confirm _key }
< Text weight = 'semibold' size = 'lg' >
2. < FormattedMessage id = 'mfa.mfa_setup_verify_title' defaultMessage = 'Verify' / >
< / T e x t >
< FormGroup
labelText = { intl . formatMessage ( messages . codePlaceholder ) }
hintText = { < FormattedMessage id = 'mfa.mfa_setup.code_hint' defaultMessage = 'Enter the code from your two-factor app.' / > }
>
< Input
name = 'code'
placeholder = { intl . formatMessage ( messages . codePlaceholder ) }
onChange = { this . handleInputChange }
autoComplete = 'off'
value = { code }
disabled = { isLoading }
required
/ >
< / F o r m G r o u p >
< FormGroup
labelText = { intl . formatMessage ( messages . passwordPlaceholder ) }
hintText = { < FormattedMessage id = 'mfa.mfa_setup.password_hint' defaultMessage = 'Enter your current password to confirm your identity.' / > }
>
< Input
type = 'password'
name = 'password'
placeholder = { intl . formatMessage ( messages . passwordPlaceholder ) }
onChange = { this . handleInputChange }
value = { password }
disabled = { isLoading }
required
/ >
< / F o r m G r o u p >
2020-08-07 13:17:13 -07:00
2022-03-21 11:09:01 -07:00
< FormActions >
2022-01-12 09:22:46 -08:00
< Button
type = 'button'
2022-03-21 11:09:01 -07:00
theme = 'ghost'
2022-01-12 09:22:46 -08:00
text = { intl . formatMessage ( messages . mfa _cancel _button ) }
onClick = { this . handleCancelClick }
disabled = { isLoading }
/ >
2022-03-21 11:09:01 -07:00
2022-01-12 09:22:46 -08:00
< Button
type = 'submit'
2022-03-21 11:09:01 -07:00
theme = 'primary'
2022-01-12 09:22:46 -08:00
text = { intl . formatMessage ( messages . mfa _setup _confirm _button ) }
disabled = { isLoading }
/ >
2022-03-21 11:09:01 -07:00
< / F o r m A c t i o n s >
< / F o r m >
< / S t a c k >
2020-08-07 13:17:13 -07:00
) ;
}
}