From 25e3988369051d13adcee60af50ff36b9d553b44 Mon Sep 17 00:00:00 2001 From: tonis Date: Wed, 25 Sep 2024 14:25:25 +0300 Subject: [PATCH 1/4] enh: add option to set user password right after confirmation --- .../Controller/RegistrationController.php | 11 ++++- src/User/Form/RegistrationForm.php | 2 +- src/User/Module.php | 18 ++++++-- src/User/Service/UserRegisterService.php | 2 +- .../resources/views/mail/text/welcome.php | 2 +- src/User/resources/views/mail/welcome.php | 2 +- src/User/resources/views/recovery/reset.php | 3 ++ .../resources/views/registration/register.php | 8 +++- tests/functional.suite.yml | 2 +- tests/functional/RegistrationCest.php | 46 +++++++++++++++++-- 10 files changed, 79 insertions(+), 17 deletions(-) diff --git a/src/User/Controller/RegistrationController.php b/src/User/Controller/RegistrationController.php index aa5f3b8a..ad0c7b5d 100644 --- a/src/User/Controller/RegistrationController.php +++ b/src/User/Controller/RegistrationController.php @@ -15,6 +15,7 @@ use Da\User\Event\SocialNetworkConnectEvent; use Da\User\Event\UserEvent; use Da\User\Factory\MailFactory; +use Da\User\Factory\TokenFactory; use Da\User\Form\RegistrationForm; use Da\User\Form\ResendForm; use Da\User\Helper\SecurityHelper; @@ -23,6 +24,7 @@ use Da\User\Query\SocialNetworkAccountQuery; use Da\User\Query\UserQuery; use Da\User\Service\AccountConfirmationService; +use Da\User\Service\PasswordRecoveryService; use Da\User\Service\ResendConfirmationService; use Da\User\Service\UserConfirmationService; use Da\User\Service\UserCreateService; @@ -223,10 +225,15 @@ public function actionConfirm($id, $code) $this->trigger(UserEvent::EVENT_BEFORE_CONFIRMATION, $event); if ($this->make(AccountConfirmationService::class, [$code, $user, $userConfirmationService])->run()) { - Yii::$app->user->login($user, $this->module->rememberLoginLifespan); Yii::$app->session->setFlash('success', Yii::t('usuario', 'Thank you, registration is now complete.')); - $this->trigger(UserEvent::EVENT_AFTER_CONFIRMATION, $event); + if($this->module->offerPasswordChangeAfterConfirmation) { + $token = TokenFactory::makeRecoveryToken($user->id); + $url = $token->getUrl(); + return $this->redirect($url); + } else { + Yii::$app->user->login($user, $this->module->rememberLoginLifespan); + } } else { Yii::$app->session->setFlash( 'danger', diff --git a/src/User/Form/RegistrationForm.php b/src/User/Form/RegistrationForm.php index b727c4b4..5269238f 100644 --- a/src/User/Form/RegistrationForm.php +++ b/src/User/Form/RegistrationForm.php @@ -73,7 +73,7 @@ public function rules() 'message' => Yii::t('usuario', 'This email address has already been taken'), ], // password rules - 'passwordRequired' => ['password', 'required', 'skipOnEmpty' => $this->module->generatePasswords], + 'passwordRequired' => ['password', 'required', 'skipOnEmpty' => !$this->module->isPasswordRequiredOnRegistration()], 'passwordLength' => ['password', 'string', 'min' => 6, 'max' => 72], 'gdprType' => ['gdpr_consent', 'boolean'], 'gdprDefault' => ['gdpr_consent', 'default', 'value' => 0, 'skipOnEmpty' => false], diff --git a/src/User/Module.php b/src/User/Module.php index 0f70d5e0..54afa828 100755 --- a/src/User/Module.php +++ b/src/User/Module.php @@ -69,7 +69,7 @@ class Module extends BaseModule 'profile.gravatar_email', 'profile.location', 'profile.website', - 'profile.bio' + 'profile.bio', ]; /** * @var string prefix to be used as a replacement when user requests deletion of his data. @@ -91,7 +91,7 @@ class Module extends BaseModule * @see AccessRuleFilter */ public $gdprConsentExcludedUrls = [ - 'user/settings/*' + 'user/settings/*', ]; /** * @var bool whether to enable two factor authentication or not @@ -118,6 +118,10 @@ class Module extends BaseModule * @var bool whether to allow registration process or not */ public $enableRegistration = true; + /** + * @var bool whether user can (re)set password on confirmation. Useful in cases where user is created by admin, and we do not want to e-mail plain text passwords. + */ + public $offerPasswordChangeAfterConfirmation = false; /** * @var bool whether to allow registration process for social network or not */ @@ -227,7 +231,7 @@ class Module extends BaseModule 'confirm//' => 'registration/confirm', 'forgot' => 'recovery/request', 'forgot/' => 'recovery/request', - 'recover//' => 'recovery/reset' + 'recover//' => 'recovery/reset', ]; /** * @var string @@ -332,4 +336,12 @@ public function hasTimeoutSessionHistory() { return $this->timeoutSessionHistory !== false && $this->timeoutSessionHistory > 0; } + + public function isPasswordRequiredOnRegistration() : bool + { + if($this->offerPasswordChangeAfterConfirmation) { + return false; + } + return !$this->generatePasswords; + } } diff --git a/src/User/Service/UserRegisterService.php b/src/User/Service/UserRegisterService.php index a48932e0..9e382141 100644 --- a/src/User/Service/UserRegisterService.php +++ b/src/User/Service/UserRegisterService.php @@ -50,7 +50,7 @@ public function run() try { $model->confirmed_at = $this->getModule()->enableEmailConfirmation ? null : time(); - $model->password = $this->getModule()->generatePasswords + $model->password = ($this->getModule()->generatePasswords or $model->getModule()->offerPasswordChangeAfterConfirmation) ? $this->securityHelper->generatePassword(8, $this->getModule()->minPasswordRequirements) : $model->password; diff --git a/src/User/resources/views/mail/text/welcome.php b/src/User/resources/views/mail/text/welcome.php index 0d4516b3..a6dc30d8 100644 --- a/src/User/resources/views/mail/text/welcome.php +++ b/src/User/resources/views/mail/text/welcome.php @@ -26,7 +26,7 @@ : password ?> -allowPasswordRecovery): ?> +allowPasswordRecovery && !$module->offerPasswordChangeAfterConfirmation): ?> : diff --git a/src/User/resources/views/mail/welcome.php b/src/User/resources/views/mail/welcome.php index 8efa10a9..64d82546 100644 --- a/src/User/resources/views/mail/welcome.php +++ b/src/User/resources/views/mail/welcome.php @@ -29,7 +29,7 @@ generatePasswords): ?> : password) ?> - allowPasswordRecovery): ?> + allowPasswordRecovery && !$module->offerPasswordChangeAfterConfirmation): ?> : diff --git a/src/User/resources/views/recovery/reset.php b/src/User/resources/views/recovery/reset.php index 8f225f1c..7e16157c 100644 --- a/src/User/resources/views/recovery/reset.php +++ b/src/User/resources/views/recovery/reset.php @@ -21,6 +21,9 @@ $this->title = Yii::t('usuario', 'Reset your password'); $this->params['breadcrumbs'][] = $this->title; ?> + +render('/shared/_alert', ['module' => Yii::$app->getModule('user')]) ?> +
diff --git a/src/User/resources/views/registration/register.php b/src/User/resources/views/registration/register.php index 29d1e753..a74f2075 100644 --- a/src/User/resources/views/registration/register.php +++ b/src/User/resources/views/registration/register.php @@ -18,10 +18,14 @@ * @var \Da\User\Model\User $user * @var \Da\User\Module $module */ - +var_dump(!$module->isPasswordRequiredOnRegistration()); $this->title = Yii::t('usuario', 'Sign up'); $this->params['breadcrumbs'][] = $this->title; ?> + +render('/shared/_alert', ['module' => Yii::$app->getModule('user')]) ?> + +
@@ -41,7 +45,7 @@ field($model, 'username') ?> - generatePasswords === false): ?> + isPasswordRequiredOnRegistration()): ?> field($model, 'password')->passwordInput() ?> diff --git a/tests/functional.suite.yml b/tests/functional.suite.yml index 455237e0..86d7da7c 100644 --- a/tests/functional.suite.yml +++ b/tests/functional.suite.yml @@ -1,4 +1,4 @@ -class_name: FunctionalTester +actor: FunctionalTester modules: enabled: - Filesystem diff --git a/tests/functional/RegistrationCest.php b/tests/functional/RegistrationCest.php index d78dcebc..552403fc 100644 --- a/tests/functional/RegistrationCest.php +++ b/tests/functional/RegistrationCest.php @@ -5,13 +5,15 @@ use Da\User\Model\User; use Da\User\Module; use tests\_fixtures\UserFixture; +use tests\_fixtures\TokenFixture; use yii\helpers\Html; class RegistrationCest { public function _before(FunctionalTester $I) { - $I->haveFixtures(['user' => UserFixture::className()]); + $I->haveFixtures(['user' => UserFixture::class]); + $I->haveFixtures(['token' => TokenFixture::class]); } public function _after(FunctionalTester $I) @@ -47,7 +49,7 @@ public function testRegistration(FunctionalTester $I) $this->register($I, 'tester@example.com', 'tester', 'tester'); $I->see('Your account has been created'); - $user = $I->grabRecord(User::className(), ['email' => 'tester@example.com']); + $user = $I->grabRecord(User::class, ['email' => 'tester@example.com']); $I->assertTrue($user->isConfirmed); $I->amOnRoute('/user/security/login'); @@ -84,12 +86,14 @@ public function testRegistrationWithConfirmation(FunctionalTester $I) */ public function testRegistrationWithoutPassword(FunctionalTester $I) { - Yii::$app->getModule('user')->enableEmailConfirmation = false; - Yii::$app->getModule('user')->generatePasswords = true; + /** @var Module $module */ + $module = Yii::$app->getModule('user'); + $module->enableEmailConfirmation = false; + $module->generatePasswords = true; $I->amOnRoute('/user/registration/register'); $this->register($I, 'tester@example.com', 'tester'); $I->see('Your account has been created'); - $user = $I->grabRecord(User::className(), ['email' => 'tester@example.com']); + $user = $I->grabRecord(User::class, ['email' => 'tester@example.com']); $I->assertEquals('tester', $user->username); /** @var \yii\mail\MessageInterface $message */ $message = $I->grabLastSentEmail(); @@ -97,6 +101,38 @@ public function testRegistrationWithoutPassword(FunctionalTester $I) $I->assertStringContainsString('We have generated a password for you', utf8_encode(quoted_printable_decode($message->toString()))); } + /** + * Tests registration when password is generated automatically and sent to user. + * + * @param FunctionalTester $I + */ + public function testRegistrationWithPasswordResetAfterConfirmation(FunctionalTester $I) + { + /** @var Module $module */ + $module = Yii::$app->getModule('user'); + $module->generatePasswords = false; + $module->offerPasswordChangeAfterConfirmation = true; + $I->amOnRoute('/user/registration/register'); + $I->dontSee('Password'); + $this->register($I, 'tester@example.com', 'tester'); + $I->see('Your account has been created'); + /** @var User $user */ + $user = $I->grabRecord(User::class, ['email' => 'tester@example.com']); + $I->assertEquals('tester', $user->username); + /** @var \yii\mail\MessageInterface $message */ + $message = $I->grabLastSentEmail(); + $I->assertArrayHasKey($user->email, $message->getTo()); + $I->assertStringNotContainsString('We have generated a password for you', utf8_encode(quoted_printable_decode($message->toString()))); + /** @var \Da\User\Query\TokenQuery $tokenQuery */ + $tokenQuery = Yii::createObject(\Da\User\Query\TokenQuery::class); + /** @var Token $confirmationToken */ + $confirmationToken = $tokenQuery->whereUserId($user->primaryKey)->one(); + $I->amOnPage($confirmationToken->getUrl()); + $I->see("Thank you, registration is now complete."); + $I->see("Reset your password"); + + } + protected function register(FunctionalTester $I, $email, $username = null, $password = null) { $I->fillField('#registrationform-email', $email); $I->fillField('#registrationform-username', $username); From ad2ae447008c5471cfcca3cd4f195ef8e864ea92 Mon Sep 17 00:00:00 2001 From: tonis Date: Wed, 25 Sep 2024 14:40:52 +0300 Subject: [PATCH 2/4] added chengelog & docs --- CHANGELOG.md | 1 + docs/install/configuration-options.md | 8 ++++++++ tests/functional/RegistrationCest.php | 3 +-- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5733c2e6..d3a9d3d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Fix #546: The profile/show page must not be visible by default, implement configurable policy (TonisOrmisson) - Fix #397: No more fatal Exceptions when connecting to already taken Social Network (edegaudenzi) - Ehh: Added option to pre-fill recovery email via url parameter (TonisOrmisson) +- Ehh: Added option for user to reset password right after confirmation (TonisOrmisson) ## 1.6.3 Mar 18th, 2024 diff --git a/docs/install/configuration-options.md b/docs/install/configuration-options.md index f1ffd672..09fabc2b 100755 --- a/docs/install/configuration-options.md +++ b/docs/install/configuration-options.md @@ -143,6 +143,14 @@ List of urls that does not require explicit data processing consent to be access Setting this attribute allows the registration process. If you set it to `false`, the module won't allow users to register by throwing a `NotFoundHttpException` if the `RegistrationController::actionRegister()` is accessed. +#### offerPasswordChangeAfterConfirmation (type: `boolean`, default: `false`) + +Setting this `true` a user will get redirected to a password reset page right after clicking the confirmation url. +This option is useful if a user is created via admin processes and user should start from setting their password. +In this case system will generate a pseudo password for the user, which is not presented to user. Instead, after +clicking the confirmation link, a password reset token is generated to user by system and user is redirected to the +password reset page with the flash message of successful confirmation. + #### enableSocialNetworkRegistration (type: `boolean`, default: `true`) Setting this attribute allows the registration process via social networks. If you set it to `false`, the module won't allow users to diff --git a/tests/functional/RegistrationCest.php b/tests/functional/RegistrationCest.php index 552403fc..c2f7c4db 100644 --- a/tests/functional/RegistrationCest.php +++ b/tests/functional/RegistrationCest.php @@ -13,7 +13,6 @@ class RegistrationCest public function _before(FunctionalTester $I) { $I->haveFixtures(['user' => UserFixture::class]); - $I->haveFixtures(['token' => TokenFixture::class]); } public function _after(FunctionalTester $I) @@ -80,7 +79,7 @@ public function testRegistrationWithConfirmation(FunctionalTester $I) } /** - * Tests registration when password is generated automatically and sent to user. + * Tests registration when user should set the password right after confirmation * * @param FunctionalTester $I */ From 903e3b2c5edb01a596663874ce98239e0c3d6201 Mon Sep 17 00:00:00 2001 From: tonis Date: Wed, 25 Sep 2024 14:52:21 +0300 Subject: [PATCH 3/4] change wrong test comments --- tests/functional/RegistrationCest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/functional/RegistrationCest.php b/tests/functional/RegistrationCest.php index c2f7c4db..225f2a10 100644 --- a/tests/functional/RegistrationCest.php +++ b/tests/functional/RegistrationCest.php @@ -79,7 +79,7 @@ public function testRegistrationWithConfirmation(FunctionalTester $I) } /** - * Tests registration when user should set the password right after confirmation + * Tests registration when password is generated automatically and sent to user. * * @param FunctionalTester $I */ @@ -101,7 +101,7 @@ public function testRegistrationWithoutPassword(FunctionalTester $I) } /** - * Tests registration when password is generated automatically and sent to user. + * Tests registration when user should set the password right after confirmation * * @param FunctionalTester $I */ From 3b2f7dc46957a9de59989e027bf24f8abe4e715b Mon Sep 17 00:00:00 2001 From: tonis Date: Wed, 25 Sep 2024 15:44:12 +0300 Subject: [PATCH 4/4] remove var_dump() --- src/User/resources/views/registration/register.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/User/resources/views/registration/register.php b/src/User/resources/views/registration/register.php index a74f2075..083d4b44 100644 --- a/src/User/resources/views/registration/register.php +++ b/src/User/resources/views/registration/register.php @@ -18,7 +18,6 @@ * @var \Da\User\Model\User $user * @var \Da\User\Module $module */ -var_dump(!$module->isPasswordRequiredOnRegistration()); $this->title = Yii::t('usuario', 'Sign up'); $this->params['breadcrumbs'][] = $this->title; ?>