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(); + }); + } + +}