From a58686d00f171176df20a5ac2a30596cabfb4d27 Mon Sep 17 00:00:00 2001 From: Robert Baruck Date: Mon, 12 Aug 2024 15:03:50 +0200 Subject: [PATCH 1/3] FEATURE: Add ability to show OTP secret for second factor setup on devices without camera --- Classes/Controller/BackendController.php | 17 +++ Classes/Controller/LoginController.php | 2 + Classes/Domain/Model/Dto/SecondFactorDto.php | 30 +---- Configuration/Settings.yaml | 7 + .../Integration/Controller/Backend/New.fusion | 123 ++++++++++++++---- .../Controller/Login/SetupSecondFactor.fusion | 2 + .../Pages/SetupSecondFactorPage.fusion | 66 ++++++++-- Resources/Private/Translations/de/Main.xlf | 12 ++ Resources/Private/Translations/en/Main.xlf | 9 ++ Resources/Public/Styles/Login.css | 122 +++++++++++++++++ Resources/Public/index.js | 63 +++++++++ 11 files changed, 392 insertions(+), 61 deletions(-) create mode 100644 Resources/Public/index.js diff --git a/Classes/Controller/BackendController.php b/Classes/Controller/BackendController.php index 86e223f..db94e8c 100644 --- a/Classes/Controller/BackendController.php +++ b/Classes/Controller/BackendController.php @@ -4,6 +4,8 @@ use Neos\Error\Messages\Message; use Neos\Flow\Annotations as Flow; +use Neos\Flow\Configuration\ConfigurationManager; +use Neos\Flow\Configuration\Exception\InvalidConfigurationTypeException; use Neos\Flow\I18n\Translator; use Neos\Flow\Mvc\Exception\StopActionException; use Neos\Flow\Mvc\FlashMessage\FlashMessageService; @@ -118,6 +120,8 @@ public function newAction(): void $qrCode = $this->tOTPService->generateQRCodeForTokenAndAccount($otp, $this->securityContext->getAccount()); $this->view->assignMultiple([ + 'styles' => array_filter($this->getNeosSettings()['userInterface']['backendLoginForm']['stylesheets']), + 'scripts' => array_filter($this->getNeosSettings()['userInterface']['backendLoginForm']['scripts']), 'secret' => $secret, 'qrCode' => $qrCode, 'flashMessages' => $this->flashMessageService @@ -220,4 +224,17 @@ public function deleteAction(SecondFactor $secondFactor): void $this->redirect('index'); } + + /** + * @return array + * @throws InvalidConfigurationTypeException + */ + protected function getNeosSettings(): array + { + $configurationManager = $this->objectManager->get(ConfigurationManager::class); + return $configurationManager->getConfiguration( + ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, + 'Neos.Neos' + ); + } } diff --git a/Classes/Controller/LoginController.php b/Classes/Controller/LoginController.php index 599feea..33c7ebc 100644 --- a/Classes/Controller/LoginController.php +++ b/Classes/Controller/LoginController.php @@ -95,6 +95,7 @@ public function askForSecondFactorAction(?string $username = null): void $this->view->assignMultiple([ 'styles' => array_filter($this->getNeosSettings()['userInterface']['backendLoginForm']['stylesheets']), + 'scripts' => array_filter($this->getNeosSettings()['userInterface']['backendLoginForm']['scripts']), 'username' => $username, 'site' => $currentSite, 'flashMessages' => $this->flashMessageService @@ -162,6 +163,7 @@ public function setupSecondFactorAction(?string $username = null): void $this->view->assignMultiple([ 'styles' => array_filter($this->getNeosSettings()['userInterface']['backendLoginForm']['stylesheets']), + 'scripts' => array_filter($this->getNeosSettings()['userInterface']['backendLoginForm']['scripts']), 'username' => $username, 'site' => $currentSite, 'secret' => $secret, diff --git a/Classes/Domain/Model/Dto/SecondFactorDto.php b/Classes/Domain/Model/Dto/SecondFactorDto.php index 4a46688..f1fd70d 100644 --- a/Classes/Domain/Model/Dto/SecondFactorDto.php +++ b/Classes/Domain/Model/Dto/SecondFactorDto.php @@ -3,34 +3,14 @@ namespace Sandstorm\NeosTwoFactorAuthentication\Domain\Model\Dto; use Neos\Neos\Domain\Model\User; -use Neos\Party\Domain\Model\Person; use Sandstorm\NeosTwoFactorAuthentication\Domain\Model\SecondFactor; -class SecondFactorDto +final class SecondFactorDto { - protected SecondFactor $secondFactor; - - protected User $user; - - public function __construct(SecondFactor $secondFactor, User $user = null) - { - $this->user = $user; - $this->secondFactor = $secondFactor; - } - - /** - * @return SecondFactor|string - */ - public function getSecondFactor(): SecondFactor - { - return $this->secondFactor; - } - - /** - * @return User - */ - public function getUser(): User + public function __construct( + readonly public SecondFactor $secondFactor, + readonly public ?User $user = null + ) { - return $this->user; } } diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index 8dc9f40..559bc32 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -11,6 +11,11 @@ Neos: label: 'Sandstorm.NeosTwoFactorAuthentication:Backend:module.label' description: 'Sandstorm.NeosTwoFactorAuthentication:Backend:module.description' icon: 'fas fa-qrcode' + additionalResources: + styleSheets: + - 'resource://Sandstorm.NeosTwoFactorAuthentication/Public/Styles/Login.css' + javaScripts: + - 'resource://Sandstorm.NeosTwoFactorAuthentication/Public/index.js' userInterface: translation: @@ -21,6 +26,8 @@ Neos: backendLoginForm: stylesheets: 'Sandstorm.NeosTwoFactorAuthentication:AdditionalStyles': 'resource://Sandstorm.NeosTwoFactorAuthentication/Public/Styles/Login.css' + scripts: + 'Sandstorm.NeosTwoFactorAuthentication:AdditionalScripts': 'resource://Sandstorm.NeosTwoFactorAuthentication/Public/index.js' Flow: http: diff --git a/Resources/Private/Fusion/Integration/Controller/Backend/New.fusion b/Resources/Private/Fusion/Integration/Controller/Backend/New.fusion index e9f3296..6bf08a2 100644 --- a/Resources/Private/Fusion/Integration/Controller/Backend/New.fusion +++ b/Resources/Private/Fusion/Integration/Controller/Backend/New.fusion @@ -7,31 +7,104 @@ Sandstorm.NeosTwoFactorAuthentication.BackendController.new = Sandstorm.NeosTwoF content = Neos.Fusion:Component { renderer = afx` - - - -
- -
- -
- -
- -
- - {I18n.id('module.new.submit-otp').package('Sandstorm.NeosTwoFactorAuthentication').source('Backend').translate()} - -
-
+ +
+ +
+ +
+ + +
+ +
+ + +
+
+

+ { + Array.join( + Array.map( + String.split(secret, ''), + char => Type.isNumeric(char) ? '' + char + '' : '' + char + '' + ), + '' + ) + } +

+ +
+ + + + + + +
+
+ + + + + + +
+
+ +
+ + +
+
+
+
+ +
+ +
+ +
+ + {I18n.id('module.new.submit-otp').package('Sandstorm.NeosTwoFactorAuthentication').source('Backend').translate()} + +
+
+ + + + ` } } diff --git a/Resources/Private/Fusion/Integration/Controller/Login/SetupSecondFactor.fusion b/Resources/Private/Fusion/Integration/Controller/Login/SetupSecondFactor.fusion index 054bab1..2c504d8 100644 --- a/Resources/Private/Fusion/Integration/Controller/Login/SetupSecondFactor.fusion +++ b/Resources/Private/Fusion/Integration/Controller/Login/SetupSecondFactor.fusion @@ -1,7 +1,9 @@ Sandstorm.NeosTwoFactorAuthentication.LoginController.setupSecondFactor = Sandstorm.NeosTwoFactorAuthentication:Page.SetupSecondFactorPage { site = ${site} styles = ${styles} + scripts = ${scripts} username = ${username} flashMessages = ${flashMessages} qrCode = ${qrCode} + secret = ${secret} } diff --git a/Resources/Private/Fusion/Presentation/Pages/SetupSecondFactorPage.fusion b/Resources/Private/Fusion/Presentation/Pages/SetupSecondFactorPage.fusion index 2e3986e..72f877f 100644 --- a/Resources/Private/Fusion/Presentation/Pages/SetupSecondFactorPage.fusion +++ b/Resources/Private/Fusion/Presentation/Pages/SetupSecondFactorPage.fusion @@ -1,6 +1,7 @@ prototype(Sandstorm.NeosTwoFactorAuthentication:Page.SetupSecondFactorPage) < prototype(Neos.Fusion:Component) { site = null styles = ${[]} + scripts = ${[]} username = '' flashMessages = ${[]} @@ -77,11 +78,59 @@ prototype(Sandstorm.NeosTwoFactorAuthentication:Page.SetupSecondFactorPage) < pr
-
+
+ +
+ +
+ + +
+
+

+ { + Array.join( + Array.map( + String.split(secret, ''), + char => Type.isNumeric(char) ? '' + char + '' : '' + char + '' + ), + '' + ) + } +

+ +
+ +
+
+ +
+
+ +
+ + +
+
+
+
+
- + + + ` diff --git a/Resources/Private/Translations/de/Main.xlf b/Resources/Private/Translations/de/Main.xlf index 56ac9f6..ece9013 100644 --- a/Resources/Private/Translations/de/Main.xlf +++ b/Resources/Private/Translations/de/Main.xlf @@ -22,6 +22,18 @@ OTP was registered successfully. Das OTP wurde erfolgreich registriert. + + Show Code + Code Anzeigen + + + Copy + Kopieren + + + Close + Schließen + diff --git a/Resources/Private/Translations/en/Main.xlf b/Resources/Private/Translations/en/Main.xlf index 1fbad6f..6a1a249 100644 --- a/Resources/Private/Translations/en/Main.xlf +++ b/Resources/Private/Translations/en/Main.xlf @@ -17,6 +17,15 @@ OTP was registered successfully. + + Show Code + + + Copy + + + Close + diff --git a/Resources/Public/Styles/Login.css b/Resources/Public/Styles/Login.css index ab42dda..bf9034a 100644 --- a/Resources/Public/Styles/Login.css +++ b/Resources/Public/Styles/Login.css @@ -14,3 +14,125 @@ .neos-two-factor-flashmessage-info { background-color: #5bc0de; } + +.neos-two-factor__secret-wrapper { + display: flex !important; +} + +.neos-two-factor__secret-wrapper dialog { + padding: 8px; + + border: none; +} + +.neos-two-factor__secret-wrapper dialog::backdrop { + background-color: rgba(0, 0, 0, 0.5); +} + +.neos-two-factor__secret-wrapper dialog > div { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; +} + +.neos-two-factor__secret-wrapper dialog .neos-two-factor__dialog__actions { + display: flex; + gap: 8px; +} + +.neos-two-factor__secret-wrapper dialog > div > .neos-actions { + max-width: 200px; +} + +.neos-two-factor__secret__copy__button { + display: flex !important; + gap: 8px; + align-items: center; + justify-content: center; +} + +.neos-two-factor__secret__copy__button svg { + height: 16px; + width: 16px; + + fill: #fff; + color: #fff; +} + +.neos-two-factor__hidden { + display: none !important; +} + +.neos-two-factor__secret { + position: relative; + display: block; + width: 100%; + overflow: hidden; +} + +.neos-two-factor__secret p { + overflow: scroll; + + color: #0f0f0f; + font-family: monospace; + letter-spacing: 2px; +} + +.neos-two-factor__secret .neos-two-factor__secret__number { + color: #007ead; +} + +.neos-two-factor__secret__overflow-indicator--left, +.neos-two-factor__secret__overflow-indicator--right { + position: absolute; + top: 0; + width: 10em; + height: 100%; + + display: flex; + align-items: center; + + pointer-events: none; +} + +.neos-two-factor__secret__overflow-indicator--left svg, +.neos-two-factor__secret__overflow-indicator--right svg { + height: 18px; + + fill: #1a1a1a; +} + +.neos-two-factor__secret__overflow-indicator--left { + left: 0; + + justify-content: left; + + background: linear-gradient(to left, rgba(255, 255, 255, 0), #fff); +} + +.neos-two-factor__secret__overflow-indicator--right { + right: 0; + + justify-content: right; + + background: linear-gradient(to right, rgba(255, 255, 255, 0), #fff); +} + +/* override custom neos backend scrollbar styles */ +.neos-two-factor__secret-wrapper ::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +.neos-two-factor__secret-wrapper ::-webkit-scrollbar-corner { + background-color: initial; +} + +.neos-two-factor__secret-wrapper ::-webkit-scrollbar-thumb { + background-color: initial; + border: initial; +} +.neos-two-factor__secret-wrapper ::-webkit-scrollbar-track { + background-color: initial; +} diff --git a/Resources/Public/index.js b/Resources/Public/index.js new file mode 100644 index 0000000..646cccb --- /dev/null +++ b/Resources/Public/index.js @@ -0,0 +1,63 @@ +window.addEventListener('load', function () { + document.querySelectorAll('.neos-two-factor__secret-wrapper') + .forEach(function (secretFormElement) { + const secretInput = secretFormElement.querySelector('input#secret') + const secretDialog = secretFormElement.querySelector('dialog') + const showSecretButton = secretFormElement.querySelector('.neos-two-factor__secret__show__button') + const secretDialogCloseButton = secretDialog.querySelector('.neos-two-factor__secret__close__button') + + showSecretButton.onclick = function () { + secretDialog.showModal() + } + + secretDialogCloseButton.onclick = function () { + secretDialog.close() + } + + const secretDisplay = secretDialog.querySelector('.neos-two-factor__secret') + const allChars = Array.from(secretDisplay.querySelectorAll('span')) + const firstChar = allChars[0] + const lastChar = allChars[allChars.length - 1] + const overflowIndicatorLeft = secretDialog.querySelector('.neos-two-factor__secret__overflow-indicator--left') + const overflowIndicatorRight = secretDialog.querySelector('.neos-two-factor__secret__overflow-indicator--right') + + const intersectionObserverFirstChar = new IntersectionObserver(function (entries) { + if (entries[0].isIntersecting) { + overflowIndicatorLeft.classList.add('neos-two-factor__hidden') + } else { + overflowIndicatorLeft.classList.remove('neos-two-factor__hidden') + } + }) + + const intersectionObserverLastChar = new IntersectionObserver(function (entries) { + if (entries[0].isIntersecting) { + overflowIndicatorRight.classList.add('neos-two-factor__hidden') + } else { + overflowIndicatorRight.classList.remove('neos-two-factor__hidden') + } + }) + + intersectionObserverFirstChar.observe(firstChar) + intersectionObserverLastChar.observe(lastChar) + + const copySecretButton = secretFormElement.querySelector('.neos-two-factor__secret__copy__button') + copySecretButton.onclick = function () { + + navigator.clipboard.writeText(secretInput.value) + .then(function () { + copySecretButton.querySelectorAll('span').forEach(element => { + element.classList.toggle('neos-two-factor__hidden') + }) + + return new Promise(function (resolve) { + setTimeout(resolve, 1000) + }) + }) + .then(function () { + copySecretButton.querySelectorAll('span').forEach(element => { + element.classList.toggle('neos-two-factor__hidden') + }) + }) + } + }) +}) From a0667d1e3ee4a8a5b6c70589473a668d052c272e Mon Sep 17 00:00:00 2001 From: Robert Baruck Date: Wed, 25 Sep 2024 10:32:54 +0200 Subject: [PATCH 2/3] TASK: Revert SecondFactorDto refactoring --- Classes/Domain/Model/Dto/SecondFactorDto.php | 29 ++++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/Classes/Domain/Model/Dto/SecondFactorDto.php b/Classes/Domain/Model/Dto/SecondFactorDto.php index f1fd70d..d87d5e0 100644 --- a/Classes/Domain/Model/Dto/SecondFactorDto.php +++ b/Classes/Domain/Model/Dto/SecondFactorDto.php @@ -5,12 +5,31 @@ use Neos\Neos\Domain\Model\User; use Sandstorm\NeosTwoFactorAuthentication\Domain\Model\SecondFactor; -final class SecondFactorDto +class SecondFactorDto { - public function __construct( - readonly public SecondFactor $secondFactor, - readonly public ?User $user = null - ) + protected SecondFactor $secondFactor; + + protected User $user; + + public function __construct(SecondFactor $secondFactor, User $user = null) + { + $this->user = $user; + $this->secondFactor = $secondFactor; + } + + /** + * @return SecondFactor|string + */ + public function getSecondFactor(): SecondFactor + { + return $this->secondFactor; + } + + /** + * @return User + */ + public function getUser(): User { + return $this->user; } } From acf27299e4bcee90f3f9118ca818898178a4a03d Mon Sep 17 00:00:00 2001 From: Robert Baruck Date: Thu, 26 Sep 2024 10:00:04 +0200 Subject: [PATCH 3/3] TASK: Clean up, fix issues --- .../Integration/Controller/Backend/New.fusion | 24 ++-- .../Pages/SetupSecondFactorPage.fusion | 27 +++- Resources/Public/Styles/Login.css | 31 +++- Resources/Public/index.js | 134 ++++++++++-------- 4 files changed, 135 insertions(+), 81 deletions(-) diff --git a/Resources/Private/Fusion/Integration/Controller/Backend/New.fusion b/Resources/Private/Fusion/Integration/Controller/Backend/New.fusion index 6bf08a2..16ed82a 100644 --- a/Resources/Private/Fusion/Integration/Controller/Backend/New.fusion +++ b/Resources/Private/Fusion/Integration/Controller/Backend/New.fusion @@ -36,21 +36,17 @@ Sandstorm.NeosTwoFactorAuthentication.BackendController.new = Sandstorm.NeosTwoF }

-
- - - - - - + -
- - - - - - +
diff --git a/Resources/Private/Fusion/Presentation/Pages/SetupSecondFactorPage.fusion b/Resources/Private/Fusion/Presentation/Pages/SetupSecondFactorPage.fusion index 72f877f..33f7c09 100644 --- a/Resources/Private/Fusion/Presentation/Pages/SetupSecondFactorPage.fusion +++ b/Resources/Private/Fusion/Presentation/Pages/SetupSecondFactorPage.fusion @@ -105,21 +105,36 @@ prototype(Sandstorm.NeosTwoFactorAuthentication:Page.SetupSecondFactorPage) < pr }

-
- + -
- +
diff --git a/Resources/Public/Styles/Login.css b/Resources/Public/Styles/Login.css index bf9034a..28d9b6f 100644 --- a/Resources/Public/Styles/Login.css +++ b/Resources/Public/Styles/Login.css @@ -16,6 +16,7 @@ } .neos-two-factor__secret-wrapper { + /* specificity hack */ display: flex !important; } @@ -46,12 +47,19 @@ } .neos-two-factor__secret__copy__button { + /* specificity hack */ display: flex !important; gap: 8px; align-items: center; justify-content: center; } +.neos-two-factor__secret__copy__button span, +.neos-two-factor__secret__copy__button span i { + display: flex; + align-items: center; +} + .neos-two-factor__secret__copy__button svg { height: 16px; width: 16px; @@ -61,6 +69,7 @@ } .neos-two-factor__hidden { + /* specificity hack */ display: none !important; } @@ -69,6 +78,17 @@ display: block; width: 100%; overflow: hidden; + + font-size: 14px; + line-height: 1.6em; +} + +.neos-two-factor__secret div, +.neos-two-factor__secret p, +.neos-two-factor__secret svg { + box-sizing: content-box; + margin: 0 !important; + padding: 0 !important; } .neos-two-factor__secret p { @@ -76,7 +96,10 @@ color: #0f0f0f; font-family: monospace; - letter-spacing: 2px; +} + +.neos-two-factor__secret span:nth-child(3n) { + margin-right: 4px; } .neos-two-factor__secret .neos-two-factor__secret__number { @@ -88,7 +111,7 @@ position: absolute; top: 0; width: 10em; - height: 100%; + height: 1.6em; display: flex; align-items: center; @@ -98,9 +121,9 @@ .neos-two-factor__secret__overflow-indicator--left svg, .neos-two-factor__secret__overflow-indicator--right svg { - height: 18px; + height: 1.2em; - fill: #1a1a1a; + fill: #3f3f3f; } .neos-two-factor__secret__overflow-indicator--left { diff --git a/Resources/Public/index.js b/Resources/Public/index.js index 646cccb..4d1d930 100644 --- a/Resources/Public/index.js +++ b/Resources/Public/index.js @@ -1,63 +1,83 @@ +// Progressively enhance the secret form element on load window.addEventListener('load', function () { document.querySelectorAll('.neos-two-factor__secret-wrapper') - .forEach(function (secretFormElement) { - const secretInput = secretFormElement.querySelector('input#secret') - const secretDialog = secretFormElement.querySelector('dialog') - const showSecretButton = secretFormElement.querySelector('.neos-two-factor__secret__show__button') - const secretDialogCloseButton = secretDialog.querySelector('.neos-two-factor__secret__close__button') - - showSecretButton.onclick = function () { - secretDialog.showModal() - } - - secretDialogCloseButton.onclick = function () { - secretDialog.close() - } - - const secretDisplay = secretDialog.querySelector('.neos-two-factor__secret') - const allChars = Array.from(secretDisplay.querySelectorAll('span')) - const firstChar = allChars[0] - const lastChar = allChars[allChars.length - 1] - const overflowIndicatorLeft = secretDialog.querySelector('.neos-two-factor__secret__overflow-indicator--left') - const overflowIndicatorRight = secretDialog.querySelector('.neos-two-factor__secret__overflow-indicator--right') - - const intersectionObserverFirstChar = new IntersectionObserver(function (entries) { - if (entries[0].isIntersecting) { - overflowIndicatorLeft.classList.add('neos-two-factor__hidden') - } else { - overflowIndicatorLeft.classList.remove('neos-two-factor__hidden') - } + .forEach(progressivelyEnhanceSecretFormElement) +}) + +function progressivelyEnhanceSecretFormElement(secretFormElement) { + // Collect inner elements (scoped to the secretFormElement) + const secretInput = secretFormElement.querySelector('input#secret') + const secretDialog = secretFormElement.querySelector('dialog') + const showSecretButton = secretFormElement.querySelector('.neos-two-factor__secret__show__button') + const secretDialogCloseButton = secretDialog.querySelector('.neos-two-factor__secret__close__button') + const secretDisplay = secretDialog.querySelector('.neos-two-factor__secret') + + // Init secret modal buttons + showSecretButton.onclick = function () { + secretDialog.showModal() + } + + secretDialogCloseButton.onclick = function () { + secretDialog.close() + } + + // Init overflow indicators + // Each character are wrapped in a span element (so we can have different styles for numbers and letters) + const allCharElements = Array.from(secretDisplay.querySelectorAll('span')) + const firstCharElement = allCharElements[0] + const lastCharElement = allCharElements[allCharElements.length - 1] + + const overflowIndicatorLeft = secretDialog.querySelector('.neos-two-factor__secret__overflow-indicator--left') + const overflowIndicatorRight = secretDialog.querySelector('.neos-two-factor__secret__overflow-indicator--right') + + const intersectionObserverOptions = { + threshold: 0.9 + } + + const firstCharIntersectionObserver = new IntersectionObserver(function (entries) { + // Hide or show indicator when first character is visible or not visible respectively + if (entries[0].isIntersecting) { + overflowIndicatorLeft.classList.add('neos-two-factor__hidden') + } else { + overflowIndicatorLeft.classList.remove('neos-two-factor__hidden') + } + }, intersectionObserverOptions) + + const lastCharIntersectionObserver = new IntersectionObserver(function (entries) { + // Hide or show indicator when last character is visible or not visible respectively + if (entries[0].isIntersecting) { + overflowIndicatorRight.classList.add('neos-two-factor__hidden') + } else { + overflowIndicatorRight.classList.remove('neos-two-factor__hidden') + } + }, intersectionObserverOptions) + + firstCharIntersectionObserver.observe(firstCharElement) + lastCharIntersectionObserver.observe(lastCharElement) + + // Init copy secret button + const copySecretButton = secretFormElement.querySelector('.neos-two-factor__secret__copy__button') + copySecretButton.onclick = async function () { + try { + // Copy secret to clipboard + await navigator.clipboard.writeText(secretInput.value) + // Disable button and show success indicator + copySecretButton.setAttribute('disabled', 'disabled') + copySecretButton.querySelectorAll('span').forEach(element => { + element.classList.toggle('neos-two-factor__hidden') }) - const intersectionObserverLastChar = new IntersectionObserver(function (entries) { - if (entries[0].isIntersecting) { - overflowIndicatorRight.classList.add('neos-two-factor__hidden') - } else { - overflowIndicatorRight.classList.remove('neos-two-factor__hidden') - } + // Wait for 1 second + await new Promise(function (resolve) { + setTimeout(resolve, 1000) }) - intersectionObserverFirstChar.observe(firstChar) - intersectionObserverLastChar.observe(lastChar) - - const copySecretButton = secretFormElement.querySelector('.neos-two-factor__secret__copy__button') - copySecretButton.onclick = function () { - - navigator.clipboard.writeText(secretInput.value) - .then(function () { - copySecretButton.querySelectorAll('span').forEach(element => { - element.classList.toggle('neos-two-factor__hidden') - }) - - return new Promise(function (resolve) { - setTimeout(resolve, 1000) - }) - }) - .then(function () { - copySecretButton.querySelectorAll('span').forEach(element => { - element.classList.toggle('neos-two-factor__hidden') - }) - }) - } - }) -}) + } finally { + // Re-enable button and hide success indicator + copySecretButton.removeAttribute('disabled') + copySecretButton.querySelectorAll('span').forEach(element => { + element.classList.toggle('neos-two-factor__hidden') + }) + } + } +}