Skip to content

Commit

Permalink
Incorporate Change Password
Browse files Browse the repository at this point in the history
Include a change password function in the demo.
  • Loading branch information
rossaddison committed Oct 28, 2023
1 parent a55bb3d commit bdff034
Show file tree
Hide file tree
Showing 9 changed files with 352 additions and 8 deletions.
5 changes: 5 additions & 0 deletions blog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2. After including npm in the environment variable settings in windows, use the command npm i.

### Support the project

[![Open Collective](https://img.shields.io/badge/Open%20Collective-sponsor-7eadf1?logo=open%20collective&logoColor=7eadf1&labelColor=555555)](https://opencollective.com/yiisoft)
Expand Down
8 changes: 8 additions & 0 deletions blog/config/common/routes/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use App\Auth\Controller\AuthController;
use App\Auth\Controller\SignupController;
use App\Auth\Controller\ResetController;
use App\Blog\Archive\ArchiveController;
use App\Blog\BlogController;
use App\Blog\CommentController;
Expand Down Expand Up @@ -61,6 +62,13 @@
) => new LimitRequestsMiddleware(new Counter($storage, 10, 10), $responseFactory))
->action([SignupController::class, 'signup'])
->name('auth/signup'),
Route::methods([Method::GET, Method::POST], '/reset')
->middleware(fn(
ResponseFactoryInterface $responseFactory,
StorageInterface $storage
) => new LimitRequestsMiddleware(new Counter($storage, 10, 10), $responseFactory))
->action([ResetController::class, 'reset'])
->name('auth/reset'),

Group::create('/user')
->routes(
Expand Down
3 changes: 3 additions & 0 deletions blog/resources/messages/en/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
'layout.page.not-found' => 'The page {url} could not be found.',
'layout.pagination-summary' => 'Showing {pageSize} out of {total} posts',
'layout.password-verify' => 'Confirm password',
'layout.password-verify.new' => 'Confirm New Password',
'layout.password.new' => 'New Password',
'layout.password' => 'Password',
'layout.rbac.assign-role' => 'Assign RBAC role to user',
'layout.remember' => 'Remember me',
Expand All @@ -45,6 +47,7 @@
'menu.language' => 'Language',
'menu.login' => 'Login',
'menu.logout' => 'Logout ({login})',
'menu.password.change' => 'Change Password',
'menu.signup' => 'Signup',
'menu.swagger' => 'Swagger',
'menu.users' => 'Users',
Expand Down
5 changes: 5 additions & 0 deletions blog/resources/views/layout/main.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@
'url' => $urlGenerator->generate('auth/signup'),
'visible' => $isGuest,
],
[
'label' => $translator->translate('menu.password.change'),
'url' => $urlGenerator->generate('auth/reset'),
'visible' => !$isGuest,
],
$isGuest ? '' : Form::tag()
->post($urlGenerator->generate('auth/logout'))
->csrf($csrf)
Expand Down
53 changes: 53 additions & 0 deletions blog/resources/views/reset/reset.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

use App\Auth\Form\ResetForm;
use Yiisoft\Form\Field;
use Yiisoft\Html\Html;
use Yiisoft\Html\Tag\Form;
use Yiisoft\Router\UrlGeneratorInterface;
use Yiisoft\Translator\TranslatorInterface;
use Yiisoft\View\WebView;

/**
* @var WebView $this
* @var TranslatorInterface $translator
* @var UrlGeneratorInterface $urlGenerator
* @var string $csrf
* @var ResetForm $formModel
*/
$this->setTitle($translator->translate('layout.reset'));
?>

<div class="container py-5 h-100">
<div class="row d-flex justify-content-center align-items-center h-100">
<div class="col-12 col-md-8 col-lg-6 col-xl-5">
<div class="card border border-dark shadow-2-strong rounded-3">
<div class="card-header bg-dark text-white">
<h1 class="fw-normal h3 text-center"><?= Html::encode($this->getTitle()) ?></h1>
</div>
<div class="card-body p-5 text-center">
<?= Form::tag()
// note: the reset function actually appears in the ResetController
->post($urlGenerator->generate('auth/reset'))
->csrf($csrf)
->id('resetForm')
->open() ?>

<?= Field::text($formModel, 'login')->addInputAttributes(['value'=> $login ?? '', 'readonly'=>'readonly']) ?>
<?= Field::password($formModel, 'password') ?>
<?= Field::password($formModel, 'password_verify') ?>
<?= Field::password($formModel, 'new_password') ?>
<?= Field::password($formModel, 'new_password_verify') ?>
<?= Field::submitButton()
->buttonId('reset-button')
->name('reset-button')
->content($translator->translate('layout.submit'))
?>
<?= Form::tag()->close() ?>
</div>
</div>
</div>
</div>
</div>
78 changes: 78 additions & 0 deletions blog/src/Auth/Controller/ResetController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?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\ResetForm;
use App\Service\WebControllerService;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Yiisoft\Form\FormHydrator;
use Yiisoft\Http\Method;
use Yiisoft\Translator\TranslatorInterface as Translator;
use Yiisoft\User\CurrentUser;
use Yiisoft\Yii\View\ViewRenderer;

