diff --git a/.dockerignore b/.dockerignore index 75687c0..d5427a2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,8 +3,8 @@ # vendor будет заполнен уже внутри контейнера vendor -# frontend и его node_modules внутри контейнера не нужны -frontend +# node_modules для этого контейнера не нужны +node_modules # Все -local файлы */config/*-local.php diff --git a/api/controllers/AccountsController.php b/api/controllers/AccountsController.php index 6b056ee..fefc292 100644 --- a/api/controllers/AccountsController.php +++ b/api/controllers/AccountsController.php @@ -52,6 +52,7 @@ class AccountsController extends Controller { 'uuid' => $account->uuid, 'username' => $account->username, 'email' => $account->email, + 'lang' => $account->lang, 'shouldChangePassword' => $account->password_hash_strategy === Account::PASS_HASH_STRATEGY_OLD_ELY, 'isActive' => $account->status === Account::STATUS_ACTIVE, 'passwordChangedAt' => $account->password_changed_at, diff --git a/api/models/PasswordResetRequestForm.php b/api/models/PasswordResetRequestForm.php deleted file mode 100644 index f691252..0000000 --- a/api/models/PasswordResetRequestForm.php +++ /dev/null @@ -1,60 +0,0 @@ - 'trim'], - ['email', 'required'], - ['email', 'email'], - ['email', 'exist', - 'targetClass' => '\common\models\User', - 'filter' => ['status' => Account::STATUS_ACTIVE], - 'message' => 'There is no user with such email.' - ], - ]; - } - - /** - * Sends an email with a link, for resetting the password. - * - * @return boolean whether the email was send - */ - public function sendEmail() - { - /* @var $user Account */ - $user = Account::findOne([ - 'status' => Account::STATUS_ACTIVE, - 'email' => $this->email, - ]); - - if ($user) { - if (!Account::isPasswordResetTokenValid($user->password_reset_token)) { - $user->generatePasswordResetToken(); - } - - if ($user->save()) { - return \Yii::$app->mailer->compose(['html' => 'passwordResetToken-html', 'text' => 'passwordResetToken-text'], ['user' => $user]) - ->setFrom([\Yii::$app->params['supportEmail'] => \Yii::$app->name . ' robot']) - ->setTo($this->email) - ->setSubject('Password reset for ' . \Yii::$app->name) - ->send(); - } - } - - return false; - } -} diff --git a/api/models/RegistrationForm.php b/api/models/RegistrationForm.php index 751a5da..77cdceb 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\LanguageValidator; use common\validators\PasswordValidate; use Ramsey\Uuid\Uuid; use Yii; @@ -20,6 +21,7 @@ class RegistrationForm extends ApiForm { public $password; public $rePassword; public $rulesAgreement; + public $lang; public function rules() { return [ @@ -33,6 +35,8 @@ class RegistrationForm extends ApiForm { ['rePassword', 'required', 'message' => 'error.rePassword_required'], ['password', PasswordValidate::class], ['rePassword', 'validatePasswordAndRePasswordMatch'], + + ['lang', LanguageValidator::class], ]; } @@ -75,6 +79,7 @@ class RegistrationForm extends ApiForm { $account->email = $this->email; $account->username = $this->username; $account->password = $this->password; + $account->lang = $this->lang; $account->status = Account::STATUS_REGISTERED; if (!$account->save()) { throw new ErrorException('Account not created.'); diff --git a/common/config/bootstrap.php b/common/config/bootstrap.php index 892906d..64528ff 100644 --- a/common/config/bootstrap.php +++ b/common/config/bootstrap.php @@ -2,3 +2,4 @@ Yii::setAlias('common', dirname(__DIR__)); Yii::setAlias('api', dirname(dirname(__DIR__)) . '/api'); Yii::setAlias('console', dirname(dirname(__DIR__)) . '/console'); +Yii::setAlias('frontend', dirname(dirname(__DIR__)) . '/frontend'); diff --git a/common/mail/passwordResetToken-html.php b/common/mail/passwordResetToken-html.php deleted file mode 100644 index 451d21d..0000000 --- a/common/mail/passwordResetToken-html.php +++ /dev/null @@ -1,15 +0,0 @@ -urlManager->createAbsoluteUrl(['site/reset-password', 'token' => $user->password_reset_token]); -?> -
-

Hello email) ?>,

- -

Follow the link below to reset your password:

- -

