Skip to content

Commit

Permalink
Use component based approach for backup codes auto generation
Browse files Browse the repository at this point in the history
  • Loading branch information
adamwoodnz committed Aug 28, 2024
1 parent b5aa09e commit b85ea72
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 188 deletions.
158 changes: 158 additions & 0 deletions settings/src/components/backup-codes-setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import apiFetch from '@wordpress/api-fetch';
import { useContext, useCallback, useEffect, useState } from '@wordpress/element';
import { Button, ButtonGroup, CheckboxControl, Flex, Notice, Spinner } from '@wordpress/components';
import { Icon, warning, cancelCircleFilled } from '@wordpress/icons';
import { GlobalContext } from '../script';
import { refreshRecord } from '../utilities/common';
import { IntroText } from './backup-codes';
import CopyToClipboardButton from './copy-to-clipboard-button';
import DownloadButton from './download-button';
import PrintButton from './print-button';

/**
* Setup the Backup Codes provider.
*
* @param props
* @param props.onSuccess
*/
export function Setup( { onSuccess } ) {
const {
setGlobalNotice,
user: { userRecord },
setError,
error,
setBackupCodesVerified,
} = useContext( GlobalContext );
const [ backupCodes, setBackupCodes ] = useState( [] );
const [ hasPrinted, setHasPrinted ] = useState( false );

// Generate new backup codes and save them in usermeta.
useEffect( () => {
// useEffect callbacks can't be async directly, because that'd return the promise as a "cleanup" function.
const generateCodes = async () => {
// This will save the backup codes and enable the provider, which isn't really what we want, but that
// mimics the upstream plugin. It's probably better to fix it there first, and then update this, to
// make sure we stay in sync with upstream.
// See https://github.com/WordPress/two-factor/issues/507
try {
const response = await apiFetch( {
path: '/two-factor/1.0/generate-backup-codes',
method: 'POST',
data: {
user_id: userRecord.record.id,
enable_provider: true,
},
} );

setBackupCodes( response.codes );

// Update the Account Status screen in case they click `Back` without verifying the codes, but
// don't redirect to the Manage screen yet. This is mainly due to the side-effects of
// `two-factor/#507`, so it will need to be modified or maybe removed when that is fixed upstream.
setBackupCodesVerified( false );
await refreshRecord( userRecord );
} catch ( apiFetchError ) {
setError( apiFetchError );
}
};

generateCodes();
// This should only run once on mount.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [] );

// Finish the setup process.
const handleFinished = useCallback( async () => {

Check warning on line 65 in settings/src/components/backup-codes-setup.js

View workflow job for this annotation

GitHub Actions / All

React Hook useCallback does nothing when called with only one argument. Did you forget to pass an array of dependencies?
// TODO: Add try catch here after https://github.com/WordPress/wporg-two-factor/pull/187/files is merged.
// The codes have already been saved to usermeta, see `generateCodes()` above.
setBackupCodesVerified( true );
setGlobalNotice( 'Backup codes have been enabled.' );
onSuccess();
} );

return (
<>
<div className="wporg-2fa__screen-intro">
<IntroText />

<p>Please print the codes and keep them in a safe place.</p>

<Notice status="warning" isDismissible={ false }>
<Icon icon={ warning } className="wporg-2fa__print-codes-warning" />
Without access to the one-time password app or a backup code, you will lose
access to your account. Once you navigate away from this page, you will not be
be able to view these codes again.
</Notice>
</div>

{ error ? (
<Notice status="error" isDismissible={ false }>
<Icon icon={ cancelCircleFilled } />
{ error.message }
</Notice>
) : (
<>
<CodeList codes={ backupCodes } />

<CheckboxControl
label="I have printed or saved these codes"
checked={ hasPrinted }
onChange={ setHasPrinted }
disabled={ error }
/>
</>
) }

<Flex justify="flex-start" align="center" className="wporg-2fa__submit-actions">
<Button
isPrimary={ hasPrinted }
isSecondary={ ! hasPrinted }
disabled={ ! hasPrinted }
onClick={ handleFinished }
>
Done
</Button>
</Flex>
</>
);
}

/**
* Display a list of backup codes and actions
*
* @param props
* @param props.codes
*/

export function CodeList( { codes } ) {
const hasCodes = !! codes.length;

return (
<>
<div className="wporg-2fa__backup-codes-list">
{ hasCodes ? (
<ol>
{ codes.map( ( code ) => {
return (
<li key={ code } className="wporg-2fa__token">
{ code.slice( 0, 4 ) + ' ' + code.slice( 4 ) }
</li>
);
} ) }
</ol>
) : (
<p>
<Spinner /> Generating backup codes...
</p>
) }
</div>
{ hasCodes && (
<ButtonGroup>
<CopyToClipboardButton codes={ codes } />
<PrintButton />
<DownloadButton codes={ codes } />
</ButtonGroup>
) }
</>
);
}
188 changes: 8 additions & 180 deletions settings/src/components/backup-codes.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,26 @@
/**
* WordPress dependencies
*/
import apiFetch from '@wordpress/api-fetch';
import { useContext, useCallback, useEffect, useState } from '@wordpress/element';
import { Button, ButtonGroup, CheckboxControl, Flex, Notice, Spinner } from '@wordpress/components';
import { useContext } from '@wordpress/element';
import { Button, Notice } from '@wordpress/components';
import { Icon, warning, cancelCircleFilled } from '@wordpress/icons';

/**
* Internal dependencies
*/
import { GlobalContext } from '../script';
import { refreshRecord } from '../utilities/common';
import ScreenLink from './screen-link';
import CopyToClipboardButton from './copy-to-clipboard-button';
import PrintButton from './print-button';
import DownloadButton from './download-button';

/**
* Setup and manage backup codes.
*
* @param props

Check failure on line 17 in settings/src/components/backup-codes.js

View workflow job for this annotation

GitHub Actions / All

@param "props" does not match an existing function parameter
* @param props.onSuccess
*/
export default function BackupCodes( { onSuccess = () => {} } ) {
export default function BackupCodes() {
const {
user: { hasPrimaryProvider },
shouldAutoGenerateBackupCodes,
} = useContext( GlobalContext );
const [ generating, setGenerating ] = useState( false );

useEffect( () => {
if ( shouldAutoGenerateBackupCodes ) {
setGenerating( true );
}
// This should only run once on mount.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [] );

// Prevent users from accessing directly through the URL.
if ( ! hasPrimaryProvider ) {
Expand All @@ -49,165 +34,10 @@ export default function BackupCodes( { onSuccess = () => {} } ) {
);
}

return generating ? (
<Setup setGenerating={ setGenerating } onSuccess={ onSuccess } />
) : (
<Manage setGenerating={ setGenerating } />
);
}

/**
* Setup the Backup Codes provider.
*
* @param props
* @param props.setGenerating
* @param props.onSuccess
*/
function Setup( { setGenerating, onSuccess } ) {
const {
setGlobalNotice,
user: { userRecord },
setError,
error,
setBackupCodesVerified,
setShouldAutoGenerateBackupCodes,
} = useContext( GlobalContext );
const [ backupCodes, setBackupCodes ] = useState( [] );
const [ hasPrinted, setHasPrinted ] = useState( false );

// Generate new backup codes and save them in usermeta.
useEffect( () => {
// useEffect callbacks can't be async directly, because that'd return the promise as a "cleanup" function.
const generateCodes = async () => {
// This will save the backup codes and enable the provider, which isn't really what we want, but that
// mimics the upstream plugin. It's probably better to fix it there first, and then update this, to
// make sure we stay in sync with upstream.
// See https://github.com/WordPress/two-factor/issues/507
try {
const response = await apiFetch( {
path: '/two-factor/1.0/generate-backup-codes',
method: 'POST',
data: {
user_id: userRecord.record.id,
enable_provider: true,
},
} );

setBackupCodes( response.codes );
setShouldAutoGenerateBackupCodes( false );

// Update the Account Status screen in case they click `Back` without verifying the codes, but
// don't redirect to the Manage screen yet. This is mainly due to the side-effects of
// `two-factor/#507`, so it will need to be modified or maybe removed when that is fixed upstream.
setBackupCodesVerified( false );
await refreshRecord( userRecord );
} catch ( apiFetchError ) {
setError( apiFetchError );
}
};

generateCodes();
// This should only run once on mount.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [] );

// Finish the setup process.
const handleFinished = useCallback( async () => {
// TODO: Add try catch here after https://github.com/WordPress/wporg-two-factor/pull/187/files is merged.
// The codes have already been saved to usermeta, see `generateCodes()` above.
setBackupCodesVerified( true );
setGlobalNotice( 'Backup codes have been enabled.' );
setGenerating( false );
onSuccess();
} );

return (
<>
<div className="wporg-2fa__screen-intro">
<IntroText />

<p>Please print the codes and keep them in a safe place.</p>

<Notice status="warning" isDismissible={ false }>
<Icon icon={ warning } className="wporg-2fa__print-codes-warning" />
Without access to the one-time password app or a backup code, you will lose
access to your account. Once you navigate away from this page, you will not be
be able to view these codes again.
</Notice>
</div>

{ error ? (
<Notice status="error" isDismissible={ false }>
<Icon icon={ cancelCircleFilled } />
{ error.message }
</Notice>
) : (
<>
<CodeList codes={ backupCodes } />

<CheckboxControl
label="I have printed or saved these codes"
checked={ hasPrinted }
onChange={ setHasPrinted }
disabled={ error }
/>
</>
) }

<Flex justify="flex-start" align="center" className="wporg-2fa__submit-actions">
<Button
isPrimary={ hasPrinted }
isSecondary={ ! hasPrinted }
disabled={ ! hasPrinted }
onClick={ handleFinished }
>
Done
</Button>
</Flex>
</>
);
return <Manage />;
}

/**
* Display a list of backup codes and actions
*
* @param props
* @param props.codes
*/
function CodeList( { codes } ) {
const hasCodes = !! codes.length;

return (
<>
<div className="wporg-2fa__backup-codes-list">
{ hasCodes ? (
<ol>
{ codes.map( ( code ) => {
return (
<li key={ code } className="wporg-2fa__token">
{ code.slice( 0, 4 ) + ' ' + code.slice( 4 ) }
</li>
);
} ) }
</ol>
) : (
<p>
<Spinner /> Generating backup codes...
</p>
) }
</div>
{ hasCodes && (
<ButtonGroup>
<CopyToClipboardButton codes={ codes } />
<PrintButton />
<DownloadButton codes={ codes } />
</ButtonGroup>
) }
</>
);
}

const IntroText = () => (
export const IntroText = () => (
<p>
Backup codes let you access your account if your primary two-factor authentication method is
unavailable, like if your phone is lost or stolen. Each code can only be used once.
Expand All @@ -216,13 +46,11 @@ const IntroText = () => (

/**
* Render the screen where users can manage Backup Codes.
*
* @param props
* @param props.setGenerating
*/
function Manage( { setGenerating } ) {
function Manage() {
const {
user: { backupCodesEnabled, backupCodesRemaining },
navigateToScreen,
} = useContext( GlobalContext );

return (
Expand All @@ -249,7 +77,7 @@ function Manage( { setGenerating } ) {
) }
</div>

<Button isSecondary onClick={ () => setGenerating( true ) }>
<Button isSecondary onClick={ () => navigateToScreen( 'backup-codes-setup' ) }>
{ backupCodesEnabled ? 'Regenerate backup codes' : 'Generate backup codes' }
</Button>
</>
Expand Down
Loading

0 comments on commit b85ea72

Please sign in to comment.