Adds a setting to disable first time wizard for new users (#5938)

This commit is contained in:
Tobias Perschon
2026-05-23 18:48:42 +02:00
committed by GitHub
parent eeeeae41e7
commit 8d245ae223
10 changed files with 146 additions and 6 deletions

View File

@@ -370,6 +370,11 @@ final class SystemConfiguration
return $this->getDefaultCurrency(); return $this->getDefaultCurrency();
} }
public function isUserWizardActive(): bool
{
return (bool) $this->find('user.wizard');
}
// ========== Timesheet configurations ========== // ========== Timesheet configurations ==========
/* /*
public function getTimesheetBreakWarningDuration(): int public function getTimesheetBreakWarningDuration(): int

View File

@@ -546,6 +546,10 @@ final class SystemConfigurationController extends AbstractController
->setLabel('theme.avatar_url') ->setLabel('theme.avatar_url')
->setType(YesNoType::class) ->setType(YesNoType::class)
->setTranslationDomain('system-configuration'), ->setTranslationDomain('system-configuration'),
(new Configuration('user.wizard'))
->setLabel('user_auth_wizard')
->setType(YesNoType::class)
->setTranslationDomain('system-configuration'),
]), ]),
(new SystemConfigurationModel('theme')) (new SystemConfigurationModel('theme'))
->setConfiguration([ ->setConfiguration([

View File

@@ -568,6 +568,9 @@ final class Configuration implements ConfigurationInterface
->booleanNode('registration') ->booleanNode('registration')
->defaultFalse() ->defaultFalse()
->end() ->end()
->booleanNode('wizard')
->defaultTrue()
->end()
->booleanNode('password_reset') ->booleanNode('password_reset')
->defaultTrue() ->defaultTrue()
->end() ->end()

View File

@@ -9,6 +9,7 @@
namespace App\EventSubscriber; namespace App\EventSubscriber;
use App\Configuration\SystemConfiguration;
use App\Entity\User; use App\Entity\User;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RedirectResponse;
@@ -23,7 +24,8 @@ class WizardSubscriber implements EventSubscriberInterface
public function __construct( public function __construct(
private UrlGeneratorInterface $urlGenerator, private UrlGeneratorInterface $urlGenerator,
private AuthorizationCheckerInterface $security, private AuthorizationCheckerInterface $security,
private TokenStorageInterface $storage private TokenStorageInterface $storage,
private SystemConfiguration $systemConfiguration
) { ) {
} }
@@ -63,6 +65,15 @@ class WizardSubscriber implements EventSubscriberInterface
return; return;
} }
if ($user->requiresPasswordReset()) {
$response = new RedirectResponse($this->urlGenerator->generate('wizard', ['wizard' => 'password']));
$event->setResponse($response);
}
if ($user->isRegularUserOnly() && !$this->systemConfiguration->isUserWizardActive()) {
return;
}
foreach (User::WIZARDS as $wizard) { foreach (User::WIZARDS as $wizard) {
if (!$user->hasSeenWizard($wizard)) { if (!$user->hasSeenWizard($wizard)) {
$response = new RedirectResponse($this->urlGenerator->generate('wizard', ['wizard' => $wizard])); $response = new RedirectResponse($this->urlGenerator->generate('wizard', ['wizard' => $wizard]));
@@ -71,10 +82,5 @@ class WizardSubscriber implements EventSubscriberInterface
return; return;
} }
} }
if ($user->requiresPasswordReset()) {
$response = new RedirectResponse($this->urlGenerator->generate('wizard', ['wizard' => 'password']));
$event->setResponse($response);
}
} }
} }

View File

@@ -105,6 +105,9 @@ class SystemConfigurationTest extends TestCase
'company' => 'Acme Corp.', 'company' => 'Acme Corp.',
], ],
], ],
'user' => [
'wizard' => true,
],
]; ];
} }
@@ -125,6 +128,7 @@ class SystemConfigurationTest extends TestCase
(new Configuration())->setName('timesheet.markdown_content')->setValue('1'), (new Configuration())->setName('timesheet.markdown_content')->setValue('1'),
(new Configuration())->setName('timesheet.default_begin')->setValue('07:00'), (new Configuration())->setName('timesheet.default_begin')->setValue('07:00'),
(new Configuration())->setName('timesheet.active_entries.hard_limit')->setValue('7'), (new Configuration())->setName('timesheet.active_entries.hard_limit')->setValue('7'),
(new Configuration())->setName('user.wizard')->setValue(false),
]; ];
} }
@@ -226,6 +230,18 @@ class SystemConfigurationTest extends TestCase
self::assertEquals('IT', $sut->getUserDefaultLanguage()); self::assertEquals('IT', $sut->getUserDefaultLanguage());
} }
public function testUserWizardWithoutLoader(): void
{
$sut = $this->getSut($this->getDefaultSettings(), []);
self::assertTrue($sut->isUserWizardActive());
}
public function testUserWizardWithLoader(): void
{
$sut = $this->getSut($this->getDefaultSettings(), $this->getDefaultLoaderSettings());
self::assertFalse($sut->isUserWizardActive());
}
public function testTimesheetWithoutLoader(): void public function testTimesheetWithoutLoader(): void
{ {
$sut = $this->getSut($this->getDefaultSettings(), []); $sut = $this->getSut($this->getDefaultSettings(), []);

View File

@@ -9,7 +9,9 @@
namespace App\Tests\Controller; namespace App\Tests\Controller;
use App\DataFixtures\UserFixtures;
use App\Entity\User; use App\Entity\User;
use App\Entity\UserPreference;
use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Group;
#[Group('integration')] #[Group('integration')]
@@ -37,6 +39,97 @@ class WizardControllerTest extends AbstractControllerBaseTestCase
$this->assertAccessIsGranted($client, '/wizard/profile'); $this->assertAccessIsGranted($client, '/wizard/profile');
} }
public function testWizardDoesNotAppearOnFirstLoginIfDisabled(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->setSystemConfiguration('user.wizard', false);
$user = $this->loadUserFromDatabase(UserFixtures::USERNAME_USER);
$user->setPreferenceValue('__wizards__', null);
$user->setRequiresPasswordReset(false);
$this->getEntityManager()->persist($user);
$this->getEntityManager()->flush();
$this->request($client, '/timesheet/');
self::assertTrue($client->getResponse()->isSuccessful());
self::assertFalse($client->getResponse()->isRedirect());
}
public function testWizardAppearsOnFirstLoginWithDefaultConfiguration(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$user = $this->loadUserFromDatabase(UserFixtures::USERNAME_USER);
$user->setPreferenceValue('__wizards__', null);
$user->setRequiresPasswordReset(false);
$this->getEntityManager()->persist($user);
$this->getEntityManager()->flush();
$this->request($client, '/timesheet/');
$this->assertIsRedirect($client, '/wizard/intro');
}
public function testProfileWizardSubmitRedirectsToDoneAndMarksSeen(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$user = $this->loadUserFromDatabase(UserFixtures::USERNAME_USER);
$user->setPreferenceValue('__wizards__', null);
$user->setRequiresPasswordReset(false);
$this->getEntityManager()->persist($user);
$this->getEntityManager()->flush();
$crawler = $this->request($client, '/wizard/profile');
$form = $crawler->filter('form[name=form]')->form();
$values = $form->getPhpValues();
$values['form']['reload'] = '0';
$values['form'][UserPreference::LANGUAGE] = 'en';
$values['form'][UserPreference::LOCALE] = 'en';
$values['form'][UserPreference::TIMEZONE] = 'Europe/Berlin';
$values['form'][UserPreference::SKIN] = 'auto';
$client->submit($form, $values);
$this->assertIsRedirect($client, '/wizard/done');
$this->getEntityManager()->clear();
$user = $this->loadUserFromDatabase(UserFixtures::USERNAME_USER);
self::assertTrue($user->hasSeenWizard('profile'));
}
public function testProfileWizardSubmitReloadsProfileWhenRequested(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$crawler = $this->request($client, '/wizard/profile');
$form = $crawler->filter('form[name=form]')->form();
$values = $form->getPhpValues();
$values['form']['reload'] = '1';
$client->submit($form, $values);
$this->assertIsRedirect($client, '/wizard/profile');
}
public function testProfileWizardSubmitRedirectsToPasswordIfResetRequired(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$user = $this->loadUserFromDatabase(UserFixtures::USERNAME_USER);
$user->setRequiresPasswordReset(true);
$this->getEntityManager()->persist($user);
$this->getEntityManager()->flush();
$crawler = $this->request($client, '/wizard/profile');
$form = $crawler->filter('form[name=form]')->form();
$values = $form->getPhpValues();
$values['form']['reload'] = '0';
$client->submit($form, $values);
$this->assertIsRedirect($client, '/wizard/password');
}
public function testDoneWizard(): void public function testDoneWizard(): void
{ {
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER); $client = $this->getClientForAuthenticatedUser(User::ROLE_USER);

View File

@@ -412,6 +412,7 @@ class ConfigurationTest extends TestCase
'user' => [ 'user' => [
'registration' => false, 'registration' => false,
'password_reset' => true, 'password_reset' => true,
'wizard' => true,
'login' => true, 'login' => true,
'password_reset_retry_ttl' => 3600, 'password_reset_retry_ttl' => 3600,
'password_reset_token_ttl' => 86400, 'password_reset_token_ttl' => 86400,

View File

@@ -14,6 +14,10 @@
<source>theme.avatar_url</source> <source>theme.avatar_url</source>
<target state="translated">Erlaube die Nutzung von URLs für Avatarbilder</target> <target state="translated">Erlaube die Nutzung von URLs für Avatarbilder</target>
</trans-unit> </trans-unit>
<trans-unit id="uf2_qIW" resname="user_auth_wizard">
<source>user_auth_wizard</source>
<target>Einrichtungsassistent für neue Benutzer anzeigen</target>
</trans-unit>
<trans-unit id="FaGqoTT" resname="timesheet.mode" xml:space="preserve"> <trans-unit id="FaGqoTT" resname="timesheet.mode" xml:space="preserve">
<source>timesheet.mode</source> <source>timesheet.mode</source>
<target state="translated">Zeiterfassungs-Modus</target> <target state="translated">Zeiterfassungs-Modus</target>

View File

@@ -254,6 +254,10 @@
<source>authentication</source> <source>authentication</source>
<target state="translated">Authentifizierung</target> <target state="translated">Authentifizierung</target>
</trans-unit> </trans-unit>
<trans-unit id="uf2_qIW" resname="user_auth_wizard">
<source>user_auth_wizard</source>
<target>Einrichtungsassistent für neue Benutzer anzeigen</target>
</trans-unit>
<trans-unit id="FaGqoTT" resname="timesheet.mode"> <trans-unit id="FaGqoTT" resname="timesheet.mode">
<source>timesheet.mode</source> <source>timesheet.mode</source>
<target state="translated">Zeiterfassungsmodus</target> <target state="translated">Zeiterfassungsmodus</target>

View File

@@ -14,6 +14,10 @@
<source>theme.avatar_url</source> <source>theme.avatar_url</source>
<target>Allow the use of URLs for avatar images</target> <target>Allow the use of URLs for avatar images</target>
</trans-unit> </trans-unit>
<trans-unit id="uf2_qIW" resname="user_auth_wizard">
<source>user_auth_wizard</source>
<target>Show setup wizard for new users</target>
</trans-unit>
<trans-unit id="FaGqoTT" resname="timesheet.mode"> <trans-unit id="FaGqoTT" resname="timesheet.mode">
<source>timesheet.mode</source> <source>timesheet.mode</source>
<target>Timetracking mode</target> <target>Timetracking mode</target>