Facelift adding TOTP devices
authorTim Düsterhus <duesterhus@woltlab.com>
Fri, 6 Nov 2020 15:30:11 +0000 (16:30 +0100)
committerTim Düsterhus <duesterhus@woltlab.com>
Mon, 16 Nov 2020 16:28:53 +0000 (17:28 +0100)
com.woltlab.wcf/templates/__totpNewDeviceContainer.tpl [new file with mode: 0644]
com.woltlab.wcf/templates/__totpSecretField.tpl
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/Multifactor/Totp/Qr.js
wcfsetup/install/files/lib/system/user/multifactor/TotpMultifactorMethod.class.php
wcfsetup/install/files/lib/system/user/multifactor/totp/NewDeviceContainer.class.php [new file with mode: 0644]
wcfsetup/install/files/style/ui/accountSecurity.scss
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/User/Multifactor/Totp/Qr.ts

diff --git a/com.woltlab.wcf/templates/__totpNewDeviceContainer.tpl b/com.woltlab.wcf/templates/__totpNewDeviceContainer.tpl
new file mode 100644 (file)
index 0000000..f2c9db9
--- /dev/null
@@ -0,0 +1,44 @@
+<section id="{@$container->getPrefixedId()}Container"{*
+       *}{if !$container->getClasses()|empty} class="{implode from=$container->getClasses() item='class' glue=' '}{$class}{/implode}"{/if}{*
+       *}{foreach from=$container->getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{*
+       *}{if !$container->checkDependencies()} style="display: none;"{/if}{*
+*}>
+       {if $container->getLabel() !== null}
+               {if $container->getDescription() !== null}
+                       <header class="sectionHeader">
+                               <h2 class="sectionTitle">{@$container->getLabel()}{if $container->markAsRequired()} <span class="formFieldRequired">*</span>{/if}</h2>
+                               <p class="sectionDescription">{@$container->getDescription()}</p>
+                       </header>
+               {else}
+                       <h2 class="sectionTitle">{@$container->getLabel()}{if $container->markAsRequired()} <span class="formFieldRequired">*</span>{/if}</h2>
+               {/if}
+       {/if}
+       
+       <div class="multifactorTotpNewDevice">
+               {if $container->getNodeById('secret')->isAvailable()}
+                       {@$container->getNodeById('secret')->getHtml()}
+               {/if}
+               
+               <div class="multifactorTotpNewDeviceFields">
+                       {foreach from=$container item='child'}
+                               {if $child->getId() !== 'secret' && $child->getId() !== 'submitButton' && $child->isAvailable()}
+                                       {@$child->getHtml()}
+                               {/if}
+                       {/foreach}
+                       
+                       {if $container->getNodeById('submitButton')->isAvailable()}
+                               <div class="formSubmit">
+                                       {@$container->getNodeById('submitButton')->getHtml()}
+                               </div>
+                       {/if}
+               </div>
+       </div>
+</section>
+
+{include file='__formContainerDependencies'}
+
+<script data-relocate="true">
+       require(['WoltLabSuite/Core/Form/Builder/Field/Dependency/Container/Default'], function(DefaultContainerDependency) {
+               new DefaultContainerDependency('{@$container->getPrefixedId()}Container');
+       });
+</script>
index 023f7ecda13b7c347333519e1bf21f8b1eb206c4..eb3fe6d0c5980e4d481f41b4ba5e8d98172005d8 100644 (file)
@@ -1,5 +1,7 @@
 <div class="totpSecretContainer">
        <input type="hidden" name="{@$field->getPrefixedId()}" value="{$field->getSignedValue()}">
+       <canvas></canvas>
+
        <kbd {*
        *}class="totpSecret" {*
        *}data-issuer="{PAGE_TITLE}" {*
index 71ac33d0158ad4153dd2fc3a13092c7b347787df..e579d7d8b25b50348cced8847d60ed4e14b9bd37 100644 (file)
@@ -14,9 +14,11 @@ define(["require", "exports", "tslib", "qr-creator"], function (require, exports
         }
         const issuer = secret.dataset.issuer;
         const label = (issuer ? `${issuer}:` : "") + accountName;
+        const canvas = container.querySelector("canvas");
         qr_creator_1.default.render({
             text: `otpauth://totp/${encodeURIComponent(label)}?secret=${encodeURIComponent(secret.textContent)}${issuer ? `&issuer=${encodeURIComponent(issuer)}` : ""}`,
-        }, container);
+            size: canvas && canvas.clientWidth ? canvas.clientWidth : 200,
+        }, canvas || container);
     }
     exports.render = render;
     exports.default = render;
