Migrate AuthenticationController to CompatController

Convert `AuthenticationController` from the legacy Zend Controller to
`CompatController`, dropping `login.phtml` in favour of the new `LoginPage`
widget and `addContent()`. The view variable assignments are replaced by
`setTitle()` and `addContent(new LoginPage(...))`. Improve `httpBadRequest()`
message for external redirect attempts.

In `CompatController`, `$this->controls` is the tab bar area rendered
above the page content. When no tabs are added it still emits an empty
`<div class="controls">` wrapper. Setting `$this->view->compact = true`
suppresses that wrapper entirely, keeping the login page markup clean.

Handle the redirect on success in an `ON_SUBMIT` event handler. Delegate
the external-backend-only check to `LoginForm::onRequest()` via `ON_REQUEST`.
Use `$this->getServerRequest()` instead of `ServerRequest::fromGlobals()`.

Two structural fixes required by the changed DOM nesting:
- `login.less`: height `100%` -> `100vh` (`#login` is now inside `.content`
  which has no explicit height, so percentage inheritance breaks)
- `history.js`: `#layout > #login` -> `#layout #login` (direct-child selector
  breaks because `#login` is now a grandchild of `#layout` through `.content`)
This commit is contained in:
Johannes Rauh 2026-04-30 10:50:40 +02:00
parent 1dd503e9af
commit 650ea2b597
4 changed files with 27 additions and 77 deletions

View file