-
diff --git a/common/mail/passwordResetToken-text.php b/common/mail/passwordResetToken-text.php deleted file mode 100644 index 936889d..0000000 --- a/common/mail/passwordResetToken-text.php +++ /dev/null @@ -1,12 +0,0 @@ -urlManager->createAbsoluteUrl(['site/reset-password', 'token' => $user->password_reset_token]); -?> -Hello email ?>, - -Follow the link below to reset your password: - - diff --git a/common/models/Account.php b/common/models/Account.php index d6d85ea..a1b5bd4 100644 --- a/common/models/Account.php +++ b/common/models/Account.php @@ -2,6 +2,7 @@ namespace common\models; use common\components\UserPass; +use common\validators\LanguageValidator; use damirka\JWT\UserTrait as UserJWTTrait; use Ely\Yii2\TempmailValidator; use Yii; @@ -17,7 +18,7 @@ use yii\db\ActiveRecord; * @property string $email * @property string $password_hash * @property integer $password_hash_strategy - * @property string $password_reset_token + * @property string $lang * @property integer $status * @property integer $created_at * @property integer $updated_at @@ -73,6 +74,9 @@ class Account extends ActiveRecord { [['email'], 'email', 'checkDNS' => true, 'enableIDN' => true, 'message' => 'error.email_invalid'], [['email'], TempmailValidator::class, 'message' => 'error.email_is_tempmail'], [['email'], 'unique', 'message' => 'error.email_not_available'], + + [['lang'], LanguageValidator::class], + [['lang'], 'default', 'value' => 'en'], ]; } diff --git a/common/validators/LanguageValidator.php b/common/validators/LanguageValidator.php new file mode 100644 index 0000000..a553bac --- /dev/null +++ b/common/validators/LanguageValidator.php @@ -0,0 +1,38 @@ +getFilesNames(); + if (in_array($value, $files)) { + return null; + } + + return [$this->message, []]; + } + + protected function getFilesNames() { + $files = array_values(array_filter(scandir($this->getFolderPath()), function(&$value) { + return $value !== '..' && $value !== '.'; + })); + + return array_map(function($value) { + return basename($value, '.json'); + }, $files); + } + + protected function getFolderPath() { + return Yii::getAlias('@frontend/src/i18n'); + } + +} diff --git a/composer.json b/composer.json index 00d00d1..15f4264 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "minimum-stability": "stable", "require": { "php": "~7.0.6", - "yiisoft/yii2": "~2.0.6", + "yiisoft/yii2": "~2.0.8", "yiisoft/yii2-bootstrap": "*", "yiisoft/yii2-swiftmailer": "*", "ramsey/uuid": "~3.1", diff --git a/console/migrations/m160512_194546_account_language.php b/console/migrations/m160512_194546_account_language.php new file mode 100644 index 0000000..71959a2 --- /dev/null +++ b/console/migrations/m160512_194546_account_language.php @@ -0,0 +1,17 @@ +addColumn('{{%accounts}}', 'lang', $this->string(5)->notNull()->defaultValue('en')->after('password_hash_strategy')); + $this->dropColumn('{{%accounts}}', 'password_reset_token'); + } + + public function safeDown() { + $this->dropColumn('{{%accounts}}', 'lang'); + $this->addColumn('{{%accounts}}', 'password_reset_token', $this->string()->unique()); + } + +} diff --git a/tests/codeception/api/functional/AccountsCurrentCest.php b/tests/codeception/api/functional/AccountsCurrentCest.php index c49e4cb..05d102b 100644 --- a/tests/codeception/api/functional/AccountsCurrentCest.php +++ b/tests/codeception/api/functional/AccountsCurrentCest.php @@ -26,8 +26,12 @@ class AccountsCurrentCest { 'id' => 1, 'username' => 'Admin', 'email' => 'admin@ely.by', + 'lang' => 'en', 'shouldChangePassword' => false, + 'isActive' => true, + 'hasMojangUsernameCollision' => false, ]); + $I->canSeeResponseJsonMatchesJsonPath('$.passwordChangedAt'); } } diff --git a/tests/codeception/api/functional/RegisterCest.php b/tests/codeception/api/functional/RegisterCest.php index 58c48bd..608bb7c 100644 --- a/tests/codeception/api/functional/RegisterCest.php +++ b/tests/codeception/api/functional/RegisterCest.php @@ -214,6 +214,7 @@ class RegisterCest { 'password' => 'some_password', 'rePassword' => 'some_password', 'rulesAgreement' => true, + 'lang' => 'ru', ]); $I->canSeeResponseCodeIs(200); $I->canSeeResponseIsJson(); diff --git a/tests/codeception/api/unit/models/ChangePasswordFormTest.php b/tests/codeception/api/unit/models/ChangePasswordFormTest.php index af8f986..acc310d 100644 --- a/tests/codeception/api/unit/models/ChangePasswordFormTest.php +++ b/tests/codeception/api/unit/models/ChangePasswordFormTest.php @@ -85,9 +85,10 @@ class ChangePasswordFormTest extends DbTestCase { 'newRePassword' => 'my-new-password', ]); $this->specify('successfully change password with legacy hash strategy', function() use ($model, $account) { + $callTime = time(); expect('form should return true', $model->changePassword())->true(); expect('new password should be successfully stored into account', $account->validatePassword('my-new-password'))->true(); - expect('password change time updated', $account->password_changed_at)->greaterOrEquals(time() - 2); + expect('password change time updated', $account->password_changed_at)->greaterOrEquals($callTime); }); } diff --git a/tests/codeception/api/unit/models/RegistrationFormTest.php b/tests/codeception/api/unit/models/RegistrationFormTest.php index fbde39d..1c01655 100644 --- a/tests/codeception/api/unit/models/RegistrationFormTest.php +++ b/tests/codeception/api/unit/models/RegistrationFormTest.php @@ -68,19 +68,43 @@ class RegistrationFormTest extends DbTestCase { 'password' => 'some_password', 'rePassword' => 'some_password', 'rulesAgreement' => true, + 'lang' => 'ru', ]); - $user = $model->signup(); + $account = $model->signup(); - expect('user should be valid', $user)->isInstanceOf(Account::class); - expect('password should be correct', $user->validatePassword('some_password'))->true(); - expect('uuid is set', $user->uuid)->notEmpty(); + $this->expectSuccessRegistration($account); + expect('lang is set', $account->lang)->equals('ru'); + } + + public function testSignupWithDefaultLanguage() { + $model = new RegistrationForm([ + 'username' => 'some_username', + 'email' => 'some_email@example.com', + 'password' => 'some_password', + 'rePassword' => 'some_password', + 'rulesAgreement' => true, + ]); + + $account = $model->signup(); + + $this->expectSuccessRegistration($account); + expect('lang is set', $account->lang)->equals('en'); + } + + /** + * @param Account|null $account + */ + private function expectSuccessRegistration($account) { + expect('user should be valid', $account)->isInstanceOf(Account::class); + expect('password should be correct', $account->validatePassword('some_password'))->true(); + expect('uuid is set', $account->uuid)->notEmpty(); expect('user model exists in database', Account::find()->andWhere([ 'username' => 'some_username', 'email' => 'some_email@example.com', ])->exists())->true(); expect('email activation code exists in database', EmailActivation::find()->andWhere([ - 'account_id' => $user->id, + 'account_id' => $account->id, 'type' => EmailActivation::TYPE_REGISTRATION_EMAIL_CONFIRMATION, ])->exists())->true(); expect_file('message file exists', $this->getMessageFile())->exists(); diff --git a/tests/codeception/common/fixtures/data/accounts.php b/tests/codeception/common/fixtures/data/accounts.php index 3ff2ebd..12112cb 100644 --- a/tests/codeception/common/fixtures/data/accounts.php +++ b/tests/codeception/common/fixtures/data/accounts.php @@ -7,7 +7,7 @@ return [ 'email' => 'admin@ely.by', 'password_hash' => '$2y$13$CXT0Rkle1EMJ/c1l5bylL.EylfmQ39O5JlHJVFpNn618OUS1HwaIi', # password_0 'password_hash_strategy' => \common\models\Account::PASS_HASH_STRATEGY_YII2, - 'password_reset_token' => null, + 'lang' => 'en', 'status' => \common\models\Account::STATUS_ACTIVE, 'created_at' => 1451775316, 'updated_at' => 1451775316, @@ -20,7 +20,7 @@ return [ 'email' => 'erickskrauch123@yandex.ru', 'password_hash' => '133c00c463cbd3e491c28cb653ce4718', # 12345678 'password_hash_strategy' => \common\models\Account::PASS_HASH_STRATEGY_OLD_ELY, - 'password_reset_token' => null, + 'lang' => 'en', 'status' => \common\models\Account::STATUS_ACTIVE, 'created_at' => 1385225069, 'updated_at' => 1385225069, @@ -33,7 +33,7 @@ return [ 'email' => 'achristiansen@gmail.com', 'password_hash' => '$2y$13$2rYkap5T6jG8z/mMK8a3Ou6aZxJcmAaTha6FEuujvHEmybSHRzW5e', # password_0 'password_hash_strategy' => \common\models\Account::PASS_HASH_STRATEGY_YII2, - 'password_reset_token' => null, + 'lang' => 'en', 'status' => \common\models\Account::STATUS_REGISTERED, 'created_at' => 1453146616, 'updated_at' => 1453146616, @@ -46,7 +46,7 @@ return [ 'email' => 'jon@ely.by', 'password_hash' => '$2y$13$2rYkap5T6jG8z/mMK8a3Ou6aZxJcmAaTha6FEuujvHEmybSHRzW5e', # password_0 'password_hash_strategy' => \common\models\Account::PASS_HASH_STRATEGY_YII2, - 'password_reset_token' => null, + 'lang' => 'en', 'status' => \common\models\Account::STATUS_REGISTERED, 'created_at' => 1457890086, 'updated_at' => 1457890086, @@ -58,7 +58,7 @@ return [ 'email' => 'notch@mojang.com', 'password_hash' => '$2y$13$2rYkap5T6jG8z/mMK8a3Ou6aZxJcmAaTha6FEuujvHEmybSHRzW5e', # password_0 'password_hash_strategy' => \common\models\Account::PASS_HASH_STRATEGY_YII2, - 'password_reset_token' => null, + 'lang' => 'en', 'status' => \common\models\Account::STATUS_ACTIVE, 'created_at' => 1462891432, 'updated_at' => 1462891432, @@ -70,7 +70,7 @@ return [ 'email' => '23derevo@gmail.com', 'password_hash' => '$2y$13$2rYkap5T6jG8z/mMK8a3Ou6aZxJcmAaTha6FEuujvHEmybSHRzW5e', # password_0 'password_hash_strategy' => \common\models\Account::PASS_HASH_STRATEGY_YII2, - 'password_reset_token' => null, + 'lang' => 'en', 'status' => \common\models\Account::STATUS_ACTIVE, 'created_at' => 1462891612, 'updated_at' => 1462891612, diff --git a/tests/codeception/common/templates/fixtures/account.php b/tests/codeception/common/templates/fixtures/account.php index b3bc951..202cd42 100644 --- a/tests/codeception/common/templates/fixtures/account.php +++ b/tests/codeception/common/templates/fixtures/account.php @@ -12,7 +12,7 @@ return [ 'email' => $faker->email, 'password_hash' => $security->generatePasswordHash('password_' . $index), 'password_hash_strategy' => \common\models\Account::PASS_HASH_STRATEGY_YII2, - 'password_reset_token' => NULL, + 'lang' => 'en', 'auth_key' => $security->generateRandomString(), 'status' => \common\models\Account::STATUS_ACTIVE, 'created_at' => time(), diff --git a/tests/codeception/common/unit/TestCase.php b/tests/codeception/common/unit/TestCase.php index 7304758..8496a63 100644 --- a/tests/codeception/common/unit/TestCase.php +++ b/tests/codeception/common/unit/TestCase.php @@ -1,11 +1,8 @@ 'bayer.hudson', - 'auth_key' => 'HP187Mvq7Mmm3CTU80dLkGmni_FUH_lR', - //password_0 - 'password_hash' => '$2y$13$EjaPFBnZOQsHdGuHI.xvhuDp1fHpo8hKRSk6yshqa9c5EG8s3C3lO', - 'password_reset_token' => 'ExzkCOaYc1L8IOBs4wdTGGbgNiG3Wz1I_1402312317', - 'created_at' => '1402312317', - 'updated_at' => '1402312317', - 'email' => 'nicole.paucek@schultz.info', - ], -]; diff --git a/tests/codeception/common/unit/validators/LanguageValidatorTest.php b/tests/codeception/common/unit/validators/LanguageValidatorTest.php new file mode 100644 index 0000000..3148306 --- /dev/null +++ b/tests/codeception/common/unit/validators/LanguageValidatorTest.php @@ -0,0 +1,53 @@ +specify('get list of 2 languages: ru and en', function() { + $model = $this->createModelWithFixturePath(); + expect($this->callProtected($model, 'getFilesNames'))->equals(['en', 'ru']); + }); + } + + public function testValidateValue() { + $this->specify('get null, because language is supported', function() { + $model = $this->createModelWithFixturePath(); + expect($this->callProtected($model, 'validateValue', 'ru'))->null(); + }); + + $this->specify('get error message, because language is unsupported', function() { + $model = $this->createModelWithFixturePath(); + expect($this->callProtected($model, 'validateValue', 'by'))->equals([ + $model->message, + [], + ]); + }); + } + + /** + * @return LanguageValidator + */ + private function createModelWithFixturePath() { + return new class extends LanguageValidator { + public function getFolderPath() { + return __DIR__ . '/../fixtures/data/i18n'; + } + }; + } + + private function callProtected($object, string $function, ...$args) { + $class = new ReflectionClass($object); + $method = $class->getMethod($function); + $method->setAccessible(true); + + return $method->invokeArgs($object, $args); + } + +}