index a9816368385e54cb9303fe82832dc52bd1a390fa..d8c692cfb3e33bc0b3558afdd9c4133231bee042 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 namespace wcf\system\user\multifactor;
 use ParagonIE\ConstantTime\Hex;
+use wcf\system\form\builder\button\FormButton;
 use wcf\system\form\builder\container\FormContainer;
 use wcf\system\form\builder\field\HiddenFormField;
 use wcf\system\form\builder\field\IFormField;
@@ -10,6 +11,7 @@ use wcf\system\form\builder\field\validation\FormFieldValidationError;
 use wcf\system\form\builder\field\validation\FormFieldValidator;
 use wcf\system\form\builder\IFormDocument;
 use wcf\system\user\multifactor\totp\CodeFormField;
+use wcf\system\user\multifactor\totp\NewDeviceContainer;
 use wcf\system\user\multifactor\totp\SecretFormField;
 use wcf\system\user\multifactor\totp\Totp;
 use wcf\system\WCF;
@@ -44,11 +46,8 @@ class TotpMultifactorMethod implements IMultifactorMethod {
         * @inheritDoc
         */
        public function createManagementForm(IFormDocument $form, ?int $setupId, $returnData = null): void {
-               if ($setupId) {
-                       
-               }
-               
-               $newDeviceContainer = FormContainer::create('newDevice')
+               $form->addDefaultButton(false);
+               $newDeviceContainer = NewDeviceContainer::create()
                        ->label('wcf.user.security.multifactor.totp.newDevice')
                        ->appendChildren([
                                SecretFormField::create(),
@@ -69,6 +68,11 @@ class TotpMultifactorMethod implements IMultifactorMethod {
                                TextFormField::create('deviceName')
                                        ->label('wcf.user.security.multifactor.totp.deviceName')
                                        ->placeholder('wcf.user.security.multifactor.totp.deviceName.placeholder'),
+                               FormButton::create('submitButton')
+                                       ->label('wcf.global.button.submit')
+                                       ->accessKey('s')
+                                       ->submit(true)
+                                       ->addClass('buttonPrimary'),
                        ]);
                $form->appendChild($newDeviceContainer);
        }
diff --git a/wcfsetup/install/files/lib/system/user/multifactor/totp/NewDeviceContainer.class.php b/wcfsetup/install/files/lib/system/user/multifactor/totp/NewDeviceContainer.class.php
new file mode 100644 (file)
index 0000000..b3cf109
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+namespace wcf\system\user\multifactor\totp;
+use wcf\system\form\builder\container\FormContainer;
+use wcf\system\form\builder\field\TDefaultIdFormField;
+
+/**
+ * Shows the form to add a new TOTP device.
+ * 
+ * @author     Tim Duesterhus
+ * @copyright  2001-2020 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\System\User\Multifactor\Totp
+ * @since      5.4
+ */
+class NewDeviceContainer extends FormContainer {
+       use TDefaultIdFormField;
+       
+       /**
+        * @inheritDoc
+        */
+       protected $templateName = '__totpNewDeviceContainer';
+       
+       /**
+        * @inheritDoc
+        */
+       protected static function getDefaultId(): string {
+               return 'newDevice';
+       }
+}
index 9cf57e45ee2b44bf27bbca330ca0bbeadde7fb5d..476a0804e0de384688e5709aa571ecb89537cb00 100644 (file)
                }
        }
 }
+
+.multifactorTotpNewDevice {
+       display: flex;
+       
+       .totpSecretContainer {
+               text-align: center;
+               width: 250px;
+               margin: 0 5px;
+               
+               canvas {
+                       width: 200px;
+                       height: 200px;
+               }
+       }
+       
+       .multifactorTotpNewDeviceFields {
+               flex: 1 1 auto;
+       }
+}
index e9af381a4d3b51371f9da5c68e11e5857424123c..f00dd0fb08c78d879b4c9039c5867d31e30f3448 100644 (file)
@@ -14,13 +14,15 @@ export function render(container: HTMLElement): void {
   const issuer = secret.dataset.issuer;
   const label = (issuer ? `${issuer}:` : "") + accountName;
 
+  const canvas = container.querySelector("canvas");
   QrCreator.render(
     {
       text: `otpauth://totp/${encodeURIComponent(label)}?secret=${encodeURIComponent(secret.textContent!)}${
         issuer ? `&issuer=${encodeURIComponent(issuer)}` : ""
       }`,
+      size: canvas && canvas.clientWidth ? canvas.clientWidth : 200,
     },
-    container,
+    canvas || container,
   );
 }