@ -5,7 +5,6 @@
namespace Icinga\Controllers;
use GuzzleHttp\Psr7\ServerRequest;
use Icinga\Application\ClassLoader;
use Icinga\Application\Hook\AuthenticationHook;
use Icinga\Application\Hook\LoginButtonHook;
@ -16,17 +15,20 @@ use Icinga\Authentication\LoginButtonForm;
use Icinga\Common\Database;
use Icinga\Exception\AuthenticationException;
use Icinga\Forms\Authentication\LoginForm;
use Icinga\Web\Controller;
use Icinga\Web\Helper\CookieHelper;
use Icinga\Web\RememberMe;
use Icinga\Web\Url;
use Icinga\Web\Widget\LoginPage;
use ipl\Html\Contract\Form;
use ipl\Web\Compat\CompatController;
use Psr\Http\Message\ServerRequestInterface;
use RuntimeException;
use Throwable;
/**
* Application wide controller for authentication
*/
class AuthenticationController extends Controller
class AuthenticationController extends CompatController
{
use Database;
@ -49,7 +51,17 @@ class AuthenticationController extends Controller
if (($requiresSetup = $icinga->requiresSetup()) && $icinga->setupTokenExists()) {
$this->redirectNow(Url::fromPath('setup'));
}
$form = new LoginForm();
$form = (new LoginForm())
->setAction(Url::fromRequest()->getAbsoluteUrl())
->on(Form::ON_SUBMIT, function (LoginForm $form) {
if ($redirectUrl = $form->getRedirectUrl()) {
$this->redirectNow($redirectUrl);
}
})
->on(Form::ON_REQUEST, function (ServerRequestInterface $_, LoginForm $form) {
$form->onRequest();
});
if (RememberMe::hasCookie() && $this->hasDb()) {
$authenticated = false;
@ -81,14 +93,16 @@ class AuthenticationController extends Controller
if ($redirect) {
$redirectUrl = Url::fromPath($redirect, [], $this->getRequest());
if ($redirectUrl->isExternal()) {
$this->httpBadRequest('nope');
$this->httpBadRequest('Redirect to an external host is not allowed');
}
} else {
$redirectUrl = $form->getRedirectUrl();
$redirectUrl = $form->createRedirectUrl();
}
$this->redirectNow($redirectUrl);
}
$request = $this->getServerRequest();
if (! $requiresSetup) {
$cookies = new CookieHelper($this->getRequest());
if (! $cookies->isSupported()) {
@ -99,11 +113,10 @@ class AuthenticationController extends Controller
->sendResponse();
exit;
}
$form->handleRequest();
$form->handleRequest($request);
}
$loginButtons = [];
$request = ServerRequest::fromGlobals();
foreach (LoginButtonHook::all() as $class => $hook) {
try {
@ -126,10 +139,10 @@ class AuthenticationController extends Controller
}
}
$this->view->form = $form;
$this->view->loginButtons = $loginButtons;
$this->view->defaultTitle = $this->translate('Icinga Web 2 Login');
$this->view->requiresSetup = $requiresSetup;
// Suppress the rendering of controls bar
$this->view->compact = true;
$this->setTitle($this->translate('Icinga Web 2 Login'));
$this->addContent(new LoginPage($form, $loginButtons, $requiresSetup));
}
/**

View file

@ -1,63 +0,0 @@
<div id="login">
<div class="login-form" data-base-target="layout">
<div role="status" class="sr-only">
<?= $this->translate(
'Welcome to Icinga Web 2. For users of the screen reader Jaws full and expectant compliant'
. ' accessibility is possible only with use of the Firefox browser. VoiceOver on Mac OS X is tested on'
. ' Chrome, Safari and Firefox.'
) ?>
</div>
<div class="logo-wrapper"><div id="icinga-logo" aria-hidden="true"></div></div>
<?php if ($requiresSetup): ?>
<p class="config-note"><?= sprintf(
$this->translate(
'It appears that you did not configure Icinga Web 2 yet so it\'s not possible to log in without any defined '
. 'authentication method. Please define a authentication method by following the instructions in the'
. ' %1$sdocumentation%3$s or by using our %2$sweb-based setup-wizard%3$s.'
),
'<a href="https://icinga.com/docs/icinga-web-2/latest/doc/05-Authentication/#authentication" title="'
. $this->translate('Icinga Web 2 Documentation') . '">',
'<a href="' . $this->href('setup') . '" title="' . $this->translate('Icinga Web 2 Setup-Wizard') . '">',
'</a>'
) ?></p>
<?php endif ?>
<?= $this->form ?>
<?= implode('', $this->loginButtons) ?>
<div id="login-footer">
<p>Icinga Web 2 &copy; 2013-<?= date('Y') ?></p>
<?= $this->qlink($this->translate('icinga.com'), 'https://icinga.com') ?>
</div>
</div>
<ul id="social">
<li>
<?= $this->qlink(
null,
'https://www.facebook.com/icinga',
null,
array(
'target' => '_blank',
'icon' => 'facebook-squared',
'title' => $this->translate('Icinga on Facebook')
)
) ?>
</li>
<li><?= $this->qlink(
null,
'https://github.com/Icinga',
null,
array(
'target' => '_blank',
'icon' => 'github-circled',
'title' => $this->translate('Icinga on GitHub')
)
) ?>
</li>
</ul>
</div>
<div id="orb-analytics" class="orb" ><?= $this->img('img/orb-analytics.png'); ?></div>
<div id="orb-automation" class="orb"><?= $this->img('img/orb-automation.png'); ?></div>
<div id="orb-cloud" class="orb"><?= $this->img('img/orb-cloud.png'); ?></div>
<div id="orb-icinga" class="orb"><?= $this->img('img/orb-icinga.png'); ?></div>
<div id="orb-infrastructure" class="orb"><?= $this->img('img/orb-infrastructure.png'); ?></div>
<div id="orb-metrics" class="orb" ><?= $this->img('img/orb-metrics.png'); ?></div>
<div id="orb-notifactions" class="orb"><?= $this->img('img/orb-notifications.png'); ?></div>

View file

@ -4,7 +4,7 @@
// Login page styles
#login {
height: 100%;
height: 100vh;
background-color: @menu-bg-color;
background-image: url(../img/icingaweb2-background-orbs.jpg);
background-repeat: no-repeat;

View file

@ -230,7 +230,7 @@
applyLocationBar: function (onload = false) {
let col2State = this.getCol2State();
if (onload && document.querySelector('#layout > #login')) {
if (onload && document.querySelector('#layout #login')) {
// The user landed on the login
let redirectInput = document.querySelector('#login form input[name=redirect]');
redirectInput.value = redirectInput.value + col2State;