diff --git a/src/Configuration/SystemConfiguration.php b/src/Configuration/SystemConfiguration.php
index ed595cd3..2a94ee93 100644
--- a/src/Configuration/SystemConfiguration.php
+++ b/src/Configuration/SystemConfiguration.php
@@ -370,6 +370,11 @@ final class SystemConfiguration
return $this->getDefaultCurrency();
}
+ public function isUserWizardActive(): bool
+ {
+ return (bool) $this->find('user.wizard');
+ }
+
// ========== Timesheet configurations ==========
/*
public function getTimesheetBreakWarningDuration(): int
diff --git a/src/Controller/SystemConfigurationController.php b/src/Controller/SystemConfigurationController.php
index 785509b4..b4e20b4f 100644
--- a/src/Controller/SystemConfigurationController.php
+++ b/src/Controller/SystemConfigurationController.php
@@ -546,6 +546,10 @@ final class SystemConfigurationController extends AbstractController
->setLabel('theme.avatar_url')
->setType(YesNoType::class)
->setTranslationDomain('system-configuration'),
+ (new Configuration('user.wizard'))
+ ->setLabel('user_auth_wizard')
+ ->setType(YesNoType::class)
+ ->setTranslationDomain('system-configuration'),
]),
(new SystemConfigurationModel('theme'))
->setConfiguration([
diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php
index 338f45b4..d3778c3e 100644
--- a/src/DependencyInjection/Configuration.php
+++ b/src/DependencyInjection/Configuration.php
@@ -568,6 +568,9 @@ final class Configuration implements ConfigurationInterface
->booleanNode('registration')
->defaultFalse()
->end()
+ ->booleanNode('wizard')
+ ->defaultTrue()
+ ->end()
->booleanNode('password_reset')
->defaultTrue()
->end()
diff --git a/src/EventSubscriber/WizardSubscriber.php b/src/EventSubscriber/WizardSubscriber.php
index a92b1484..6802d78f 100644
--- a/src/EventSubscriber/WizardSubscriber.php
+++ b/src/EventSubscriber/WizardSubscriber.php
@@ -9,6 +9,7 @@
namespace App\EventSubscriber;
+use App\Configuration\SystemConfiguration;
use App\Entity\User;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
@@ -23,7 +24,8 @@ class WizardSubscriber implements EventSubscriberInterface
public function __construct(
private UrlGeneratorInterface $urlGenerator,
private AuthorizationCheckerInterface $security,
- private TokenStorageInterface $storage
+ private TokenStorageInterface $storage,
+ private SystemConfiguration $systemConfiguration
) {
}
@@ -63,6 +65,15 @@ class WizardSubscriber implements EventSubscriberInterface
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) {
if (!$user->hasSeenWizard($wizard)) {
$response = new RedirectResponse($this->urlGenerator->generate('wizard', ['wizard' => $wizard]));
@@ -71,10 +82,5 @@ class WizardSubscriber implements EventSubscriberInterface
return;
}
}
-
- if ($user->requiresPasswordReset()) {
- $response = new RedirectResponse($this->urlGenerator->generate('wizard', ['wizard' => 'password']));
- $event->setResponse($response);
- }
}
}
diff --git a/tests/Configuration/SystemConfigurationTest.php b/tests/Configuration/SystemConfigurationTest.php
index 5463e3c2..f22cc337 100644
--- a/tests/Configuration/SystemConfigurationTest.php
+++ b/tests/Configuration/SystemConfigurationTest.php
@@ -105,6 +105,9 @@ class SystemConfigurationTest extends TestCase
'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.default_begin')->setValue('07:00'),
(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());
}
+ 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
{
$sut = $this->getSut($this->getDefaultSettings(), []);
diff --git a/tests/Controller/WizardControllerTest.php b/tests/Controller/WizardControllerTest.php
index 34d9ad83..089a114c 100644
--- a/tests/Controller/WizardControllerTest.php
+++ b/tests/Controller/WizardControllerTest.php
@@ -9,7 +9,9 @@
namespace App\Tests\Controller;
+use App\DataFixtures\UserFixtures;
use App\Entity\User;
+use App\Entity\UserPreference;
use PHPUnit\Framework\Attributes\Group;
#[Group('integration')]
@@ -37,6 +39,97 @@ class WizardControllerTest extends AbstractControllerBaseTestCase
$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
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php
index 138a39cc..836a0fb0 100644
--- a/tests/DependencyInjection/ConfigurationTest.php
+++ b/tests/DependencyInjection/ConfigurationTest.php
@@ -412,6 +412,7 @@ class ConfigurationTest extends TestCase
'user' => [
'registration' => false,
'password_reset' => true,
+ 'wizard' => true,
'login' => true,
'password_reset_retry_ttl' => 3600,
'password_reset_token_ttl' => 86400,
diff --git a/translations/system-configuration.de.xlf b/translations/system-configuration.de.xlf
index 01325cd5..e30c531a 100644
--- a/translations/system-configuration.de.xlf
+++ b/translations/system-configuration.de.xlf
@@ -14,6 +14,10 @@
theme.avatar_url
Erlaube die Nutzung von URLs für Avatarbilder
+
+ user_auth_wizard
+ Einrichtungsassistent für neue Benutzer anzeigen
+
timesheet.mode
Zeiterfassungs-Modus
diff --git a/translations/system-configuration.de_CH.xlf b/translations/system-configuration.de_CH.xlf
index 2d5dad80..a85ab10f 100644
--- a/translations/system-configuration.de_CH.xlf
+++ b/translations/system-configuration.de_CH.xlf
@@ -254,6 +254,10 @@
authentication
Authentifizierung
+
+ user_auth_wizard
+ Einrichtungsassistent für neue Benutzer anzeigen
+
timesheet.mode
Zeiterfassungsmodus
diff --git a/translations/system-configuration.en.xlf b/translations/system-configuration.en.xlf
index d4c43093..e806a094 100644
--- a/translations/system-configuration.en.xlf
+++ b/translations/system-configuration.en.xlf
@@ -14,6 +14,10 @@
theme.avatar_url
Allow the use of URLs for avatar images
+
+ user_auth_wizard
+ Show setup wizard for new users
+
timesheet.mode
Timetracking mode