From 2a4da87fd5dbe97594e788508dc3aab7293cec10 Mon Sep 17 00:00:00 2001 From: ErickSkrauch Date: Thu, 12 May 2016 01:13:19 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B0=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0=20?= =?UTF-8?q?=D0=B2=D0=BE=D1=81=D1=81=D1=82=D0=B0=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D0=B0=D1=80=D0=BE=D0=BB=D1=8F?= =?UTF-8?q?=20=D0=BE=D1=82=20=D0=B0=D0=BA=D0=BA=D0=B0=D1=83=D0=BD=D1=82?= =?UTF-8?q?=D0=B0=20=D0=9B=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B8=20=D0=BF=D0=B0=D1=80=D0=BE?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=B2=D1=8B=D0=BD=D0=B5=D1=81=D0=B5=D0=BD=D0=B0?= =?UTF-8?q?=20=D0=B2=20=D0=BE=D1=82=D0=B4=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B?= =?UTF-8?q?=D0=B9=20=D0=B2=D0=B0=D0=BB=D0=B8=D0=B4=D0=B0=D1=82=D0=BE=D1=80?= =?UTF-8?q?=20=D0=92=20composer.json=20=D0=B4=D0=BE=D0=BA=D0=B8=D0=BD?= =?UTF-8?q?=D1=83=D1=82=D0=B0=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D0=BC?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=8C=20=D0=BE=D1=82=20php7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/controllers/AuthenticationController.php | 22 +++++- api/models/ChangePasswordForm.php | 7 +- api/models/RecoverPasswordForm.php | 71 +++++++++++++++++++ api/models/RegistrationForm.php | 3 +- common/validators/PasswordValidate.php | 15 ++++ composer.json | 2 +- .../api/_pages/AuthenticationRoute.php | 9 +++ .../api/functional/RecoverPasswordCest.php | 36 ++++++++++ .../unit/models/RecoverPasswordFormTest.php | 44 ++++++++++++ 9 files changed, 201 insertions(+), 8 deletions(-) create mode 100644 api/models/RecoverPasswordForm.php create mode 100644 common/validators/PasswordValidate.php create mode 100644 tests/codeception/api/functional/RecoverPasswordCest.php create mode 100644 tests/codeception/api/unit/models/RecoverPasswordFormTest.php diff --git a/api/controllers/AuthenticationController.php b/api/controllers/AuthenticationController.php index 4aab302..cd7cf92 100644 --- a/api/controllers/AuthenticationController.php +++ b/api/controllers/AuthenticationController.php @@ -3,6 +3,7 @@ namespace api\controllers; use api\models\ForgotPasswordForm; use api\models\LoginForm; +use api\models\RecoverPasswordForm; use common\helpers\StringHelper; use Yii; use yii\filters\AccessControl; @@ -13,13 +14,13 @@ class AuthenticationController extends Controller { public function behaviors() { return ArrayHelper::merge(parent::behaviors(), [ 'authenticator' => [ - 'except' => ['login', 'forgot-password'], + 'except' => ['login', 'forgot-password', 'recover-password'], ], 'access' => [ 'class' => AccessControl::class, 'rules' => [ [ - 'actions' => ['login', 'forgot-password'], + 'actions' => ['login', 'forgot-password', 'recover-password'], 'allow' => true, 'roles' => ['?'], ], @@ -32,6 +33,7 @@ class AuthenticationController extends Controller { return [ 'login' => ['POST'], 'forgot-password' => ['POST'], + 'recover-password' => ['POST'], ]; } @@ -93,4 +95,20 @@ class AuthenticationController extends Controller { return $response; } + public function actionRecoverPassword() { + $model = new RecoverPasswordForm(); + $model->load(Yii::$app->request->post()); + if (($jwt = $model->recoverPassword()) === false) { + return [ + 'success' => false, + 'errors' => $this->normalizeModelErrors($model->getErrors()), + ]; + } + + return [ + 'success' => true, + 'jwt' => $jwt, + ]; + } + } diff --git a/api/models/ChangePasswordForm.php b/api/models/ChangePasswordForm.php index e34a9e5..a71fc78 100644 --- a/api/models/ChangePasswordForm.php +++ b/api/models/ChangePasswordForm.php @@ -1,9 +1,9 @@ 'error.newPassword_required'], - ['newRePassword', 'required', 'message' => 'error.newRePassword_required'], - ['newPassword', 'string', 'min' => 8, 'tooShort' => 'error.password_too_short'], + [['newPassword', 'newRePassword'], 'required', 'message' => 'error.{attribute}_required'], + ['newPassword', PasswordValidate::class], ['newRePassword', 'validatePasswordAndRePasswordMatch'], ['logoutAll', 'boolean'], ]); diff --git a/api/models/RecoverPasswordForm.php b/api/models/RecoverPasswordForm.php new file mode 100644 index 0000000..b607a17 --- /dev/null +++ b/api/models/RecoverPasswordForm.php @@ -0,0 +1,71 @@ + 'error.{attribute}_required'], + ['newPassword', PasswordValidate::class], + ['newRePassword', 'validatePasswordAndRePasswordMatch'], + ]); + } + + public function validatePasswordAndRePasswordMatch($attribute) { + if (!$this->hasErrors()) { + if ($this->newPassword !== $this->newRePassword) { + $this->addError($attribute, 'error.rePassword_does_not_match'); + } + } + } + + public function recoverPassword() { + if (!$this->validate()) { + return false; + } + + $confirmModel = $this->getActivationCodeModel(); + if ($confirmModel->type !== EmailActivation::TYPE_FORGOT_PASSWORD_KEY) { + $confirmModel->delete(); + // TODO: вот где-то здесь нужно ещё попутно сгенерировать соответствующую ошибку + return false; + } + + $transaction = Yii::$app->db->beginTransaction(); + try { + $account = $confirmModel->account; + $account->password = $this->newPassword; + if (!$confirmModel->delete()) { + throw new ErrorException('Unable remove activation key.'); + } + + if (!$account->save()) { + throw new ErrorException('Unable activate user account.'); + } + + $transaction->commit(); + } catch (ErrorException $e) { + $transaction->rollBack(); + if (YII_DEBUG) { + throw $e; + } else { + return false; + } + } + + // TODO: ещё было бы неплохо уведомить пользователя о том, что его E-mail изменился + + return $account->getJWT(); + } + +} diff --git a/api/models/RegistrationForm.php b/api/models/RegistrationForm.php index c762475..751a5da 100644 --- a/api/models/RegistrationForm.php +++ b/api/models/RegistrationForm.php @@ -7,6 +7,7 @@ use common\components\UserFriendlyRandomKey; use common\models\Account; use common\models\confirmations\RegistrationConfirmation; use common\models\EmailActivation; +use common\validators\PasswordValidate; use Ramsey\Uuid\Uuid; use Yii; use yii\base\ErrorException; @@ -30,7 +31,7 @@ class RegistrationForm extends ApiForm { ['password', 'required', 'message' => 'error.password_required'], ['rePassword', 'required', 'message' => 'error.rePassword_required'], - ['password', 'string', 'min' => 8, 'tooShort' => 'error.password_too_short'], + ['password', PasswordValidate::class], ['rePassword', 'validatePasswordAndRePasswordMatch'], ]; } diff --git a/common/validators/PasswordValidate.php b/common/validators/PasswordValidate.php new file mode 100644 index 0000000..24ac883 --- /dev/null +++ b/common/validators/PasswordValidate.php @@ -0,0 +1,15 @@ +=5.6.0", + "php": "~7.0.6", "yiisoft/yii2": "~2.0.6", "yiisoft/yii2-bootstrap": "*", "yiisoft/yii2-swiftmailer": "*", diff --git a/tests/codeception/api/_pages/AuthenticationRoute.php b/tests/codeception/api/_pages/AuthenticationRoute.php index ed98d2c..615ec63 100644 --- a/tests/codeception/api/_pages/AuthenticationRoute.php +++ b/tests/codeception/api/_pages/AuthenticationRoute.php @@ -23,4 +23,13 @@ class AuthenticationRoute extends BasePage { ]); } + public function recoverPassword($key = null, $newPassword = null, $newRePassword = null) { + $this->route = ['authentication/recover-password']; + $this->actor->sendPOST($this->getUrl(), [ + 'key' => $key, + 'newPassword' => $newPassword, + 'newRePassword' => $newRePassword, + ]); + } + } diff --git a/tests/codeception/api/functional/RecoverPasswordCest.php b/tests/codeception/api/functional/RecoverPasswordCest.php new file mode 100644 index 0000000..6060896 --- /dev/null +++ b/tests/codeception/api/functional/RecoverPasswordCest.php @@ -0,0 +1,36 @@ +wantTo('change my account password, using key from email'); + $authRoute->recoverPassword('H24HBDCHHAG2HGHGHS', '12345678', '12345678'); + $I->canSeeResponseContainsJson([ + 'success' => true, + ]); + $I->canSeeResponseJsonMatchesJsonPath('$.jwt'); + + $I->wantTo('ensure, that jwt token is valid'); + $jwt = $I->grabDataFromResponseByJsonPath('$.jwt')[0]; + $I->amBearerAuthenticated($jwt); + $accountRoute = new AccountsRoute($I); + $accountRoute->current(); + $I->canSeeResponseCodeIs(200); + $I->canSeeResponseIsJson(); + $I->notLoggedIn(); + + $I->wantTo('check, that password is really changed'); + $authRoute->login('Notch', '12345678'); + $I->canSeeResponseContainsJson([ + 'success' => true, + ]); + } + +} diff --git a/tests/codeception/api/unit/models/RecoverPasswordFormTest.php b/tests/codeception/api/unit/models/RecoverPasswordFormTest.php new file mode 100644 index 0000000..1409ad3 --- /dev/null +++ b/tests/codeception/api/unit/models/RecoverPasswordFormTest.php @@ -0,0 +1,44 @@ + [ + 'class' => EmailActivationFixture::class, + 'dataFile' => '@tests/codeception/common/fixtures/data/email-activations.php', + ], + ]; + } + + public function testRecoverPassword() { + $fixture = $this->emailActivations['freshPasswordRecovery']; + $this->specify('change user account password by email confirmation key', function() use ($fixture) { + $model = new RecoverPasswordForm([ + 'key' => $fixture['key'], + 'newPassword' => '12345678', + 'newRePassword' => '12345678', + ]); + expect($model->recoverPassword())->notEquals(false); + $activationExists = EmailActivation::find()->andWhere(['key' => $fixture['key']])->exists(); + expect($activationExists)->false(); + /** @var Account $account */ + $account = Account::findOne($fixture['account_id']); + expect($account->validatePassword('12345678'))->true(); + }); + } + +}