final class ResetController
{
public function __construct(
private Translator $translator,
private CurrentUser $current_user,
private WebControllerService $webService,
private ViewRenderer $viewRenderer,
)
{
$this->current_user = $current_user;
$this->translator = $translator;
$this->viewRenderer = $viewRenderer->withControllerName('reset');
}

public function reset(
AuthService $authService,
Identity $identity,
IdentityRepository $identityRepository,
ServerRequestInterface $request,
FormHydrator $formHydrator,
ResetForm $resetForm
): ResponseInterface {
// permit an authenticated user with permission editPost (ie. not a guest) only and null!== current user
if (!$authService->isGuest()) {
// see demo/blog/resources/rbac
if ($this->current_user->can('editPost',[])) {
// readonly the login detail on the reset form
$identity_id = $this->current_user->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($resetForm, $request->getParsedBody())
&& $resetForm->reset()
) {
// 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();
return $this->redirectToMain();
}
return $this->viewRenderer->render('reset', ['formModel' => $resetForm, 'login' => $login]);
} // identity
} // identity_id
} // current user
} // auth service
return $this->redirectToMain();
} // reset

private function redirectToMain(): ResponseInterface
{
return $this->webService->getRedirectResponse('site/index');
}
}
175 changes: 175 additions & 0 deletions blog/src/Auth/Form/ResetForm.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
<?php

declare(strict_types=1);

namespace App\Auth\Form;

use App\User\UserRepository;
use App\Auth\AuthService;
use Yiisoft\Form\FormModel;
use Yiisoft\Translator\TranslatorInterface;
use Yiisoft\Validator\Result;
use Yiisoft\Validator\Rule\Callback;
use Yiisoft\Validator\Rule\Length;
use Yiisoft\Validator\Rule\Required;
use Yiisoft\Validator\ValidatorInterface;
use Yiisoft\Validator\RulesProviderInterface;

final class ResetForm extends FormModel implements RulesProviderInterface
{
private string $login = '';
private string $password = '';
private string $password_verify = '';
private string $new_password = '';
private string $new_password_verify = '';

public function __construct(
private AuthService $authService,
private ValidatorInterface $validator,
private TranslatorInterface $translator,
private UserRepository $userRepository,
) {
}

public function reset(): bool
{
if ($this->validator->validate($this)->isValid()) {
$user = $this->userRepository->findByLogin($this->getLogin());
if (null!==$user) {
$user->setPassword($this->getNewPassword());
// The cookie identity auth_key is regenerated on logout
// Refer to ResetController reset function
$this->userRepository->save($user);
return true;
}
}
return false;
}

/**
* @return string[]
*
* @psalm-return array{login: string, password: string, password_verify: string, new_password: string, new_password_verify: string}
*/
public function getAttributeLabels(): array
{
return [
'login' => $this->translator->translate('layout.login'),
'password' => $this->translator->translate('layout.password'),
'password_verify' => $this->translator->translate('layout.password-verify'),
'new_password' => $this->translator->translate('layout.password.new'),
'new_password_verify' => $this->translator->translate('layout.password-verify.new'),
];
}

/**
* @return string
*
* @psalm-return 'Reset'
*/
public function getFormName(): string
{
return 'Reset';
}

public function getLogin(): string
{
return $this->login;
}

public function getPassword(): string
{
return $this->password;
}

public function getPasswordVerify(): string
{
return $this->password_verify;
}

public function getNewPassword(): string
{
return $this->new_password;
}

public function getNewPasswordVerify() : string
{
return $this->new_password_verify;
}

public function getRules(): array
{
return [
'login' => [
new Required(),
new Length(min: 1, max: 48, skipOnError: true),
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;
},
],
'password' => $this->PasswordRules(),
'password_verify' => $this->PasswordVerifyRules(),
'new_password' => [
new Required(),
new Length(min: 8),
],
'new_password_verify' => $this->NewPasswordVerifyRules()
];
}

private function PasswordRules(): array
{
return [
new Required(),
new Callback(
callback: function (): Result {
$result = new Result();
if (!$this->authService->login($this->login, $this->password)) {
$result->addError($this->translator->translate('validator.invalid.login.password'));
}

return $result;
},
skipOnEmpty: true,
),
];
}

private function PasswordVerifyRules(): array
{
return [
new Required(),
new Callback(
callback: function (): Result {
$result = new Result();
if (!($this->password === $this->password_verify)) {
$result->addError($this->translator->translate('validator.password.not.match'));
}
return $result;
},
skipOnEmpty: true,
),
];
}

private function NewPasswordVerifyRules(): array
{
return [
new Required(),
new Callback(
callback: function (): Result {
$result = new Result();
if (!($this->new_password === $this->new_password_verify)) {
$result->addError($this->translator->translate('validator.password.not.match.new'));
}
return $result;
},
skipOnEmpty: true,
),
];
}
}
Loading

0 comments on commit bdff034

Please sign in to comment.