mirror of
https://github.com/elyby/accounts.git
synced 2025-05-31 14:11:46 +05:30
Объединены сущности для авторизации посредством JWT токенов и токенов, выданных через oAuth2.
Все действия, связанные с аккаунтами, теперь вызываются через url `/api/v1/accounts/<id>/<action>`. Добавлена вменяемая система разграничения прав на основе RBAC. Теперь oAuth2 токены генерируются как случайная строка в 40 символов длинной, а не UUID. Исправлен баг с неправильным временем жизни токена в ответе успешного запроса аутентификации. Теперь все unit тесты можно успешно прогнать без наличия интернета.
This commit is contained in:
@@ -1,67 +0,0 @@
|
||||
<?php
|
||||
namespace api\models;
|
||||
|
||||
use common\models\Account;
|
||||
use Emarref\Jwt\Claim\JwtId;
|
||||
use Emarref\Jwt\Exception\ExpiredException;
|
||||
use Yii;
|
||||
use yii\base\NotSupportedException;
|
||||
use yii\web\IdentityInterface;
|
||||
use yii\web\UnauthorizedHttpException;
|
||||
|
||||
class AccountIdentity extends Account implements IdentityInterface {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public static function findIdentityByAccessToken($token, $type = null) {
|
||||
/** @var \api\components\User\Component $component */
|
||||
$component = Yii::$app->user;
|
||||
try {
|
||||
$token = $component->parseToken($token);
|
||||
} catch (ExpiredException $e) {
|
||||
throw new UnauthorizedHttpException('Token expired');
|
||||
} catch (\Exception $e) {
|
||||
throw new UnauthorizedHttpException('Incorrect token');
|
||||
}
|
||||
|
||||
// Если исключение выше не случилось, то значит всё оке
|
||||
/** @var JwtId $jti */
|
||||
$jti = $token->getPayload()->findClaimByName(JwtId::NAME);
|
||||
$account = static::findOne($jti->getValue());
|
||||
if ($account === null) {
|
||||
throw new UnauthorizedHttpException('Invalid token');
|
||||
}
|
||||
|
||||
return $account;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getId() {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
throw new NotSupportedException('This method used for cookie auth, except we using JWT tokens');
|
||||
}
|
||||
|
||||
}
|
28
api/models/OauthAccountInfo.php
Normal file
28
api/models/OauthAccountInfo.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
namespace api\models;
|
||||
|
||||
use api\models\base\BaseAccountForm;
|
||||
use api\modules\accounts\models\AccountInfo;
|
||||
use common\models\Account;
|
||||
|
||||
class OauthAccountInfo extends BaseAccountForm {
|
||||
|
||||
private $model;
|
||||
|
||||
public function __construct(Account $account, array $config = []) {
|
||||
parent::__construct($account, $config);
|
||||
$this->model = new AccountInfo($account);
|
||||
}
|
||||
|
||||
public function info(): array {
|
||||
$response = $this->model->info();
|
||||
|
||||
$response['profileLink'] = $response['elyProfileLink'];
|
||||
unset($response['elyProfileLink']);
|
||||
$response['preferredLanguage'] = $response['lang'];
|
||||
unset($response['lang']);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
}
|
@@ -83,7 +83,7 @@ class OauthProcess {
|
||||
try {
|
||||
$grant = $this->getAuthorizationCodeGrant();
|
||||
$authParams = $grant->checkAuthorizeParams();
|
||||
$account = Yii::$app->user->identity;
|
||||
$account = Yii::$app->user->identity->getAccount();
|
||||
/** @var \common\models\OauthClient $clientModel */
|
||||
$clientModel = OauthClient::findOne($authParams->getClient()->getId());
|
||||
|
||||
|
@@ -1,9 +1,8 @@
|
||||
<?php
|
||||
namespace api\models\authentication;
|
||||
|
||||
use api\models\AccountIdentity;
|
||||
use api\models\base\ApiForm;
|
||||
use api\models\profile\ChangeUsernameForm;
|
||||
use api\modules\accounts\models\ChangeUsernameForm;
|
||||
use api\validators\EmailActivationKeyValidator;
|
||||
use common\models\Account;
|
||||
use common\models\EmailActivation;
|
||||
@@ -14,14 +13,14 @@ class ConfirmEmailForm extends ApiForm {
|
||||
|
||||
public $key;
|
||||
|
||||
public function rules() {
|
||||
public function rules(): array {
|
||||
return [
|
||||
['key', EmailActivationKeyValidator::class, 'type' => EmailActivation::TYPE_REGISTRATION_EMAIL_CONFIRMATION],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \api\components\User\LoginResult|bool
|
||||
* @return \api\components\User\AuthenticationResult|bool
|
||||
* @throws ErrorException
|
||||
*/
|
||||
public function confirm() {
|
||||
@@ -48,7 +47,7 @@ class ConfirmEmailForm extends ApiForm {
|
||||
|
||||
$transaction->commit();
|
||||
|
||||
return Yii::$app->user->login(new AccountIdentity($account->attributes), true);
|
||||
return Yii::$app->user->createJwtAuthenticationToken($account, true);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -101,7 +101,7 @@ class ForgotPasswordForm extends ApiForm {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getLogin() {
|
||||
public function getLogin(): string {
|
||||
return $this->login;
|
||||
}
|
||||
|
||||
|
@@ -1,7 +1,6 @@
|
||||
<?php
|
||||
namespace api\models\authentication;
|
||||
|
||||
use api\models\AccountIdentity;
|
||||
use api\models\base\ApiForm;
|
||||
use api\validators\TotpValidator;
|
||||
use common\helpers\Error as E;
|
||||
@@ -9,9 +8,6 @@ use api\traits\AccountFinder;
|
||||
use common\models\Account;
|
||||
use Yii;
|
||||
|
||||
/**
|
||||
* @method AccountIdentity|null getAccount()
|
||||
*/
|
||||
class LoginForm extends ApiForm {
|
||||
use AccountFinder;
|
||||
|
||||
@@ -86,12 +82,12 @@ class LoginForm extends ApiForm {
|
||||
}
|
||||
}
|
||||
|
||||
public function getLogin() {
|
||||
public function getLogin(): string {
|
||||
return $this->login;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \api\components\User\LoginResult|bool
|
||||
* @return \api\components\User\AuthenticationResult|bool
|
||||
*/
|
||||
public function login() {
|
||||
if (!$this->validate()) {
|
||||
@@ -104,11 +100,7 @@ class LoginForm extends ApiForm {
|
||||
$account->save();
|
||||
}
|
||||
|
||||
return Yii::$app->user->login($account, $this->rememberMe);
|
||||
}
|
||||
|
||||
protected function getAccountClassName() {
|
||||
return AccountIdentity::class;
|
||||
return Yii::$app->user->createJwtAuthenticationToken($account, $this->rememberMe);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
<?php
|
||||
namespace api\models\authentication;
|
||||
|
||||
use api\models\AccountIdentity;
|
||||
use api\models\base\ApiForm;
|
||||
use api\validators\EmailActivationKeyValidator;
|
||||
use common\helpers\Error as E;
|
||||
@@ -37,7 +36,7 @@ class RecoverPasswordForm extends ApiForm {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \api\components\User\LoginResult|bool
|
||||
* @return \api\components\User\AuthenticationResult|bool
|
||||
* @throws ErrorException
|
||||
*/
|
||||
public function recoverPassword() {
|
||||
@@ -61,7 +60,7 @@ class RecoverPasswordForm extends ApiForm {
|
||||
|
||||
$transaction->commit();
|
||||
|
||||
return Yii::$app->user->login(new AccountIdentity($account->attributes), false);
|
||||
return Yii::$app->user->createJwtAuthenticationToken($account, false);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -32,7 +32,7 @@ class RefreshTokenForm extends ApiForm {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \api\components\User\RenewResult|bool
|
||||
* @return \api\components\User\AuthenticationResult|bool
|
||||
*/
|
||||
public function renew() {
|
||||
if (!$this->validate()) {
|
||||
@@ -42,7 +42,7 @@ class RefreshTokenForm extends ApiForm {
|
||||
/** @var \api\components\User\Component $component */
|
||||
$component = Yii::$app->user;
|
||||
|
||||
return $component->renew($this->getSession());
|
||||
return $component->renewJwtAuthenticationToken($this->getSession());
|
||||
}
|
||||
|
||||
/**
|
||||
|
22
api/models/base/BaseAccountForm.php
Normal file
22
api/models/base/BaseAccountForm.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace api\models\base;
|
||||
|
||||
use common\models\Account;
|
||||
|
||||
class BaseAccountForm extends ApiForm {
|
||||
|
||||
/**
|
||||
* @var Account
|
||||
*/
|
||||
private $account;
|
||||
|
||||
public function __construct(Account $account, array $config = []) {
|
||||
parent::__construct($config);
|
||||
$this->account = $account;
|
||||
}
|
||||
|
||||
public function getAccount(): Account {
|
||||
return $this->account;
|
||||
}
|
||||
|
||||
}
|
@@ -1,35 +0,0 @@
|
||||
<?php
|
||||
namespace api\models\profile;
|
||||
|
||||
use api\models\base\ApiForm;
|
||||
use common\models\Account;
|
||||
use yii\base\ErrorException;
|
||||
use const \common\LATEST_RULES_VERSION;
|
||||
|
||||
class AcceptRulesForm extends ApiForm {
|
||||
|
||||
/**
|
||||
* @var Account
|
||||
*/
|
||||
private $account;
|
||||
|
||||
public function __construct(Account $account, array $config = []) {
|
||||
$this->account = $account;
|
||||
parent::__construct($config);
|
||||
}
|
||||
|
||||
public function agreeWithLatestRules() : bool {
|
||||
$account = $this->getAccount();
|
||||
$account->rules_agreement_version = LATEST_RULES_VERSION;
|
||||
if (!$account->save()) {
|
||||
throw new ErrorException('Cannot set user rules version');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getAccount() : Account {
|
||||
return $this->account;
|
||||
}
|
||||
|
||||
}
|
@@ -1,85 +0,0 @@
|
||||
<?php
|
||||
namespace api\models\profile\ChangeEmail;
|
||||
|
||||
use api\models\base\ApiForm;
|
||||
use api\validators\EmailActivationKeyValidator;
|
||||
use common\helpers\Amqp;
|
||||
use common\models\Account;
|
||||
use common\models\amqp\EmailChanged;
|
||||
use common\models\EmailActivation;
|
||||
use PhpAmqpLib\Message\AMQPMessage;
|
||||
use Yii;
|
||||
use yii\base\ErrorException;
|
||||
|
||||
class ConfirmNewEmailForm extends ApiForm {
|
||||
|
||||
public $key;
|
||||
|
||||
/**
|
||||
* @var Account
|
||||
*/
|
||||
private $account;
|
||||
|
||||
public function rules() {
|
||||
return [
|
||||
['key', EmailActivationKeyValidator::class, 'type' => EmailActivation::TYPE_NEW_EMAIL_CONFIRMATION],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Account
|
||||
*/
|
||||
public function getAccount(): Account {
|
||||
return $this->account;
|
||||
}
|
||||
|
||||
public function changeEmail(): bool {
|
||||
if (!$this->validate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$transaction = Yii::$app->db->beginTransaction();
|
||||
|
||||
/** @var \common\models\confirmations\NewEmailConfirmation $activation */
|
||||
$activation = $this->key;
|
||||
$activation->delete();
|
||||
|
||||
$account = $this->getAccount();
|
||||
$oldEmail = $account->email;
|
||||
$account->email = $activation->newEmail;
|
||||
if (!$account->save()) {
|
||||
throw new ErrorException('Cannot save new account email value');
|
||||
}
|
||||
|
||||
$this->createTask($account->id, $account->email, $oldEmail);
|
||||
|
||||
$transaction->commit();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param integer $accountId
|
||||
* @param string $newEmail
|
||||
* @param string $oldEmail
|
||||
* @throws \PhpAmqpLib\Exception\AMQPExceptionInterface
|
||||
*/
|
||||
public function createTask($accountId, $newEmail, $oldEmail) {
|
||||
$model = new EmailChanged;
|
||||
$model->accountId = $accountId;
|
||||
$model->oldEmail = $oldEmail;
|
||||
$model->newEmail = $newEmail;
|
||||
|
||||
$message = Amqp::getInstance()->prepareMessage($model, [
|
||||
'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
|
||||
]);
|
||||
|
||||
Amqp::sendToEventsExchange('accounts.email-changed', $message);
|
||||
}
|
||||
|
||||
public function __construct(Account $account, array $config = []) {
|
||||
$this->account = $account;
|
||||
parent::__construct($config);
|
||||
}
|
||||
|
||||
}
|
@@ -1,117 +0,0 @@
|
||||
<?php
|
||||
namespace api\models\profile\ChangeEmail;
|
||||
|
||||
use common\emails\EmailHelper;
|
||||
use api\models\base\ApiForm;
|
||||
use api\validators\PasswordRequiredValidator;
|
||||
use common\helpers\Error as E;
|
||||
use common\models\Account;
|
||||
use common\models\confirmations\CurrentEmailConfirmation;
|
||||
use common\models\EmailActivation;
|
||||
use Yii;
|
||||
use yii\base\ErrorException;
|
||||
use yii\base\Exception;
|
||||
|
||||
class InitStateForm extends ApiForm {
|
||||
|
||||
public $email;
|
||||
|
||||
public $password;
|
||||
|
||||
private $account;
|
||||
|
||||
public function __construct(Account $account, array $config = []) {
|
||||
$this->account = $account;
|
||||
$this->email = $account->email;
|
||||
parent::__construct($config);
|
||||
}
|
||||
|
||||
public function getAccount() : Account {
|
||||
return $this->account;
|
||||
}
|
||||
|
||||
public function rules() {
|
||||
return [
|
||||
['email', 'validateFrequency'],
|
||||
['password', PasswordRequiredValidator::class, 'account' => $this->account],
|
||||
];
|
||||
}
|
||||
|
||||
public function validateFrequency($attribute) {
|
||||
if (!$this->hasErrors()) {
|
||||
$emailConfirmation = $this->getEmailActivation();
|
||||
if ($emailConfirmation !== null && !$emailConfirmation->canRepeat()) {
|
||||
$this->addError($attribute, E::RECENTLY_SENT_MESSAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function sendCurrentEmailConfirmation() : bool {
|
||||
if (!$this->validate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$transaction = Yii::$app->db->beginTransaction();
|
||||
try {
|
||||
$this->removeOldCode();
|
||||
$activation = $this->createCode();
|
||||
|
||||
EmailHelper::changeEmailConfirmCurrent($activation);
|
||||
|
||||
$transaction->commit();
|
||||
} catch (Exception $e) {
|
||||
$transaction->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CurrentEmailConfirmation
|
||||
* @throws ErrorException
|
||||
*/
|
||||
public function createCode() : CurrentEmailConfirmation {
|
||||
$account = $this->getAccount();
|
||||
$emailActivation = new CurrentEmailConfirmation();
|
||||
$emailActivation->account_id = $account->id;
|
||||
if (!$emailActivation->save()) {
|
||||
throw new ErrorException('Cannot save email activation model');
|
||||
}
|
||||
|
||||
return $emailActivation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаляет старый ключ активации, если он существует
|
||||
*/
|
||||
public function removeOldCode() {
|
||||
$emailActivation = $this->getEmailActivation();
|
||||
if ($emailActivation === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$emailActivation->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает E-mail активацию, которая использовалась внутри процесса для перехода на следующий шаг.
|
||||
* Метод предназначен для проверки, не слишком ли часто отправляются письма о смене E-mail.
|
||||
* Проверяем тип подтверждения нового E-mail, поскольку при переходе на этот этап, активация предыдущего
|
||||
* шага удаляется.
|
||||
* @return EmailActivation|null
|
||||
* @throws ErrorException
|
||||
*/
|
||||
public function getEmailActivation() {
|
||||
return $this->getAccount()
|
||||
->getEmailActivations()
|
||||
->andWhere([
|
||||
'type' => [
|
||||
EmailActivation::TYPE_CURRENT_EMAIL_CONFIRMATION,
|
||||
EmailActivation::TYPE_NEW_EMAIL_CONFIRMATION,
|
||||
],
|
||||
])
|
||||
->one();
|
||||
}
|
||||
|
||||
}
|
@@ -1,76 +0,0 @@
|
||||
<?php
|
||||
namespace api\models\profile\ChangeEmail;
|
||||
|
||||
use common\emails\EmailHelper;
|
||||
use api\models\base\ApiForm;
|
||||
use api\validators\EmailActivationKeyValidator;
|
||||
use common\models\Account;
|
||||
use common\models\confirmations\NewEmailConfirmation;
|
||||
use common\models\EmailActivation;
|
||||
use common\validators\EmailValidator;
|
||||
use Yii;
|
||||
use yii\base\ErrorException;
|
||||
|
||||
class NewEmailForm extends ApiForm {
|
||||
|
||||
public $key;
|
||||
|
||||
public $email;
|
||||
|
||||
/**
|
||||
* @var Account
|
||||
*/
|
||||
private $account;
|
||||
|
||||
public function rules() {
|
||||
return [
|
||||
['key', EmailActivationKeyValidator::class, 'type' => EmailActivation::TYPE_CURRENT_EMAIL_CONFIRMATION],
|
||||
['email', EmailValidator::class],
|
||||
];
|
||||
}
|
||||
|
||||
public function getAccount(): Account {
|
||||
return $this->account;
|
||||
}
|
||||
|
||||
public function sendNewEmailConfirmation(): bool {
|
||||
if (!$this->validate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$transaction = Yii::$app->db->beginTransaction();
|
||||
|
||||
/** @var \common\models\confirmations\CurrentEmailConfirmation $previousActivation */
|
||||
$previousActivation = $this->key;
|
||||
$previousActivation->delete();
|
||||
|
||||
$activation = $this->createCode();
|
||||
|
||||
EmailHelper::changeEmailConfirmNew($activation);
|
||||
|
||||
$transaction->commit();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return NewEmailConfirmation
|
||||
* @throws ErrorException
|
||||
*/
|
||||
public function createCode() {
|
||||
$emailActivation = new NewEmailConfirmation();
|
||||
$emailActivation->account_id = $this->getAccount()->id;
|
||||
$emailActivation->newEmail = $this->email;
|
||||
if (!$emailActivation->save()) {
|
||||
throw new ErrorException('Cannot save email activation model');
|
||||
}
|
||||
|
||||
return $emailActivation;
|
||||
}
|
||||
|
||||
public function __construct(Account $account, array $config = []) {
|
||||
$this->account = $account;
|
||||
parent::__construct($config);
|
||||
}
|
||||
|
||||
}
|
@@ -1,45 +0,0 @@
|
||||
<?php
|
||||
namespace api\models\profile;
|
||||
|
||||
use api\models\base\ApiForm;
|
||||
use common\models\Account;
|
||||
use common\validators\LanguageValidator;
|
||||
use yii\base\ErrorException;
|
||||
|
||||
class ChangeLanguageForm extends ApiForm {
|
||||
|
||||
public $lang;
|
||||
|
||||
private $account;
|
||||
|
||||
public function __construct(Account $account, array $config = []) {
|
||||
$this->account = $account;
|
||||
parent::__construct($config);
|
||||
}
|
||||
|
||||
public function rules() {
|
||||
return [
|
||||
['lang', 'required'],
|
||||
['lang', LanguageValidator::class],
|
||||
];
|
||||
}
|
||||
|
||||
public function applyLanguage() : bool {
|
||||
if (!$this->validate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$account = $this->getAccount();
|
||||
$account->lang = $this->lang;
|
||||
if (!$account->save()) {
|
||||
throw new ErrorException('Cannot change user language');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getAccount() : Account {
|
||||
return $this->account;
|
||||
}
|
||||
|
||||
}
|
@@ -1,85 +0,0 @@
|
||||
<?php
|
||||
namespace api\models\profile;
|
||||
|
||||
use api\models\base\ApiForm;
|
||||
use api\validators\PasswordRequiredValidator;
|
||||
use common\helpers\Error as E;
|
||||
use common\models\Account;
|
||||
use common\validators\PasswordValidator;
|
||||
use Yii;
|
||||
use yii\base\ErrorException;
|
||||
use yii\helpers\ArrayHelper;
|
||||
|
||||
class ChangePasswordForm extends ApiForm {
|
||||
|
||||
public $newPassword;
|
||||
|
||||
public $newRePassword;
|
||||
|
||||
public $logoutAll;
|
||||
|
||||
public $password;
|
||||
|
||||
/**
|
||||
* @var \common\models\Account
|
||||
*/
|
||||
private $_account;
|
||||
|
||||
public function __construct(Account $account, array $config = []) {
|
||||
$this->_account = $account;
|
||||
parent::__construct($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function rules() {
|
||||
return ArrayHelper::merge(parent::rules(), [
|
||||
['newPassword', 'required', 'message' => E::NEW_PASSWORD_REQUIRED],
|
||||
['newRePassword', 'required', 'message' => E::NEW_RE_PASSWORD_REQUIRED],
|
||||
['newPassword', PasswordValidator::class],
|
||||
['newRePassword', 'validatePasswordAndRePasswordMatch'],
|
||||
['logoutAll', 'boolean'],
|
||||
['password', PasswordRequiredValidator::class, 'account' => $this->_account],
|
||||
]);
|
||||
}
|
||||
|
||||
public function validatePasswordAndRePasswordMatch($attribute) {
|
||||
if (!$this->hasErrors($attribute)) {
|
||||
if ($this->newPassword !== $this->newRePassword) {
|
||||
$this->addError($attribute, E::NEW_RE_PASSWORD_DOES_NOT_MATCH);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
* @throws ErrorException
|
||||
*/
|
||||
public function changePassword() : bool {
|
||||
if (!$this->validate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$transaction = Yii::$app->db->beginTransaction();
|
||||
$account = $this->_account;
|
||||
$account->setPassword($this->newPassword);
|
||||
|
||||
if ($this->logoutAll) {
|
||||
Yii::$app->user->terminateSessions();
|
||||
}
|
||||
|
||||
if (!$account->save()) {
|
||||
throw new ErrorException('Cannot save user model');
|
||||
}
|
||||
|
||||
$transaction->commit();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getAccount() : Account {
|
||||
return $this->_account;
|
||||
}
|
||||
|
||||
}
|
@@ -1,98 +0,0 @@
|
||||
<?php
|
||||
namespace api\models\profile;
|
||||
|
||||
use api\models\base\ApiForm;
|
||||
use api\validators\PasswordRequiredValidator;
|
||||
use common\helpers\Amqp;
|
||||
use common\models\Account;
|
||||
use common\models\amqp\UsernameChanged;
|
||||
use common\models\UsernameHistory;
|
||||
use common\validators\UsernameValidator;
|
||||
use Exception;
|
||||
use PhpAmqpLib\Message\AMQPMessage;
|
||||
use Yii;
|
||||
use yii\base\ErrorException;
|
||||
|
||||
class ChangeUsernameForm extends ApiForm {
|
||||
|
||||
public $username;
|
||||
|
||||
public $password;
|
||||
|
||||
/**
|
||||
* @var Account
|
||||
*/
|
||||
private $account;
|
||||
|
||||
public function __construct(Account $account, array $config = []) {
|
||||
parent::__construct($config);
|
||||
$this->account = $account;
|
||||
}
|
||||
|
||||
public function rules(): array {
|
||||
return [
|
||||
['username', UsernameValidator::class, 'accountCallback' => function() {
|
||||
return $this->account->id;
|
||||
}],
|
||||
['password', PasswordRequiredValidator::class],
|
||||
];
|
||||
}
|
||||
|
||||
public function change(): bool {
|
||||
if (!$this->validate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$account = $this->account;
|
||||
if ($this->username === $account->username) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$transaction = Yii::$app->db->beginTransaction();
|
||||
try {
|
||||
$oldNickname = $account->username;
|
||||
$account->username = $this->username;
|
||||
if (!$account->save()) {
|
||||
throw new ErrorException('Cannot save account model with new username');
|
||||
}
|
||||
|
||||
$usernamesHistory = new UsernameHistory();
|
||||
$usernamesHistory->account_id = $account->id;
|
||||
$usernamesHistory->username = $account->username;
|
||||
if (!$usernamesHistory->save()) {
|
||||
throw new ErrorException('Cannot save username history record');
|
||||
}
|
||||
|
||||
$this->createEventTask($account->id, $account->username, $oldNickname);
|
||||
|
||||
$transaction->commit();
|
||||
} catch (Exception $e) {
|
||||
$transaction->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: вынести это в отдельную сущность, т.к. эта команда используется внутри формы регистрации
|
||||
*
|
||||
* @param integer $accountId
|
||||
* @param string $newNickname
|
||||
* @param string $oldNickname
|
||||
* @throws \PhpAmqpLib\Exception\AMQPExceptionInterface
|
||||
*/
|
||||
public function createEventTask($accountId, $newNickname, $oldNickname) {
|
||||
$model = new UsernameChanged;
|
||||
$model->accountId = $accountId;
|
||||
$model->oldUsername = $oldNickname;
|
||||
$model->newUsername = $newNickname;
|
||||
|
||||
$message = Amqp::getInstance()->prepareMessage($model, [
|
||||
'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
|
||||
]);
|
||||
|
||||
Amqp::sendToEventsExchange('accounts.username-changed', $message);
|
||||
}
|
||||
|
||||
}
|
@@ -1,181 +0,0 @@
|
||||
<?php
|
||||
namespace api\models\profile;
|
||||
|
||||
use api\models\base\ApiForm;
|
||||
use api\validators\TotpValidator;
|
||||
use api\validators\PasswordRequiredValidator;
|
||||
use BaconQrCode\Common\ErrorCorrectionLevel;
|
||||
use BaconQrCode\Encoder\Encoder;
|
||||
use BaconQrCode\Renderer\Color\Rgb;
|
||||
use BaconQrCode\Renderer\Image\Svg;
|
||||
use BaconQrCode\Writer;
|
||||
use common\components\Qr\ElyDecorator;
|
||||
use common\helpers\Error as E;
|
||||
use common\models\Account;
|
||||
use OTPHP\TOTP;
|
||||
use ParagonIE\ConstantTime\Encoding;
|
||||
use Yii;
|
||||
use yii\base\ErrorException;
|
||||
|
||||
class TwoFactorAuthForm extends ApiForm {
|
||||
|
||||
const SCENARIO_ACTIVATE = 'enable';
|
||||
const SCENARIO_DISABLE = 'disable';
|
||||
|
||||
public $totp;
|
||||
|
||||
public $timestamp;
|
||||
|
||||
public $password;
|
||||
|
||||
/**
|
||||
* @var Account
|
||||
*/
|
||||
private $account;
|
||||
|
||||
public function __construct(Account $account, array $config = []) {
|
||||
$this->account = $account;
|
||||
parent::__construct($config);
|
||||
}
|
||||
|
||||
public function rules(): array {
|
||||
$bothScenarios = [self::SCENARIO_ACTIVATE, self::SCENARIO_DISABLE];
|
||||
return [
|
||||
['timestamp', 'integer', 'on' => [self::SCENARIO_ACTIVATE]],
|
||||
['account', 'validateOtpDisabled', 'on' => self::SCENARIO_ACTIVATE],
|
||||
['account', 'validateOtpEnabled', 'on' => self::SCENARIO_DISABLE],
|
||||
['totp', 'required', 'message' => E::TOTP_REQUIRED, 'on' => $bothScenarios],
|
||||
['totp', TotpValidator::class, 'on' => $bothScenarios,
|
||||
'account' => $this->account,
|
||||
'timestamp' => function() {
|
||||
return $this->timestamp;
|
||||
},
|
||||
],
|
||||
['password', PasswordRequiredValidator::class, 'account' => $this->account, 'on' => $bothScenarios],
|
||||
];
|
||||
}
|
||||
|
||||
public function getCredentials(): array {
|
||||
if (empty($this->account->otp_secret)) {
|
||||
$this->setOtpSecret();
|
||||
}
|
||||
|
||||
$provisioningUri = $this->getTotp()->getProvisioningUri();
|
||||
|
||||
return [
|
||||
'qr' => 'data:image/svg+xml,' . trim($this->drawQrCode($provisioningUri)),
|
||||
'uri' => $provisioningUri,
|
||||
'secret' => $this->account->otp_secret,
|
||||
];
|
||||
}
|
||||
|
||||
public function activate(): bool {
|
||||
if ($this->scenario !== self::SCENARIO_ACTIVATE || !$this->validate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$transaction = Yii::$app->db->beginTransaction();
|
||||
|
||||
$account = $this->account;
|
||||
$account->is_otp_enabled = true;
|
||||
if (!$account->save()) {
|
||||
throw new ErrorException('Cannot enable otp for account');
|
||||
}
|
||||
|
||||
Yii::$app->user->terminateSessions();
|
||||
|
||||
$transaction->commit();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function disable(): bool {
|
||||
if ($this->scenario !== self::SCENARIO_DISABLE || !$this->validate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$account = $this->account;
|
||||
$account->is_otp_enabled = false;
|
||||
$account->otp_secret = null;
|
||||
if (!$account->save()) {
|
||||
throw new ErrorException('Cannot disable otp for account');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function validateOtpDisabled($attribute) {
|
||||
if ($this->account->is_otp_enabled) {
|
||||
$this->addError($attribute, E::OTP_ALREADY_ENABLED);
|
||||
}
|
||||
}
|
||||
|
||||
public function validateOtpEnabled($attribute) {
|
||||
if (!$this->account->is_otp_enabled) {
|
||||
$this->addError($attribute, E::OTP_NOT_ENABLED);
|
||||
}
|
||||
}
|
||||
|
||||
public function getAccount(): Account {
|
||||
return $this->account;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return TOTP
|
||||
*/
|
||||
public function getTotp(): TOTP {
|
||||
$totp = TOTP::create($this->account->otp_secret);
|
||||
$totp->setLabel($this->account->email);
|
||||
$totp->setIssuer('Ely.by');
|
||||
|
||||
return $totp;
|
||||
}
|
||||
|
||||
public function drawQrCode(string $content): string {
|
||||
$content = $this->forceMinimalQrContentLength($content);
|
||||
|
||||
$renderer = new Svg();
|
||||
$renderer->setMargin(0);
|
||||
$renderer->setForegroundColor(new Rgb(32, 126, 92));
|
||||
$renderer->addDecorator(new ElyDecorator());
|
||||
|
||||
$writer = new Writer($renderer);
|
||||
|
||||
return $writer->writeString($content, Encoder::DEFAULT_BYTE_MODE_ECODING, ErrorCorrectionLevel::H);
|
||||
}
|
||||
|
||||
/**
|
||||
* otp_secret кодируется в Base32, т.к. после кодирования в результурющей строке нет символов,
|
||||
* которые можно перепутать (1 и l, O и 0, и т.д.). Отрицательной стороной является то, что итоговая
|
||||
* строка составляет 160% от исходной. Поэтому, генерируя исходный приватный ключ, мы должны обеспечить
|
||||
* ему такую длину, чтобы 160% его длины было равно запрошенному значению
|
||||
*
|
||||
* @param int $length
|
||||
* @throws ErrorException
|
||||
*/
|
||||
protected function setOtpSecret(int $length = 24): void {
|
||||
$randomBytesLength = ceil($length / 1.6);
|
||||
$randomBase32 = trim(Encoding::base32EncodeUpper(random_bytes($randomBytesLength)), '=');
|
||||
$this->account->otp_secret = substr($randomBase32, 0, $length);
|
||||
if (!$this->account->save()) {
|
||||
throw new ErrorException('Cannot set account otp_secret');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* В используемой либе для рендеринга QR кода нет возможности указать QR code version.
|
||||
* http://www.qrcode.com/en/about/version.html
|
||||
* По какой-то причине 7 и 8 версии не читаются вовсе, с логотипом или без.
|
||||
* Поэтому нужно иначально привести строку к длинне 9 версии (91), добавляя к концу
|
||||
* строки необходимое количество символов "#". Этот символ используется, т.к. нашим
|
||||
* контентом является ссылка и чтобы не вводить лишние параметры мы помечаем добавочную
|
||||
* часть как хеш часть и все программы для чтения QR кодов продолжают свою работу.
|
||||
*
|
||||
* @param string $content
|
||||
* @return string
|
||||
*/
|
||||
private function forceMinimalQrContentLength(string $content): string {
|
||||
return str_pad($content, 91, '#');
|
||||
}
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user