Add copy button for recovery codes

Bug: T354028
Co-Authored-by: TheDJ <hartman.wiki@gmail.com>
Change-Id: I3edf9f698aa1f7f5a9881516027a65a88ea1cac4
This commit is contained in:
Reedy 2024-01-11 22:45:39 +00:00 committed by TheDJ
parent dc63d00723
commit ffb7da7a45
9 changed files with 106 additions and 18 deletions

View file

@ -1,7 +1,7 @@
{
"root": true,
"extends": [
"wikimedia/client-es5",
"wikimedia/client",
"wikimedia/jquery",
"wikimedia/mediawiki"
]

View file

@ -1,6 +1,6 @@
/* eslint-env node */
module.exports = function ( grunt ) {
var conf = grunt.file.readJSON( 'extension.json' );
const conf = grunt.file.readJSON( 'extension.json' );
grunt.loadNpmTasks( 'grunt-banana-checker' );
grunt.loadNpmTasks( 'grunt-eslint' );

View file

@ -101,16 +101,25 @@
}
},
"ResourceModules": {
"ext.oath.totp.showqrcode.styles": {
"ext.oath.styles": {
"class": "MediaWiki\\ResourceLoader\\CodexModule",
"styles": [
"totp/ext.oath.showqrcode.styles.less"
"totp/ext.oath.showqrcode.styles.less",
"recovery/ext.oauth.recovery.less"
],
"codexStyleOnly": "true",
"codexComponents": [
"CdxButton",
"CdxIcon"
]
},
"ext.oath": {
"packageFiles": [
"recovery/ext.oath.recovery.copy.js"
],
"messages": [
"oathauth-recoverycodes-copy-success"
]
}
},
"ResourceFileModulePaths": {

View file

@ -16,7 +16,9 @@
"oathauth-recoverycodes-important": "This step is important! Do not skip this step!",
"oathauth-recoverycodes-neveragain": "These codes will never be shown again!",
"oathauth-recoverytokens-createdat": "Recovery tokens created: $1",
"oathauth-recoverycodes-download": "Download recovery codes",
"oathauth-recoverycodes-download": "Download",
"oathauth-recoverycodes-copy": "Copy",
"oathauth-recoverycodes-copy-success": "Recovery codes were copied to your clipboard!",
"oathauth-disable": "Disable two-factor authentication",
"oathauth-validatedoath": "Validated two-factor credentials. Two-factor authentication will now be enforced.",
"oathauth-noscratchforvalidation": "You cannot use a recovery code to confirm two-factor authentication. Recovery codes are for backup and incidental use only. Please use a code from your two-factor authentication application (such as Google Authenticator).",

View file

@ -32,6 +32,8 @@
"oathauth-recoverycodes-neveragain": "Plain text, found on Special:OATH while enabling OATH.",
"oathauth-recoverytokens-createdat": "Plain text, found on Special:OATH while enabling OATH.",
"oathauth-recoverycodes-download": "Plain text, text of the download link on Special:OATH while enabling OATH.",
"oathauth-recoverycodes-copy": "Plain text, label of the button on Special:OATH to copy the recovery codes",
"oathauth-recoverycodes-copy-success": "Notification bubble presented on Special:OATH after copying recovery codes",
"oathauth-disable": "Page title on Special:OATH while disabling OATH.\n\nSee [https://en.wikipedia.org/wiki/Two_factor_authentication two factor authentication]",
"oathauth-validatedoath": "Plain text found on Special:OATH after a token has been validated.\n\nSee [https://en.wikipedia.org/wiki/Two_factor_authentication two factor authentication]",
"oathauth-noscratchforvalidation": "Plain text found on Special:OATH if the user used the incorrect type of token while enabling OATH.\n\nSee [https://en.wikipedia.org/wiki/Two_factor_authentication two factor authentication]",

View file

@ -0,0 +1,40 @@
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*/
class CopyButton {
static attach() {
// eslint-disable-next-line no-jquery/no-global-selector
$( '.mw-oathauth-recoverycodes-copy-button' )
.addClass( 'clipboard-api-supported' )
.on( 'click', ( e ) => {
e.preventDefault();
// eslint-disable-next-line compat/compat
navigator.clipboard.writeText( mw.config.get( 'oathauth-recoverycodes' ) ).then( () => {
mw.notify( mw.msg( 'oathauth-recoverycodes-copy-success' ), {
type: 'success',
tag: 'recoverycodes'
} );
} );
} );
}
}
if ( navigator.clipboard && navigator.clipboard.writeText ) {
// navigator.clipboard() is not supported in Safari 11.1, iOS Safari 11.3-11.4
$( CopyButton.attach );
}

View file

@ -0,0 +1,21 @@
@import 'mediawiki.skin.variables.less';
.mw-oathauth-recoverycodes-download-icon {
.cdx-mixin-css-icon( @cdx-icon-download, @param-is-button-icon: true );
}
.mw-oathauth-recoverycodes-copy-button {
&.cdx-button {
margin-right: @spacing-horizontal-button;
margin-inline-end: @spacing-horizontal-button;
}
.cdx-button__icon {
.cdx-mixin-css-icon( @cdx-icon-copy, @param-is-button-icon: true );
}
.client-nojs &,
&:not( .clipboard-api-supported ) {
display: none;
}
}

View file

@ -1,5 +1,3 @@
@import 'mediawiki.skin.variables.less';
kbd {
font-family: monospace, monospace;
white-space: nowrap;
@ -9,11 +7,3 @@ kbd {
fieldset {
page-break-inside: avoid;
}
.cdx-button.mw-oathauth-recoverycodes-download {
margin-top: 1em;
}
.mw-oathauth-recoverycodes-download-icon {
.cdx-mixin-css-icon( @cdx-icon-download, @param-is-button-icon: true );
}

View file

@ -20,7 +20,9 @@ class TOTPEnableForm extends OATHAuthOOUIHTMLForm {
* @return string
*/
public function getHTML( $submitResult ) {
$this->getOutput()->addModuleStyles( 'ext.oath.totp.showqrcode.styles' );
$out = $this->getOutput();
$out->addModuleStyles( 'ext.oath.styles' );
$out->addModules( 'ext.oath' );
return parent::getHTML( $submitResult );
}
@ -69,6 +71,8 @@ class TOTPEnableForm extends OATHAuthOOUIHTMLForm {
->build();
$now = wfTimestampNow();
$recoveryCodes = $this->getScratchTokensForDisplay( $key );
$this->getOutput()->addJsConfigVars( 'oathauth-recoverycodes', $this->createTextList( $recoveryCodes ) );
// messages used: oathauth-step1, oathauth-step2, oathauth-step3, oathauth-step4
return [
@ -115,9 +119,10 @@ class TOTPEnableForm extends OATHAuthOOUIHTMLForm {
. $this->msg( 'word-separator' )->escaped()
. $this->msg( 'parentheses' )->rawParams( wfTimestamp( TS_ISO_8601, $now ) )->escaped()
) . '<br/>' .
$this->createResourceList( $this->getScratchTokensForDisplay( $key ) ) . '<br/>' .
$this->createResourceList( $recoveryCodes ) . '<br/>' .
'<strong>' . $this->msg( 'oathauth-recoverycodes-neveragain' )->escaped() . '</strong><br/>' .
$this->createDownloadLink( $this->getScratchTokensForDisplay( $key ) ),
$this->createCopyButton() .
$this->createDownloadLink( $recoveryCodes ),
'raw' => true,
'section' => 'step3',
],
@ -146,6 +151,15 @@ class TOTPEnableForm extends OATHAuthOOUIHTMLForm {
return Html::rawElement( 'ul', [], $resourceList );
}
/**
* @param array $items
*
* @return string
*/
private function createTextList( $items ) {
return "* " . implode( "\n* ", $items );
}
private function createDownloadLink( array $scratchTokensForDisplay ): string {
$icon = Html::element( 'span', [
'class' => [ 'mw-oathauth-recoverycodes-download-icon', 'cdx-button__icon' ],
@ -167,6 +181,16 @@ class TOTPEnableForm extends OATHAuthOOUIHTMLForm {
);
}
private function createCopyButton(): string {
return Html::rawElement( 'button', [
'class' => 'cdx-button mw-oathauth-recoverycodes-copy-button'
], Html::element( 'span', [
'class' => 'cdx-button__icon',
'aria-hidden' => 'true',
] ) . $this->msg( 'oathauth-recoverycodes-copy' )->escaped()
);
}
/**
* Retrieve the current secret for display purposes
*