-
-
Notifications
You must be signed in to change notification settings - Fork 133
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
Change Password #602
Change Password #602
Conversation
rossaddison
commented
Oct 28, 2023
Q | A |
---|---|
Is bugfix? | ❌ |
New feature? | ✔️ |
Breaks BC? | ❌ |
Fixed issues | None |
PR Summary
|
blog/README.md
Outdated
@@ -67,6 +67,11 @@ The code is statically analyzed with [Psalm](https://psalm.dev/). To run static | |||
./vendor/bin/psalm | |||
``` | |||
|
|||
### Installing Bootstrap Node Modules with WAMP | |||
|
|||
1. Make a directory node_modules in the blog directory. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't npm
creating node_modules
automatically?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Functionally a change password and not a reset password form that is included in the main layout and not on the login form. (Change Password)[yiisoft#602]
Adopt suggestions in (#602)[yiisoft/demo#602]
Please review these changes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added several comments, fix several errors and adapt PR to last changes in Yii Form.
{ | ||
private string $login = ''; | ||
private string $password = ''; | ||
private string $passwordVerify = ''; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private string $passwordVerify = ''; | |
private string $passwordVerify = ''; |
Not need field for duplicate old password.
Removved passwordVerify field in the form
@rossaddison would you please resolve conflicts? Thanks. |
return $this->redirectToMain(); | ||
} | ||
// permit an authenticated user, ie. not a guest, only and null!== current user | ||
if (!$authService->isGuest()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (!$authService->isGuest()) {
already not need, we have this check above.
In the same way, you can remove "if" nesting in further code.
->id('changeForm') | ||
->open() ?> | ||
|
||
<?= Field::text($formModel, 'login')->addInputAttributes(['value'=> $login ?? '', 'readonly'=>'readonly']) ?> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you want add ability to change password for any user? Not only current?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. I will need to add a permission to the readonly => readonly e.g.
!$this->currentUser->can('changePasswordForAnyUser',[]) ? 'readonly'=>'readonly' : '';
This will make sure that only administrators with the canChangePasswordForAnyUser permission can insert a username in the login textbox in place of the {login} in order to change the password.
...And remove the
if ($this->currentUser->can('viewInv',[])) {
since all authenticated users with a lesser permission (of viewInv) should be able to change their password.
changepassword.php
<?= $canChangePasswordForAnyUser ? Field::text($formModel, 'login')->addInputAttributes(['value'=> $login ?? ''])
: Field::text($formModel, 'login')->addInputAttributes(['value'=> $login ?? '', 'readonly'=>'readonly']); ?>
Controller.php
public function change(
AuthService $authService,
Identity $identity,
IdentityRepository $identityRepository,
ServerRequestInterface $request,
FormHydrator $formHydrator,
ChangePasswordForm $changePasswordForm
): ResponseInterface {
if ($authService->isGuest()) {
return $this->redirectToMain();
}
// readonly the login detail on the change form
$identity_id = $this->currentUser->getIdentity()->getId();
if (null!==$identity_id) {
$identity = $identityRepository->findIdentity($identity_id);
if (null!==$identity) {
// Identity and User are in a HasOne relationship so no null value
$login = $identity->getUser()?->getLogin();
if ($request->getMethod() === Method::POST
&& $formHydrator->populate($changePasswordForm, $request->getParsedBody())
&& $changePasswordForm->change()
) {
// Identity implements CookieLoginIdentityInterface: ensure the regeneration of the cookie auth key by means of $authService->logout();
// @see vendor\yiisoft\user\src\Login\Cookie\CookieLoginIdentityInterface
// Specific note: "Make sure to invalidate earlier issued keys when you implement force user logout,
// PASSWORD CHANGE and other scenarios, that require forceful access revocation for old sessions.
// The authService logout function will regenerate the auth key here => overwriting any auth key
$authService->logout();
$this->flash_message('success', $this->translator->translate('validator.password.change'));
return $this->redirectToMain();
}
return $this->viewRenderer->render('change',
[
'formModel' => $changePasswordForm,
'login' => $login,
'canChangePasswordForAnyUser' => $this->currentUser->can('changePasswordForAnyUser')
]);
} // identity
} // identity_id
} // r
} | ||
// permit an authenticated user, ie. not a guest, only and null!== current user | ||
if (!$authService->isGuest()) { | ||
if ($this->current_user->can('viewInv',[])) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
viewInv
isn't a good name for permission. Let's not shorten names.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok. Any suggestions?
If a user (normally an administrator) has the permission 'changePasswordForAnyUser', the login form 'username' text box will be not readonly so that the administrator can change any username's password in the event of a user's personal request to change their password by typing in their username.
blog/composer.json
Outdated
"rossaddison/mailer": "dev-master", | ||
"rossaddison/mailer-symfony": "*", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rossaddison you can link your packages locally with yiisoft/yii-dev-tool
Take a look at it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could not get the yiisoft/mailer to function at the time due to a compatability issue with yiisoft/view i think it was at the time. So used my temporary forks instead. I do not believe the blog/composer.json, which my invoice/composer.json is based on can be compiled at the moment with composer although I have managed to keep mine going with quite a few tweaks. I am definitely going to try and get the yiisoft/yii-dev-tool integrated.
blog/composer.json
Outdated
"rossaddison/yii-swagger": "dev-master", | ||
"rossaddison/yii-view": "*" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was a composer setup issue. Ignore.
blog/config/common/di/rbac.php
Outdated
* @var array $params['yiisoft/aliases'] | ||
* @var array $params['yiisoft/aliases']['aliases'] | ||
* @var string $params['yiisoft/aliases']['aliases']['@root'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does that work in PhpStorm? Elsewhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just on wampserver at the moment now with windows 11. Have not tested on phpStorm yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean that is IDE annotation and, as far as I know, no IDE supports this style.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The following code gives no psalm level 1 errors and is reasonably explicit.
<?php
declare(strict_types=1);
use Yiisoft\Access\AccessCheckerInterface;
use Yiisoft\Rbac\AssignmentsStorageInterface;
use Yiisoft\Rbac\ItemsStorageInterface;
use Yiisoft\Rbac\Manager;
use Yiisoft\Rbac\Php\AssignmentsStorage;
use Yiisoft\Rbac\Php\ItemsStorage;
/**
* @see $params['yiisoft/aliases']['aliases']['@root']
* @var array $params
* @var string $root
*/
$root = array_search('@root', $params);
return [
ItemsStorageInterface::class => [
'class' => ItemsStorage::class,
'__construct()' => [
'directory' => $root . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'rbac',
],
],
AssignmentsStorageInterface::class => [
'class' => AssignmentsStorage::class,
'__construct()' => [
'directory' => $root . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'rbac',
],
],
AccessCheckerInterface::class => Manager::class,
];
blog/config/common/di/router.php
Outdated
'reset' => function (array $defaultArguments = []) { | ||
$defaultArguments = ['_language', 'en']; | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks suspicious. What does it reset?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Psalm level 1 testing no longer has an issue with $defaultArguments. I have removed array $defaultArguments = [] initialization and it works fine as before. I will remove it also from rossaddison/invoice. I cannot remember the exact error unfortunately.
A question from my side is where does the parameter default_arguments get assigned outside of the function since it is prefixed with a $this. i.e
'reset' => function () {
$this->defaultArguments = ['_language', 'en'];
},
This is why Psalm had a problem with the current setting. There is no such parameter coming from anywhere else with a type associated with it unless it has been recently setup in a vendor/config and this is why I decided to initialize it as a function parameter since I could not find an occurance elsewhere.
blog/config/common/di/router.php
Outdated
->addGroup( | ||
Group::create('/{_language}')->routes(...$config->get('app-routes')), | ||
) | ||
->addGroup( | ||
Group::create()->routes(...$config->get('routes')), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was that created to exclude debug routes from language routing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
key 'app-routes' exists in the configuration.php file whereas key 'routes' does not. Just thought routes was a better naming convention for the ['common/routes/asterisk..php']. My configuration file key 'routes' in rossaddison/invoice {root}configuration.php points to ['common/routes/*.php' as well but I do not have an app-routes key instead I have 'routes'
]. So the package still picks up all php files in the routes folder for merging. So suggest condensing the two into one.
Where in the demo would I get('routes')? Then I could possibly interchange them.
$this->currentUser = $currentUser; | ||
$this->session = $session; | ||
$this->flash = new Flash($session); | ||
$this->translator = $translator; | ||
$this->viewRenderer = $viewRenderer->withControllerName('changepassword'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$this->currentUser = $currentUser; | |
$this->session = $session; | |
$this->flash = new Flash($session); | |
$this->translator = $translator; | |
$this->viewRenderer = $viewRenderer->withControllerName('changepassword'); | |
$this->flash = new Flash($session); | |
$this->viewRenderer = $viewRenderer->withControllerName('changepassword'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Get the following Psalm Level 1 errors: (8 in total)
with the following code:
<?php
declare(strict_types=1);
namespace App\Auth\Controller;
use App\Auth\AuthService;
use App\Auth\Identity;
use App\Auth\IdentityRepository;
use App\Auth\Form\ChangePasswordForm;
use App\Service\WebControllerService;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Yiisoft\FormModel\FormHydrator;
use Yiisoft\Http\Method;
use Yiisoft\Session\SessionInterface as Session;
use Yiisoft\Session\Flash\Flash;
use Yiisoft\Translator\TranslatorInterface as Translator;
use Yiisoft\User\CurrentUser;
use Yiisoft\Yii\View\Renderer\ViewRenderer;
final class ChangePasswordController
{
public function __construct(
private Flash $flash,
private Translator $translator,
private WebControllerService $webService,
private ViewRenderer $viewRenderer,
)
{
$this->flash = new Flash($session);
$this->translator = $translator;
$this->viewRenderer = $viewRenderer->withControllerName('changepassword');
}
public function change(
AuthService $authService,
Identity $identity,
IdentityRepository $identityRepository,
ServerRequestInterface $request,
FormHydrator $formHydrator,
ChangePasswordForm $changePasswordForm
): ResponseInterface {
if ($authService->isGuest()) {
return $this->redirectToMain();
}
$identityId = $this->currentUser->getIdentity()->getId();
if (null!==$identityId) {
$identity = $identityRepository->findIdentity($identityId);
if (null!==$identity) {
// Identity and User are in a HasOne relationship so no null value
$login = $identity->getUser()?->getLogin();
if ($request->getMethod() === Method::POST
&& $formHydrator->populate($changePasswordForm, $request->getParsedBody())
&& $changePasswordForm->change()
) {
// Identity implements CookieLoginIdentityInterface: ensure the regeneration of the cookie auth key by means of $authService->logout();
// @see vendor\yiisoft\user\src\Login\Cookie\CookieLoginIdentityInterface
// Specific note: "Make sure to invalidate earlier issued keys when you implement force user logout,
// PASSWORD CHANGE and other scenarios, that require forceful access revocation for old sessions.
// The authService logout function will regenerate the auth key here => overwriting any auth key
$authService->logout();
$this->flash_message('success', $this->translator->translate('validator.password.change'));
return $this->redirectToMain();
}
return $this->viewRenderer->render('change', [
'formModel' => $changePasswordForm,
'login' => $login,
/**
* @see resources\rbac\items.php
* @see https://github.com/yiisoft/demo/pull/602
*/
'changePasswordForAnyUser' => $this->currentUser->can('changePasswordForAnyUser')
]);
} // identity
} // identityId
return $this->redirectToMain();
} // reset
/**
* @param string $level
* @param string $message
* @return Flash|null
*/
private function flash_message(string $level, string $message): Flash|null {
if (strlen($message) > 0) {
$this->flash->add($level, $message, true);
return $this->flash;
}
return null;
}
private function redirectToMain(): ResponseInterface
{
return $this->webService->getRedirectResponse('site/index');
}
}
And original code:
<?php
declare(strict_types=1);
namespace App\Auth\Controller;
use App\Auth\AuthService;
use App\Auth\Identity;
use App\Auth\IdentityRepository;
use App\Auth\Form\ChangePasswordForm;
use App\Service\WebControllerService;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Yiisoft\FormModel\FormHydrator;
use Yiisoft\Http\Method;
use Yiisoft\Session\SessionInterface as Session;
use Yiisoft\Session\Flash\Flash;
use Yiisoft\Translator\TranslatorInterface as Translator;
use Yiisoft\User\CurrentUser;
use Yiisoft\Yii\View\Renderer\ViewRenderer;
final class ChangePasswordController
{
public function __construct(
private Session $session,
private Flash $flash,
private Translator $translator,
private CurrentUser $currentUser,
private WebControllerService $webService,
private ViewRenderer $viewRenderer,
)
{
$this->currentUser = $currentUser;
$this->session = $session;
$this->flash = new Flash($session);
$this->translator = $translator;
$this->viewRenderer = $viewRenderer->withControllerName('changepassword');
}
public function change(
AuthService $authService,
Identity $identity,
IdentityRepository $identityRepository,
ServerRequestInterface $request,
FormHydrator $formHydrator,
ChangePasswordForm $changePasswordForm
): ResponseInterface {
if ($authService->isGuest()) {
return $this->redirectToMain();
}
$identityId = $this->currentUser->getIdentity()->getId();
if (null!==$identityId) {
$identity = $identityRepository->findIdentity($identityId);
if (null!==$identity) {
// Identity and User are in a HasOne relationship so no null value
$login = $identity->getUser()?->getLogin();
if ($request->getMethod() === Method::POST
&& $formHydrator->populate($changePasswordForm, $request->getParsedBody())
&& $changePasswordForm->change()
) {
// Identity implements CookieLoginIdentityInterface: ensure the regeneration of the cookie auth key by means of $authService->logout();
// @see vendor\yiisoft\user\src\Login\Cookie\CookieLoginIdentityInterface
// Specific note: "Make sure to invalidate earlier issued keys when you implement force user logout,
// PASSWORD CHANGE and other scenarios, that require forceful access revocation for old sessions.
// The authService logout function will regenerate the auth key here => overwriting any auth key
$authService->logout();
$this->flash_message('success', $this->translator->translate('validator.password.change'));
return $this->redirectToMain();
}
return $this->viewRenderer->render('change', [
'formModel' => $changePasswordForm,
'login' => $login,
/**
* @see resources\rbac\items.php
* @see https://github.com/yiisoft/demo/pull/602
*/
'changePasswordForAnyUser' => $this->currentUser->can('changePasswordForAnyUser')
]);
} // identity
} // identityId
return $this->redirectToMain();
} // reset
/**
* @param string $level
* @param string $message
* @return Flash|null
*/
private function flash_message(string $level, string $message): Flash|null {
if (strlen($message) > 0) {
$this->flash->add($level, $message, true);
return $this->flash;
}
return null;
}
private function redirectToMain(): ResponseInterface
{
return $this->webService->getRedirectResponse('site/index');
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the code as it is I am getting no Psalm Level 1 errors.
rossaddison/invoice has the following psalm.xml file which incorporates the following:
<plugins><pluginClass class="Psalm\SymfonyPsalmPlugin\Plugin"/></plugins>
<projectFiles>
<directory name="config" />
<directory name="resources/views" />
<directory name="src" />
<file name="public/index.php"/>
<file name="yii"/>
<file name="autoload.php"/>
<ignoreFiles>
<directory name="vendor/yiisoft/requirements/src" />
</ignoreFiles>
</projectFiles>
</psalm>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The config directory is included.
function (mixed $value): Result { | ||
$result = new Result(); | ||
if ($this->userRepository->findByLogin((string)$value) == null) { | ||
$result->addError($this->translator->translate('validator.user.exist.not')); | ||
} | ||
return $result; | ||
}, | ||
], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally, that should be implemented by catching a database exception on inserting a record because SELECT
and then INSERT
aren't atomic and, thus, can cause an exception anyway if a value is inserted by another user in between.
'password' => $this->PasswordRules(), | ||
'newPassword' => [ | ||
new Required(), | ||
new Length(min: 8), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd not limit password length because it weakens security overall.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok.
Apply: 1. Password Length should not be limited 2. Avoid Non Atomical ....$this->userRepository->findByLogin((string)$value...derived Rule with user not exist message. @see yiisoft/demo#602 The login field is derived from the $login value from the ChangePasswordController and is read only.
I have created a fork of yiisoft/demo at rossaddison/demo branch change_password and will present my changes more incrementally. |