diff --git a/settings/rest-api.php b/settings/rest-api.php index 7e979e72..0b5063a5 100644 --- a/settings/rest-api.php +++ b/settings/rest-api.php @@ -213,9 +213,11 @@ function register_user_fields(): void { 'get_callback' => function( $user ) { $keys = WebAuthn_Credential_Store::get_user_keys( get_userdata( $user['id'] ) ); - // Remove sensitive and unnecessary data. array_walk( $keys, function( & $key ) { - $key = array_intersect_key( (array) $key, array_flip( [ 'id', 'name' ] ) ); + $key->delete_nonce = wp_create_nonce( 'delete-key_' . $key->credential_id ); + + // Remove unnecessary data. + $key = array_intersect_key( (array) $key, array_flip( [ 'id', 'credential_id', 'name', 'delete_nonce' ] ) ); } ); return $keys; diff --git a/settings/src/block.json b/settings/src/block.json index 0d411767..d3e151e2 100644 --- a/settings/src/block.json +++ b/settings/src/block.json @@ -17,7 +17,7 @@ "textdomain": "wporg", "editorScript": "file:./index.js", "editorStyle": "file:./index.css", - "viewScript": [ "file:./script.js", "zxcvbn-async", "two-factor-qr-code-generator" ], + "viewScript": [ "wp-util", "zxcvbn-async", "two-factor-qr-code-generator", "file:./script.js" ], "style": [ "file:./style-index.css", "wp-components" ], "render": "file:./render.php" } diff --git a/settings/src/components/webauthn/list-keys.js b/settings/src/components/webauthn/list-keys.js index 7ce107fc..6e3b9095 100644 --- a/settings/src/components/webauthn/list-keys.js +++ b/settings/src/components/webauthn/list-keys.js @@ -1,38 +1,134 @@ /** * WordPress dependencies */ -import { Button } from '@wordpress/components'; +import { Button, Modal, Notice, Spinner } from '@wordpress/components'; +import { useCallback, useContext, useState } from '@wordpress/element'; +import { Icon, cancelCircleFilled } from '@wordpress/icons'; -const confirm = window.confirm; +/** + * Internal dependencies + */ +import { GlobalContext } from '../../script'; +import { refreshRecord } from '../../utilities'; + +/** + * Global dependencies + */ +const ajax = wp.ajax; /** * Render the list of keys. + */ +export default function ListKeys() { + const { + user: { userRecord }, + setGlobalNotice, + } = useContext( GlobalContext ); + const keys = userRecord.record[ '2fa_webauthn_keys' ]; + + const [ modalKey, setModalKey ] = useState( null ); + const [ modalError, setModalError ] = useState( null ); + const [ deleting, setDeleting ] = useState( false ); + + /** + * After the user confirms their intent, POST an AJAX request to remove a key. + */ + const onConfirmDelete = useCallback( async () => { + setDeleting( true ); + + try { + await ajax.post( 'webauthn_delete_key', { + user_id: userRecord.record.id, + handle: modalKey.credential_id, + _ajax_nonce: modalKey.delete_nonce, + } ); + + setGlobalNotice( modalKey.name + ' has been deleted.' ); + setModalKey( null ); + refreshRecord( userRecord ); + } catch ( error ) { + // The endpoint returns some errors as a string, but others as an object. + setModalError( error?.responseJSON?.data || error ); + } finally { + setDeleting( false ); + } + }, [ modalKey ] ); + + return ( + <> +
+ Are you sure you want to remove the { keyToRemove.name }
security key?
+
+
@@ -75,7 +80,8 @@ function Manage( { setStep, userKeys } ) {