Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added option offerPasswordChangeAfterConfirmation #565

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 8 additions & 0 deletions docs/install/configuration-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions src/User/Controller/RegistrationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion src/User/Form/RegistrationForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
18 changes: 15 additions & 3 deletions src/User/Module.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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
*/
Expand Down Expand Up @@ -227,7 +231,7 @@ class Module extends BaseModule
'confirm/<id:\d+>/<code:[A-Za-z0-9_-]+>' => 'registration/confirm',
'forgot' => 'recovery/request',
'forgot/<email:[a-zA-Z0-9_.±]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+>' => 'recovery/request',
'recover/<id:\d+>/<code:[A-Za-z0-9_-]+>' => 'recovery/reset'
'recover/<id:\d+>/<code:[A-Za-z0-9_-]+>' => 'recovery/reset',
];
/**
* @var string
Expand Down Expand Up @@ -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;
}
}
2 changes: 1 addition & 1 deletion src/User/Service/UserRegisterService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion src/User/resources/views/mail/text/welcome.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
<?= Yii::t('usuario', 'We have generated a password for you') ?>:
<?= $user->password ?>
<?php endif ?>
<?php if ($module->allowPasswordRecovery): ?>
<?php if ($module->allowPasswordRecovery && !$module->offerPasswordChangeAfterConfirmation): ?>
<?= Yii::t('usuario', 'If you haven\'t received a password, you can reset it at') ?>:
<?= Url::to(['/user/recovery/request'], true) ?>
<?php endif ?>
Expand Down
2 changes: 1 addition & 1 deletion src/User/resources/views/mail/welcome.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
<?php if ($showPassword || $module->generatePasswords): ?>
<?= Yii::t('usuario', 'We have generated a password for you') ?>: <strong><?= Html::encode($user->password) ?></strong>
<?php endif ?>
<?php if ($module->allowPasswordRecovery): ?>
<?php if ($module->allowPasswordRecovery && !$module->offerPasswordChangeAfterConfirmation): ?>
<?= Yii::t('usuario', 'If you haven\'t received a password, you can reset it at') ?>: <strong><?= Html::a(Html::encode(Url::to(['/user/recovery/request'], true)), Url::to(['/user/recovery/request'], true)) ?></strong>
<?php endif ?>

Expand Down
3 changes: 3 additions & 0 deletions src/User/resources/views/recovery/reset.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
$this->title = Yii::t('usuario', 'Reset your password');
$this->params['breadcrumbs'][] = $this->title;
?>

<?= $this->render('/shared/_alert', ['module' => Yii::$app->getModule('user')]) ?>

<div class="row">
<div class="col-md-4 col-md-offset-4 col-sm-6 col-sm-offset-3">
<div class="panel panel-default">
Expand Down
7 changes: 5 additions & 2 deletions src/User/resources/views/registration/register.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@
* @var \Da\User\Model\User $user
* @var \Da\User\Module $module
*/

$this->title = Yii::t('usuario', 'Sign up');
$this->params['breadcrumbs'][] = $this->title;
?>

<?= $this->render('/shared/_alert', ['module' => Yii::$app->getModule('user')]) ?>


<div class="row">
<div class="col-md-4 col-md-offset-4 col-sm-6 col-sm-offset-3">
<div class="panel panel-default">
Expand All @@ -41,7 +44,7 @@

<?= $form->field($model, 'username') ?>

<?php if ($module->generatePasswords === false): ?>
<?php if ($module->isPasswordRequiredOnRegistration()): ?>
<?= $form->field($model, 'password')->passwordInput() ?>
<?php endif ?>

Expand Down
2 changes: 1 addition & 1 deletion tests/functional.suite.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
class_name: FunctionalTester
actor: FunctionalTester
modules:
enabled:
- Filesystem
Expand Down
45 changes: 40 additions & 5 deletions tests/functional/RegistrationCest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
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]);
}

public function _after(FunctionalTester $I)
Expand Down Expand Up @@ -47,7 +48,7 @@ public function testRegistration(FunctionalTester $I)

$this->register($I, '[email protected]', 'tester', 'tester');
$I->see('Your account has been created');
$user = $I->grabRecord(User::className(), ['email' => '[email protected]']);
$user = $I->grabRecord(User::class, ['email' => '[email protected]']);
$I->assertTrue($user->isConfirmed);

$I->amOnRoute('/user/security/login');
Expand Down Expand Up @@ -84,19 +85,53 @@ 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, '[email protected]', 'tester');
$I->see('Your account has been created');
$user = $I->grabRecord(User::className(), ['email' => '[email protected]']);
$user = $I->grabRecord(User::class, ['email' => '[email protected]']);
$I->assertEquals('tester', $user->username);
/** @var \yii\mail\MessageInterface $message */
$message = $I->grabLastSentEmail();
$I->assertArrayHasKey($user->email, $message->getTo());
$I->assertStringContainsString('We have generated a password for you', utf8_encode(quoted_printable_decode($message->toString())));
}

/**
* Tests registration when user should set the password right after confirmation
*
* @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, '[email protected]', 'tester');
$I->see('Your account has been created');
/** @var User $user */
$user = $I->grabRecord(User::class, ['email' => '[email protected]']);
$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);
Expand Down
Loading