mirror of
https://github.com/elyby/accounts.git
synced 2024-11-26 16:52:02 +05:30
Изменёна кодировка столбца username в usernames_history для организации бинарного поиска
Из Account вынесена логика аутентификации в дочерний AccountIdentity Исправлена логика валидации при вызове на неизменённом нике для формы смены ника
This commit is contained in:
parent
2a4da87fd5
commit
184ff02240
@ -13,7 +13,7 @@ return [
|
|||||||
'controllerNamespace' => 'api\controllers',
|
'controllerNamespace' => 'api\controllers',
|
||||||
'components' => [
|
'components' => [
|
||||||
'user' => [
|
'user' => [
|
||||||
'identityClass' => \common\models\Account::class,
|
'identityClass' => \api\models\AccountIdentity::class,
|
||||||
'enableSession' => false,
|
'enableSession' => false,
|
||||||
'loginUrl' => null,
|
'loginUrl' => null,
|
||||||
],
|
],
|
||||||
@ -21,7 +21,7 @@ return [
|
|||||||
'traceLevel' => YII_DEBUG ? 3 : 0,
|
'traceLevel' => YII_DEBUG ? 3 : 0,
|
||||||
'targets' => [
|
'targets' => [
|
||||||
[
|
[
|
||||||
'class' => 'yii\log\FileTarget',
|
'class' => \yii\log\FileTarget::class,
|
||||||
'levels' => ['error', 'warning'],
|
'levels' => ['error', 'warning'],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
37
api/models/AccountIdentity.php
Normal file
37
api/models/AccountIdentity.php
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
namespace api\models;
|
||||||
|
|
||||||
|
use common\models\Account;
|
||||||
|
use yii\base\NotSupportedException;
|
||||||
|
use yii\web\IdentityInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @method static findIdentityByAccessToken($token, $type = null) этот метод реализуется в UserTrait, который
|
||||||
|
* подключён в родительском Account и позволяет выполнить условия интерфейса
|
||||||
|
* @method string getId() метод реализован в родительском классе, т.к. UserTrait требует, чтобы этот метод
|
||||||
|
* присутствовал обязательно, но при этом не навязывает его как абстрактный
|
||||||
|
*/
|
||||||
|
class AccountIdentity extends Account implements IdentityInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
public static function findIdentity($id) {
|
||||||
|
return static::findOne($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
public function getAuthKey() {
|
||||||
|
throw new NotSupportedException('This method used for cookie auth, except we using JWT tokens');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
public function validateAuthKey($authKey) {
|
||||||
|
return $this->getAuthKey() === $authKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -3,7 +3,6 @@ namespace api\models;
|
|||||||
|
|
||||||
use api\models\base\PasswordProtectedForm;
|
use api\models\base\PasswordProtectedForm;
|
||||||
use common\helpers\Amqp;
|
use common\helpers\Amqp;
|
||||||
use common\models\Account;
|
|
||||||
use common\models\amqp\UsernameChanged;
|
use common\models\amqp\UsernameChanged;
|
||||||
use common\models\UsernameHistory;
|
use common\models\UsernameHistory;
|
||||||
use PhpAmqpLib\Message\AMQPMessage;
|
use PhpAmqpLib\Message\AMQPMessage;
|
||||||
@ -23,7 +22,7 @@ class ChangeUsernameForm extends PasswordProtectedForm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function validateUsername($attribute) {
|
public function validateUsername($attribute) {
|
||||||
$account = new Account();
|
$account = $this->getAccount();
|
||||||
$account->username = $this->$attribute;
|
$account->username = $this->$attribute;
|
||||||
if (!$account->validate(['username'])) {
|
if (!$account->validate(['username'])) {
|
||||||
$this->addErrors($account->getErrors());
|
$this->addErrors($account->getErrors());
|
||||||
|
@ -2,28 +2,26 @@
|
|||||||
namespace common\models;
|
namespace common\models;
|
||||||
|
|
||||||
use common\components\UserPass;
|
use common\components\UserPass;
|
||||||
use damirka\JWT\UserTrait;
|
use damirka\JWT\UserTrait as UserJWTTrait;
|
||||||
use Ely\Yii2\TempmailValidator;
|
use Ely\Yii2\TempmailValidator;
|
||||||
use Yii;
|
use Yii;
|
||||||
use yii\base\InvalidConfigException;
|
use yii\base\InvalidConfigException;
|
||||||
use yii\base\NotSupportedException;
|
|
||||||
use yii\behaviors\TimestampBehavior;
|
use yii\behaviors\TimestampBehavior;
|
||||||
use yii\db\ActiveRecord;
|
use yii\db\ActiveRecord;
|
||||||
use yii\web\IdentityInterface;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Поля модели:
|
* Поля модели:
|
||||||
* @property integer $id
|
* @property integer $id
|
||||||
* @property string $uuid
|
* @property string $uuid
|
||||||
* @property string $username
|
* @property string $username
|
||||||
* @property string $email
|
* @property string $email
|
||||||
* @property string $password_hash
|
* @property string $password_hash
|
||||||
* @property integer $password_hash_strategy
|
* @property integer $password_hash_strategy
|
||||||
* @property string $password_reset_token
|
* @property string $password_reset_token
|
||||||
* @property integer $status
|
* @property integer $status
|
||||||
* @property integer $created_at
|
* @property integer $created_at
|
||||||
* @property integer $updated_at
|
* @property integer $updated_at
|
||||||
* @property integer $password_changed_at
|
* @property integer $password_changed_at
|
||||||
*
|
*
|
||||||
* Геттеры-сеттеры:
|
* Геттеры-сеттеры:
|
||||||
* @property string $password пароль пользователя (только для записи)
|
* @property string $password пароль пользователя (только для записи)
|
||||||
@ -36,8 +34,8 @@ use yii\web\IdentityInterface;
|
|||||||
* Поведения:
|
* Поведения:
|
||||||
* @mixin TimestampBehavior
|
* @mixin TimestampBehavior
|
||||||
*/
|
*/
|
||||||
class Account extends ActiveRecord implements IdentityInterface {
|
class Account extends ActiveRecord {
|
||||||
use UserTrait;
|
use UserJWTTrait;
|
||||||
|
|
||||||
const STATUS_DELETED = -10;
|
const STATUS_DELETED = -10;
|
||||||
const STATUS_REGISTERED = 0;
|
const STATUS_REGISTERED = 0;
|
||||||
@ -52,7 +50,7 @@ class Account extends ActiveRecord implements IdentityInterface {
|
|||||||
|
|
||||||
public function behaviors() {
|
public function behaviors() {
|
||||||
return [
|
return [
|
||||||
TimestampBehavior::className(),
|
TimestampBehavior::class,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,74 +76,6 @@ class Account extends ActiveRecord implements IdentityInterface {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
public static function findIdentity($id) {
|
|
||||||
return static::findOne(['id' => $id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds user by password reset token
|
|
||||||
*
|
|
||||||
* @param string $token password reset token
|
|
||||||
*
|
|
||||||
* @return static|null
|
|
||||||
*
|
|
||||||
* TODO: этот метод нужно убрать из базовой модели
|
|
||||||
*/
|
|
||||||
public static function findByPasswordResetToken($token) {
|
|
||||||
if (!static::isPasswordResetTokenValid($token)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return static::findOne([
|
|
||||||
'password_reset_token' => $token,
|
|
||||||
'status' => self::STATUS_ACTIVE,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds out if password reset token is valid
|
|
||||||
*
|
|
||||||
* @param string $token password reset token
|
|
||||||
*
|
|
||||||
* @return boolean
|
|
||||||
*
|
|
||||||
* TODO: этот метод нужно убрать из базовой модели
|
|
||||||
*/
|
|
||||||
public static function isPasswordResetTokenValid($token) {
|
|
||||||
if (empty($token)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$timestamp = (int) substr($token, strrpos($token, '_') + 1);
|
|
||||||
$expire = Yii::$app->params['user.passwordResetTokenExpire'];
|
|
||||||
|
|
||||||
return $timestamp + $expire >= time();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
public function getId() {
|
|
||||||
return $this->getPrimaryKey();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
public function getAuthKey() {
|
|
||||||
throw new NotSupportedException('This method used for cookie auth, except we using JWT tokens');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
public function validateAuthKey($authKey) {
|
|
||||||
return $this->getAuthKey() === $authKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates password
|
* Validates password
|
||||||
*
|
*
|
||||||
@ -183,24 +113,6 @@ class Account extends ActiveRecord implements IdentityInterface {
|
|||||||
$this->password_changed_at = time();
|
$this->password_changed_at = time();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates new password reset token
|
|
||||||
*
|
|
||||||
* TODO: этот метод нужно отсюда убрать
|
|
||||||
*/
|
|
||||||
public function generatePasswordResetToken() {
|
|
||||||
$this->password_reset_token = Yii::$app->security->generateRandomString() . '_' . time();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes password reset token
|
|
||||||
*
|
|
||||||
* TODO: этот метод нужно отсюда убрать
|
|
||||||
*/
|
|
||||||
public function removePasswordResetToken() {
|
|
||||||
$this->password_reset_token = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getEmailActivations() {
|
public function getEmailActivations() {
|
||||||
return $this->hasMany(EmailActivation::class, ['account_id' => 'id']);
|
return $this->hasMany(EmailActivation::class, ['account_id' => 'id']);
|
||||||
}
|
}
|
||||||
@ -266,4 +178,13 @@ class Account extends ActiveRecord implements IdentityInterface {
|
|||||||
->exists();
|
->exists();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: нужно создать PR в UserTrait репо, чтобы этот метод сделали абстрактным
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getId() {
|
||||||
|
return $this->getPrimaryKey();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use console\db\Migration;
|
||||||
|
|
||||||
|
class m160512_080955_usernames_history_encoding extends Migration {
|
||||||
|
|
||||||
|
public function safeUp() {
|
||||||
|
$this->getDb()->createCommand('
|
||||||
|
ALTER TABLE {{%usernames_history}}
|
||||||
|
MODIFY username VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL
|
||||||
|
')->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function safeDown() {
|
||||||
|
$this->alterColumn('{{%usernames_history}}', 'username', $this->string()->notNull());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -10,7 +10,7 @@ use tests\codeception\common\fixtures\AccountFixture;
|
|||||||
use Yii;
|
use Yii;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property array $accounts
|
* @property AccountFixture $accounts
|
||||||
*/
|
*/
|
||||||
class ChangeUsernameFormTest extends DbTestCase {
|
class ChangeUsernameFormTest extends DbTestCase {
|
||||||
use Specify;
|
use Specify;
|
||||||
@ -26,52 +26,94 @@ class ChangeUsernameFormTest extends DbTestCase {
|
|||||||
|
|
||||||
public function testChange() {
|
public function testChange() {
|
||||||
$this->specify('successfully change username to new one', function() {
|
$this->specify('successfully change username to new one', function() {
|
||||||
$model = new DummyChangeUsernameForm([
|
$model = $this->createModel([
|
||||||
'password' => 'password_0',
|
'password' => 'password_0',
|
||||||
'username' => 'my_new_nickname',
|
'username' => 'my_new_nickname',
|
||||||
]);
|
]);
|
||||||
expect($model->change())->true();
|
expect($model->change())->true();
|
||||||
expect(Account::findOne(1)->username)->equals('my_new_nickname');
|
expect(Account::findOne($this->getAccountId())->username)->equals('my_new_nickname');
|
||||||
expect(UsernameHistory::findOne(['username' => 'my_new_nickname']))->isInstanceOf(UsernameHistory::class);
|
expect(UsernameHistory::findOne(['username' => 'my_new_nickname']))->isInstanceOf(UsernameHistory::class);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testUsernameUnavailable() {
|
public function testChangeWithoutChange() {
|
||||||
|
$this->scenario->incomplete('This test is written invalid');
|
||||||
|
return;
|
||||||
|
|
||||||
|
// TODO: этот тест написан неправильно - запись всё равно добавляется в базу данных, но тест не замечает
|
||||||
|
/** @noinspection PhpUnreachableStatementInspection */
|
||||||
|
$this->specify('no new UsernameHistory record, if we don\'t change nickname', function() {
|
||||||
|
$model = $this->createModel([
|
||||||
|
'password' => 'password_0',
|
||||||
|
'username' => $this->accounts['admin']['username'],
|
||||||
|
]);
|
||||||
|
$callTime = time();
|
||||||
|
expect($model->change())->true();
|
||||||
|
expect(UsernameHistory::findOne([
|
||||||
|
'AND',
|
||||||
|
'username' => $this->accounts['admin']['username'],
|
||||||
|
['>=', 'applied_in', $callTime - 5],
|
||||||
|
]))->null();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testChangeCase() {
|
||||||
|
$this->specify('username should change, if we change case of some letters', function() {
|
||||||
|
$newUsername = mb_strtoupper($this->accounts['admin']['username']);
|
||||||
|
$model = $this->createModel([
|
||||||
|
'password' => 'password_0',
|
||||||
|
'username' => $newUsername,
|
||||||
|
]);
|
||||||
|
expect($model->change())->true();
|
||||||
|
expect(Account::findOne($this->getAccountId())->username)->equals($newUsername);
|
||||||
|
expect(UsernameHistory::findOne(['username' => $newUsername]))->isInstanceOf(UsernameHistory::class);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateUsername() {
|
||||||
$this->specify('error.username_not_available expected if username is already taken', function() {
|
$this->specify('error.username_not_available expected if username is already taken', function() {
|
||||||
$model = new DummyChangeUsernameForm([
|
$model = $this->createModel([
|
||||||
'password' => 'password_0',
|
'password' => 'password_0',
|
||||||
'username' => 'Jon',
|
'username' => 'Jon',
|
||||||
]);
|
]);
|
||||||
$model->validate();
|
$model->validateUsername('username');
|
||||||
expect($model->getErrors('username'))->equals(['error.username_not_available']);
|
expect($model->getErrors('username'))->equals(['error.username_not_available']);
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->specify('error.username_not_available is NOT expected if username is already taken by CURRENT user', function() {
|
$this->specify('error.username_not_available is NOT expected if username is already taken by CURRENT user', function() {
|
||||||
$model = new DummyChangeUsernameForm([
|
$model = $this->createModel([
|
||||||
'password' => 'password_0',
|
'password' => 'password_0',
|
||||||
'username' => 'Admin',
|
'username' => $this->accounts['admin']['username'],
|
||||||
]);
|
]);
|
||||||
$model->validate();
|
$model->validateUsername('username');
|
||||||
expect($model->getErrors('username'))->equals([]);
|
expect($model->getErrors('username'))->isEmpty();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCreateTask() {
|
public function testCreateTask() {
|
||||||
$model = new DummyChangeUsernameForm();
|
$model = $this->createModel();
|
||||||
$model->createTask('1', 'test1', 'test');
|
$model->createTask('1', 'test1', 'test');
|
||||||
// TODO: у меня пока нет идей о том, чтобы это как-то успешно протестировать, увы
|
// TODO: у меня пока нет идей о том, чтобы это как-то успешно протестировать, увы
|
||||||
// но по крайней мере можно убедиться, что оно не падает где-то на этом шаге
|
// но по крайней мере можно убедиться, что оно не падает где-то на этом шаге
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
private function createModel(array $params = []) : ChangeUsernameForm {
|
||||||
|
/** @noinspection PhpUnusedLocalVariableInspection */
|
||||||
|
$params = array_merge($params, [
|
||||||
|
'accountId' => $this->getAccountId(),
|
||||||
|
]);
|
||||||
|
|
||||||
// TODO: тут образуется магическая переменная 1, что не круто. После перехода на php7 можно заюзать анонимный класс
|
return new class($params) extends ChangeUsernameForm {
|
||||||
// и создавать модель прямо внутри теста, где доступен объект фикстур с именами переменных
|
public $accountId;
|
||||||
|
|
||||||
class DummyChangeUsernameForm extends ChangeUsernameForm {
|
protected function getAccount() {
|
||||||
|
return Account::findOne($this->accountId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
protected function getAccount() {
|
private function getAccountId() {
|
||||||
return Account::findOne(1);
|
return $this->accounts['admin']['id'];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user