Skip to content

Commit bbea643

Browse files
committed
Improve TwoFactorConfigForm
A new class `FakeFormElement` is introduced which integrates `ValidHtml` into IPL Web `CompatForm`s. The following parts of the `TwoFactorConfigForm` are now added to the Form via the new class: - The QR code image - The manual auth URL for TOTP The manual auth URL is now wrapped by a copy-to-clipboard element, so the user doesn’t have to select it manually. Additionally the QR code image and the manual auth url have now descriptions.
1 parent 2c8ed1c commit bbea643

3 files changed

Lines changed: 101 additions & 23 deletions

File tree

application/forms/Account/TwoFactorConfigForm.php

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@
77
use Icinga\Authentication\TwoFactorTotp;
88
use Icinga\Common\Database;
99
use Icinga\User;
10+
use Icinga\Web\Form\Element\FakeFormElement;
1011
use Icinga\Web\Form\Validator\TotpTokenValidator;
1112
use Icinga\Web\Notification;
1213
use ipl\Html\Attributes;
1314
use ipl\Html\HtmlElement;
15+
use ipl\Html\Text;
1416
use ipl\Web\Common\FormUid;
1517
use ipl\Web\Compat\CompatForm;
1618
use ipl\Web\Url;
19+
use ipl\Web\Widget\CopyToClipboard;
1720

1821
/**
1922
* Form for enabling and disabling 2FA or creating and updating the 2FA TOTP secret
@@ -79,16 +82,16 @@ protected function assemble(): void
7982
'submit',
8083
static::SUBMIT_DISABLE,
8184
[
82-
'label' => $this->translate('Disable 2FA'),
83-
'data-progress-label' => $this->translate('Disabling')
85+
'label' => $this->translate('Disable 2FA'),
86+
'data-progress-label' => $this->translate('Disabling')
8487
]
8588
);
8689
} else {
8790
$this->addElement(
8891
'checkbox',
8992
'enabled_2fa',
9093
[
91-
'class' => 'autosubmit',
94+
'class' => 'autosubmit',
9295
'label' => $this->translate('Enable 2FA (TOTP)'),
9396
'description' => $this->translate(
9497
'This option allows you to enable or to disable the two factor authentication via TOTP.'
@@ -105,23 +108,26 @@ protected function assemble(): void
105108
$this->twoFactor = TwoFactorTotp::createFromSecret($secret, $this->user->getUsername());
106109
}
107110

108-
$this->addHtml(HtmlElement::create('img', Attributes::create([
109-
'class' => 'two-factor-totp-qr-code',
110-
'src' => $this->twoFactor->createQRCode()
111-
])));
112-
113-
$this->addElement(
114-
'textarea',
115-
'2fa_manual_auth_url',
116-
[
117-
'class' => 'two-factor-totp-auth-url',
118-
'ignore' => true,
119-
'disabled' => true,
120-
'label' => $this->translate('Manual Auth URL'),
121-
'value' => $this->twoFactor->getTotpAuthUrl(),
122-
'rows' => 4
123-
]
111+
$this->addHtml(new FakeFormElement(
112+
HtmlElement::create('img', Attributes::create([
113+
'class' => 'two-factor-totp-qr-code',
114+
'src' => $this->twoFactor->createQRCode()
115+
])),
116+
$this->translate('QR Code'),
117+
$this->translate('Use your authenticator app to scan the QR code.')
118+
));
119+
120+
$manualAuthUrl = HtmlElement::create(
121+
'div',
122+
Attributes::create(['class' => 'two-factor-totp-auth-url']),
123+
new Text($this->twoFactor->getTotpAuthUrl()),
124124
);
125+
CopyToClipboard::attachTo($manualAuthUrl);
126+
$this->addHtml(new FakeFormElement(
127+
$manualAuthUrl,
128+
$this->translate('Manual Auth URL'),
129+
$this->translate('If you have no camera to scan the QR code you can enter the auth URL manually.')
130+
));
125131

126132
$this->addElement(
127133
'text',
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
/* Icinga Web 2 | (c) 2025 Icinga GmbH | GPLv2+ */
4+
5+
namespace Icinga\Web\Form\Element;
6+
7+
use ipl\Html\Attributes;
8+
use ipl\Html\BaseHtmlElement;
9+
use ipl\Html\HtmlElement;
10+
use ipl\Html\Text;
11+
use ipl\Html\ValidHtml;
12+
use ipl\Web\Widget\Icon;
13+
14+
/**
15+
* This is an element that integrates {@see ValidHtml} into an IPL Web {@see CompatForm}.
16+
*
17+
* The html will be rendered as follows:
18+
* <pre>
19+
* <div class='control-group'>
20+
* <div class='control-label-group'>
21+
* <!-- if label is set -->
22+
* <label>$this->label</label>
23+
* </div>
24+
* <div class='fake-form-element'>
25+
* $this->content
26+
* </div>
27+
* <!-- if description is set -->
28+
* <i class="icon fa-info-circle control-info fa" role="image"
29+
* title="Please enter the token from your authenticator app to verify your setup."></i>
30+
* </div>
31+
* </pre>
32+
*/
33+
class FakeFormElement extends BaseHtmlElement
34+
{
35+
protected $tag = 'div';
36+
37+
protected $defaultAttributes = ['class' => 'control-group'];
38+
39+
/**
40+
* @param ValidHtml $content The element to add to the form
41+
* @param ?string $label The label to render in the control-label-group element
42+
* @param ?string $description The description to show for the element
43+
*/
44+
public function __construct(
45+
protected ValidHtml $content,
46+
protected ?string $label = null,
47+
protected ?string $description = null
48+
) {
49+
}
50+
51+
protected function assemble(): void
52+
{
53+
$this->addHtml(
54+
HtmlElement::create(
55+
'div',
56+
Attributes::create(['class' => 'control-label-group']),
57+
$this->label ? HtmlElement::create('label', null, new Text($this->label)) : null
58+
)
59+
);
60+
$this->addHtml(
61+
HtmlElement::create('div', Attributes::create(['class' => 'fake-form-element']), $this->content)
62+
);
63+
if ($this->description) {
64+
$this->addHtml(
65+
new Icon('info-circle', Attributes::create(['class' => 'control-info', 'title' => $this->description]))
66+
);
67+
}
68+
}
69+
}

public/css/icinga/forms.less

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,8 @@ form.icinga-form {
186186
input[type="file"],
187187
.control-group > fieldset,
188188
textarea,
189-
select {
189+
select,
190+
.control-group > .fake-form-element {
190191
flex: 1 1 auto;
191192
width: 0;
192193
}
@@ -606,12 +607,14 @@ form.icinga-form .form-info {
606607
}
607608

608609
.two-factor-totp-qr-code {
609-
display: block;
610-
margin: 0 auto;
611610
width: 20em;
612611
height: 20em;
613612
}
614613

615614
.two-factor-totp-auth-url {
616-
resize: none;
615+
background-color: @base-disabled;
616+
padding: @vertical-padding;
617+
border-radius: .25em;
618+
line-break: anywhere;
619+
user-select: all;
617620
}

0 commit comments

Comments
 (0)