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:
10
api/modules/accounts/Module.php
Normal file
10
api/modules/accounts/Module.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace api\modules\accounts;
|
||||
|
||||
use yii\base\Module as BaseModule;
|
||||
|
||||
class Module extends BaseModule {
|
||||
|
||||
public $id = 'accounts';
|
||||
|
||||
}
|
12
api/modules/accounts/actions/AcceptRulesAction.php
Normal file
12
api/modules/accounts/actions/AcceptRulesAction.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace api\modules\accounts\actions;
|
||||
|
||||
use api\modules\accounts\models\AcceptRulesForm;
|
||||
|
||||
class AcceptRulesAction extends BaseAccountAction {
|
||||
|
||||
protected function getFormClassName(): string {
|
||||
return AcceptRulesForm::class;
|
||||
}
|
||||
|
||||
}
|
12
api/modules/accounts/actions/BanAccountAction.php
Normal file
12
api/modules/accounts/actions/BanAccountAction.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace api\modules\accounts\actions;
|
||||
|
||||
use api\modules\accounts\models\BanAccountForm;
|
||||
|
||||
class BanAccountAction extends BaseAccountAction {
|
||||
|
||||
protected function getFormClassName(): string {
|
||||
return BanAccountForm::class;
|
||||
}
|
||||
|
||||
}
|
73
api/modules/accounts/actions/BaseAccountAction.php
Normal file
73
api/modules/accounts/actions/BaseAccountAction.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
namespace api\modules\accounts\actions;
|
||||
|
||||
use api\modules\accounts\models\AccountActionForm;
|
||||
use common\models\Account;
|
||||
use Yii;
|
||||
use yii\base\Action;
|
||||
use yii\web\NotFoundHttpException;
|
||||
|
||||
abstract class BaseAccountAction extends Action {
|
||||
|
||||
final public function run(int $id): array {
|
||||
$className = $this->getFormClassName();
|
||||
/** @var AccountActionForm $model */
|
||||
$model = new $className($this->findAccount($id));
|
||||
$model->load($this->getRequestData());
|
||||
if (!$model->performAction()) {
|
||||
return $this->formatFailedResult($model);
|
||||
}
|
||||
|
||||
return $this->formatSuccessResult($model);
|
||||
}
|
||||
|
||||
abstract protected function getFormClassName(): string;
|
||||
|
||||
public function getRequestData(): array {
|
||||
return Yii::$app->request->post();
|
||||
}
|
||||
|
||||
public function getSuccessResultData(AccountActionForm $model): array {
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getFailedResultData(AccountActionForm $model): array {
|
||||
return [];
|
||||
}
|
||||
|
||||
private function formatFailedResult(AccountActionForm $model): array {
|
||||
$response = [
|
||||
'success' => false,
|
||||
'errors' => $model->getFirstErrors(),
|
||||
];
|
||||
|
||||
$data = $this->getFailedResultData($model);
|
||||
if (!empty($data)) {
|
||||
$response['data'] = $data;
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function formatSuccessResult(AccountActionForm $model): array {
|
||||
$response = [
|
||||
'success' => true,
|
||||
];
|
||||
$data = $this->getSuccessResultData($model);
|
||||
if (!empty($data)) {
|
||||
$response['data'] = $data;
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function findAccount(int $id): Account {
|
||||
$account = Account::findOne($id);
|
||||
if ($account === null) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
return $account;
|
||||
}
|
||||
|
||||
}
|
23
api/modules/accounts/actions/ChangeEmailAction.php
Normal file
23
api/modules/accounts/actions/ChangeEmailAction.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
namespace api\modules\accounts\actions;
|
||||
|
||||
use api\modules\accounts\models\AccountActionForm;
|
||||
use api\modules\accounts\models\ChangeEmailForm;
|
||||
|
||||
class ChangeEmailAction extends BaseAccountAction {
|
||||
|
||||
protected function getFormClassName(): string {
|
||||
return ChangeEmailForm::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ChangeEmailForm|AccountActionForm $model
|
||||
* @return array
|
||||
*/
|
||||
public function getSuccessResultData(AccountActionForm $model): array {
|
||||
return [
|
||||
'email' => $model->getAccount()->email,
|
||||
];
|
||||
}
|
||||
|
||||
}
|
12
api/modules/accounts/actions/ChangeLanguageAction.php
Normal file
12
api/modules/accounts/actions/ChangeLanguageAction.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace api\modules\accounts\actions;
|
||||
|
||||
use api\modules\accounts\models\ChangeLanguageForm;
|
||||
|
||||
class ChangeLanguageAction extends BaseAccountAction {
|
||||
|
||||
protected function getFormClassName(): string {
|
||||
return ChangeLanguageForm::class;
|
||||
}
|
||||
|
||||
}
|
12
api/modules/accounts/actions/ChangePasswordAction.php
Normal file
12
api/modules/accounts/actions/ChangePasswordAction.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace api\modules\accounts\actions;
|
||||
|
||||
use api\modules\accounts\models\ChangePasswordForm;
|
||||
|
||||
class ChangePasswordAction extends BaseAccountAction {
|
||||
|
||||
protected function getFormClassName(): string {
|
||||
return ChangePasswordForm::class;
|
||||
}
|
||||
|
||||
}
|
12
api/modules/accounts/actions/ChangeUsernameAction.php
Normal file
12
api/modules/accounts/actions/ChangeUsernameAction.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace api\modules\accounts\actions;
|
||||
|
||||
use api\modules\accounts\models\ChangeUsernameForm;
|
||||
|
||||
class ChangeUsernameAction extends BaseAccountAction {
|
||||
|
||||
protected function getFormClassName(): string {
|
||||
return ChangeUsernameForm::class;
|
||||
}
|
||||
|
||||
}
|
12
api/modules/accounts/actions/DisableTwoFactorAuthAction.php
Normal file
12
api/modules/accounts/actions/DisableTwoFactorAuthAction.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace api\modules\accounts\actions;
|
||||
|
||||
use api\modules\accounts\models\DisableTwoFactorAuthForm;
|
||||
|
||||
class DisableTwoFactorAuthAction extends BaseAccountAction {
|
||||
|
||||
protected function getFormClassName(): string {
|
||||
return DisableTwoFactorAuthForm::class;
|
||||
}
|
||||
|
||||
}
|
32
api/modules/accounts/actions/EmailVerificationAction.php
Normal file
32
api/modules/accounts/actions/EmailVerificationAction.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
namespace api\modules\accounts\actions;
|
||||
|
||||
use api\modules\accounts\models\AccountActionForm;
|
||||
use api\modules\accounts\models\SendEmailVerificationForm;
|
||||
use common\helpers\Error as E;
|
||||
|
||||
class EmailVerificationAction extends BaseAccountAction {
|
||||
|
||||
protected function getFormClassName(): string {
|
||||
return SendEmailVerificationForm::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SendEmailVerificationForm|AccountActionForm $model
|
||||
* @return array
|
||||
*/
|
||||
public function getFailedResultData(AccountActionForm $model): array {
|
||||
$emailError = $model->getFirstError('email');
|
||||
if ($emailError !== E::RECENTLY_SENT_MESSAGE) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$emailActivation = $model->getEmailActivation();
|
||||
|
||||
return [
|
||||
'canRepeatIn' => $emailActivation->canRepeatIn(),
|
||||
'repeatFrequency' => $emailActivation->repeatTimeout,
|
||||
];
|
||||
}
|
||||
|
||||
}
|
12
api/modules/accounts/actions/EnableTwoFactorAuthAction.php
Normal file
12
api/modules/accounts/actions/EnableTwoFactorAuthAction.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace api\modules\accounts\actions;
|
||||
|
||||
use api\modules\accounts\models\EnableTwoFactorAuthForm;
|
||||
|
||||
class EnableTwoFactorAuthAction extends BaseAccountAction {
|
||||
|
||||
protected function getFormClassName(): string {
|
||||
return EnableTwoFactorAuthForm::class;
|
||||
}
|
||||
|
||||
}
|
12
api/modules/accounts/actions/NewEmailVerificationAction.php
Normal file
12
api/modules/accounts/actions/NewEmailVerificationAction.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace api\modules\accounts\actions;
|
||||
|
||||
use api\modules\accounts\models\SendNewEmailVerificationForm;
|
||||
|
||||
class NewEmailVerificationAction extends BaseAccountAction {
|
||||
|
||||
protected function getFormClassName(): string {
|
||||
return SendNewEmailVerificationForm::class;
|
||||
}
|
||||
|
||||
}
|
12
api/modules/accounts/actions/PardonAccountAction.php
Normal file
12
api/modules/accounts/actions/PardonAccountAction.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace api\modules\accounts\actions;
|
||||
|
||||
use api\modules\accounts\models\PardonAccountForm;
|
||||
|
||||
class PardonAccountAction extends BaseAccountAction {
|
||||
|
||||
protected function getFormClassName(): string {
|
||||
return PardonAccountForm::class;
|
||||
}
|
||||
|
||||
}
|
133
api/modules/accounts/controllers/DefaultController.php
Normal file
133
api/modules/accounts/controllers/DefaultController.php
Normal file
@ -0,0 +1,133 @@
|
||||
<?php
|
||||
namespace api\modules\accounts\controllers;
|
||||
|
||||
use api\controllers\Controller;
|
||||
use api\modules\accounts\actions;
|
||||
use api\modules\accounts\models\AccountInfo;
|
||||
use api\modules\accounts\models\TwoFactorAuthInfo;
|
||||
use common\models\Account;
|
||||
use common\rbac\Permissions as P;
|
||||
use Yii;
|
||||
use yii\filters\AccessControl;
|
||||
use yii\helpers\ArrayHelper;
|
||||
use yii\web\NotFoundHttpException;
|
||||
|
||||
class DefaultController extends Controller {
|
||||
|
||||
public function behaviors(): array {
|
||||
$paramsCallback = function() {
|
||||
return [
|
||||
'accountId' => Yii::$app->request->get('id'),
|
||||
];
|
||||
};
|
||||
|
||||
return ArrayHelper::merge(Controller::behaviors(), [
|
||||
'access' => [
|
||||
'class' => AccessControl::class,
|
||||
'rules' => [
|
||||
[
|
||||
'allow' => true,
|
||||
'actions' => ['get'],
|
||||
'roles' => [P::OBTAIN_ACCOUNT_INFO],
|
||||
'roleParams' => function() use ($paramsCallback) {
|
||||
return array_merge($paramsCallback(), [
|
||||
'optionalRules' => true,
|
||||
]);
|
||||
},
|
||||
],
|
||||
[
|
||||
'allow' => true,
|
||||
'actions' => ['username'],
|
||||
'roles' => [P::CHANGE_ACCOUNT_USERNAME],
|
||||
'roleParams' => $paramsCallback,
|
||||
],
|
||||
[
|
||||
'allow' => true,
|
||||
'actions' => ['password'],
|
||||
'roles' => [P::CHANGE_ACCOUNT_PASSWORD],
|
||||
'roleParams' => $paramsCallback,
|
||||
],
|
||||
[
|
||||
'allow' => true,
|
||||
'actions' => ['language'],
|
||||
'roles' => [P::CHANGE_ACCOUNT_LANGUAGE],
|
||||
'roleParams' => $paramsCallback,
|
||||
],
|
||||
[
|
||||
'allow' => true,
|
||||
'actions' => [
|
||||
'email',
|
||||
'email-verification',
|
||||
'new-email-verification',
|
||||
],
|
||||
'roles' => [P::CHANGE_ACCOUNT_EMAIL],
|
||||
'roleParams' => $paramsCallback,
|
||||
],
|
||||
[
|
||||
'allow' => true,
|
||||
'actions' => ['rules'],
|
||||
'roles' => [P::ACCEPT_NEW_PROJECT_RULES],
|
||||
'roleParams' => function() use ($paramsCallback) {
|
||||
return array_merge($paramsCallback(), [
|
||||
'optionalRules' => true,
|
||||
]);
|
||||
},
|
||||
],
|
||||
[
|
||||
'allow' => true,
|
||||
'actions' => [
|
||||
'get-two-factor-auth-credentials',
|
||||
'enable-two-factor-auth',
|
||||
'disable-two-factor-auth',
|
||||
],
|
||||
'roles' => [P::MANAGE_TWO_FACTOR_AUTH],
|
||||
'roleParams' => $paramsCallback,
|
||||
],
|
||||
[
|
||||
'allow' => true,
|
||||
'actions' => [
|
||||
'ban',
|
||||
'pardon',
|
||||
],
|
||||
'roles' => [P::BLOCK_ACCOUNT],
|
||||
'roleParams' => $paramsCallback,
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function actions(): array {
|
||||
return [
|
||||
'username' => actions\ChangeUsernameAction::class,
|
||||
'password' => actions\ChangePasswordAction::class,
|
||||
'language' => actions\ChangeLanguageAction::class,
|
||||
'email' => actions\ChangeEmailAction::class,
|
||||
'email-verification' => actions\EmailVerificationAction::class,
|
||||
'new-email-verification' => actions\NewEmailVerificationAction::class,
|
||||
'rules' => actions\AcceptRulesAction::class,
|
||||
'enable-two-factor-auth' => actions\EnableTwoFactorAuthAction::class,
|
||||
'disable-two-factor-auth' => actions\DisableTwoFactorAuthAction::class,
|
||||
'ban' => actions\BanAccountAction::class,
|
||||
'pardon' => actions\PardonAccountAction::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function actionGet(int $id): array {
|
||||
return (new AccountInfo($this->findAccount($id)))->info();
|
||||
}
|
||||
|
||||
public function actionGetTwoFactorAuthCredentials(int $id): array {
|
||||
return (new TwoFactorAuthInfo($this->findAccount($id)))->getCredentials();
|
||||
}
|
||||
|
||||
private function findAccount(int $id): Account {
|
||||
$account = Account::findOne($id);
|
||||
if ($account === null) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
return $account;
|
||||
}
|
||||
|
||||
}
|
19
api/modules/accounts/models/AcceptRulesForm.php
Normal file
19
api/modules/accounts/models/AcceptRulesForm.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
namespace api\modules\accounts\models;
|
||||
|
||||
use yii\base\ErrorException;
|
||||
use const \common\LATEST_RULES_VERSION;
|
||||
|
||||
class AcceptRulesForm extends AccountActionForm {
|
||||
|
||||
public function performAction(): bool {
|
||||
$account = $this->getAccount();
|
||||
$account->rules_agreement_version = LATEST_RULES_VERSION;
|
||||
if (!$account->save()) {
|
||||
throw new ErrorException('Cannot set user rules version');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
10
api/modules/accounts/models/AccountActionForm.php
Normal file
10
api/modules/accounts/models/AccountActionForm.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace api\modules\accounts\models;
|
||||
|
||||
use api\models\base\BaseAccountForm;
|
||||
|
||||
abstract class AccountActionForm extends BaseAccountForm {
|
||||
|
||||
abstract public function performAction(): bool;
|
||||
|
||||
}
|
54
api/modules/accounts/models/AccountInfo.php
Normal file
54
api/modules/accounts/models/AccountInfo.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
namespace api\modules\accounts\models;
|
||||
|
||||
use api\models\base\BaseAccountForm;
|
||||
use common\models\Account;
|
||||
use common\rbac\Permissions as P;
|
||||
use yii\di\Instance;
|
||||
use yii\web\User;
|
||||
|
||||
class AccountInfo extends BaseAccountForm {
|
||||
|
||||
/**
|
||||
* @var User|string
|
||||
*/
|
||||
public $user = 'user';
|
||||
|
||||
public function init() {
|
||||
parent::init();
|
||||
$this->user = Instance::ensure($this->user, User::class);
|
||||
}
|
||||
|
||||
public function info(): array {
|
||||
$account = $this->getAccount();
|
||||
|
||||
$response = [
|
||||
'id' => $account->id,
|
||||
'uuid' => $account->uuid,
|
||||
'username' => $account->username,
|
||||
'isOtpEnabled' => (bool)$account->is_otp_enabled,
|
||||
'registeredAt' => $account->created_at,
|
||||
'lang' => $account->lang,
|
||||
'elyProfileLink' => $account->getProfileLink(),
|
||||
];
|
||||
|
||||
$authManagerParams = [
|
||||
'accountId' => $account->id,
|
||||
'optionalRules' => true,
|
||||
];
|
||||
|
||||
if ($this->user->can(P::OBTAIN_ACCOUNT_EMAIL, $authManagerParams)) {
|
||||
$response['email'] = $account->email;
|
||||
}
|
||||
|
||||
if ($this->user->can(P::OBTAIN_EXTENDED_ACCOUNT_INFO, $authManagerParams)) {
|
||||
$response['isActive'] = $account->status === Account::STATUS_ACTIVE;
|
||||
$response['passwordChangedAt'] = $account->password_changed_at;
|
||||
$response['hasMojangUsernameCollision'] = $account->hasMojangUsernameCollision();
|
||||
$response['shouldAcceptRules'] = !$account->isAgreedWithActualRules();
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
<?php
|
||||
namespace api\modules\internal\models;
|
||||
namespace api\modules\accounts\models;
|
||||
|
||||
use api\models\base\ApiForm;
|
||||
use api\modules\internal\helpers\Error as E;
|
||||
use common\helpers\Amqp;
|
||||
use common\models\Account;
|
||||
@ -10,7 +9,7 @@ use PhpAmqpLib\Message\AMQPMessage;
|
||||
use Yii;
|
||||
use yii\base\ErrorException;
|
||||
|
||||
class BanForm extends ApiForm {
|
||||
class BanAccountForm extends AccountActionForm {
|
||||
|
||||
public const DURATION_FOREVER = -1;
|
||||
|
||||
@ -31,11 +30,6 @@ class BanForm extends ApiForm {
|
||||
*/
|
||||
public $message = '';
|
||||
|
||||
/**
|
||||
* @var Account
|
||||
*/
|
||||
private $account;
|
||||
|
||||
public function rules(): array {
|
||||
return [
|
||||
[['duration'], 'integer', 'min' => self::DURATION_FOREVER],
|
||||
@ -44,24 +38,20 @@ class BanForm extends ApiForm {
|
||||
];
|
||||
}
|
||||
|
||||
public function getAccount(): Account {
|
||||
return $this->account;
|
||||
}
|
||||
|
||||
public function validateAccountActivity() {
|
||||
if ($this->account->status === Account::STATUS_BANNED) {
|
||||
if ($this->getAccount()->status === Account::STATUS_BANNED) {
|
||||
$this->addError('account', E::ACCOUNT_ALREADY_BANNED);
|
||||
}
|
||||
}
|
||||
|
||||
public function ban(): bool {
|
||||
public function performAction(): bool {
|
||||
if (!$this->validate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$transaction = Yii::$app->db->beginTransaction();
|
||||
|
||||
$account = $this->account;
|
||||
$account = $this->getAccount();
|
||||
$account->status = Account::STATUS_BANNED;
|
||||
if (!$account->save()) {
|
||||
throw new ErrorException('Cannot ban account');
|
||||
@ -76,7 +66,7 @@ class BanForm extends ApiForm {
|
||||
|
||||
public function createTask(): void {
|
||||
$model = new AccountBanned();
|
||||
$model->accountId = $this->account->id;
|
||||
$model->accountId = $this->getAccount()->id;
|
||||
$model->duration = $this->duration;
|
||||
$model->message = $this->message;
|
||||
|
||||
@ -87,9 +77,4 @@ class BanForm extends ApiForm {
|
||||
Amqp::sendToEventsExchange('accounts.account-banned', $message);
|
||||
}
|
||||
|
||||
public function __construct(Account $account, array $config = []) {
|
||||
$this->account = $account;
|
||||
parent::__construct($config);
|
||||
}
|
||||
|
||||
}
|
60
api/modules/accounts/models/ChangeEmailForm.php
Normal file
60
api/modules/accounts/models/ChangeEmailForm.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
namespace api\modules\accounts\models;
|
||||
|
||||
use api\validators\EmailActivationKeyValidator;
|
||||
use common\helpers\Amqp;
|
||||
use common\models\amqp\EmailChanged;
|
||||
use common\models\EmailActivation;
|
||||
use PhpAmqpLib\Message\AMQPMessage;
|
||||
use Yii;
|
||||
use yii\base\ErrorException;
|
||||
|
||||
class ChangeEmailForm extends AccountActionForm {
|
||||
|
||||
public $key;
|
||||
|
||||
public function rules(): array {
|
||||
return [
|
||||
['key', EmailActivationKeyValidator::class, 'type' => EmailActivation::TYPE_NEW_EMAIL_CONFIRMATION],
|
||||
];
|
||||
}
|
||||
|
||||
public function performAction(): 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;
|
||||
}
|
||||
|
||||
public function createTask(int $accountId, string $newEmail, string $oldEmail): void {
|
||||
$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);
|
||||
}
|
||||
|
||||
}
|
32
api/modules/accounts/models/ChangeLanguageForm.php
Normal file
32
api/modules/accounts/models/ChangeLanguageForm.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
namespace api\modules\accounts\models;
|
||||
|
||||
use api\exceptions\ThisShouldNotHappenException;
|
||||
use common\validators\LanguageValidator;
|
||||
|
||||
class ChangeLanguageForm extends AccountActionForm {
|
||||
|
||||
public $lang;
|
||||
|
||||
public function rules(): array {
|
||||
return [
|
||||
['lang', 'required'],
|
||||
['lang', LanguageValidator::class],
|
||||
];
|
||||
}
|
||||
|
||||
public function performAction(): bool {
|
||||
if (!$this->validate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$account = $this->getAccount();
|
||||
$account->lang = $this->lang;
|
||||
if (!$account->save()) {
|
||||
throw new ThisShouldNotHappenException('Cannot change user language');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
69
api/modules/accounts/models/ChangePasswordForm.php
Normal file
69
api/modules/accounts/models/ChangePasswordForm.php
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
namespace api\modules\accounts\models;
|
||||
|
||||
use api\components\User\Component;
|
||||
use api\exceptions\ThisShouldNotHappenException;
|
||||
use api\validators\PasswordRequiredValidator;
|
||||
use common\helpers\Error as E;
|
||||
use common\validators\PasswordValidator;
|
||||
use Yii;
|
||||
use yii\helpers\ArrayHelper;
|
||||
|
||||
class ChangePasswordForm extends AccountActionForm {
|
||||
|
||||
public $newPassword;
|
||||
|
||||
public $newRePassword;
|
||||
|
||||
public $logoutAll;
|
||||
|
||||
public $password;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function rules(): array {
|
||||
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->getAccount(), 'when' => function() {
|
||||
return !$this->hasErrors();
|
||||
}],
|
||||
]);
|
||||
}
|
||||
|
||||
public function validatePasswordAndRePasswordMatch($attribute): void {
|
||||
if (!$this->hasErrors($attribute)) {
|
||||
if ($this->newPassword !== $this->newRePassword) {
|
||||
$this->addError($attribute, E::NEW_RE_PASSWORD_DOES_NOT_MATCH);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function performAction(): bool {
|
||||
if (!$this->validate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$transaction = Yii::$app->db->beginTransaction();
|
||||
|
||||
$account = $this->getAccount();
|
||||
$account->setPassword($this->newPassword);
|
||||
|
||||
if ($this->logoutAll) {
|
||||
Yii::$app->user->terminateSessions($account, Component::KEEP_CURRENT_SESSION);
|
||||
}
|
||||
|
||||
if (!$account->save()) {
|
||||
throw new ThisShouldNotHappenException('Cannot save user model');
|
||||
}
|
||||
|
||||
$transaction->commit();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
83
api/modules/accounts/models/ChangeUsernameForm.php
Normal file
83
api/modules/accounts/models/ChangeUsernameForm.php
Normal file
@ -0,0 +1,83 @@
|
||||
<?php
|
||||
namespace api\modules\accounts\models;
|
||||
|
||||
use api\exceptions\ThisShouldNotHappenException;
|
||||
use api\validators\PasswordRequiredValidator;
|
||||
use common\helpers\Amqp;
|
||||
use common\models\amqp\UsernameChanged;
|
||||
use common\models\UsernameHistory;
|
||||
use common\validators\UsernameValidator;
|
||||
use PhpAmqpLib\Message\AMQPMessage;
|
||||
use Yii;
|
||||
use yii\base\ErrorException;
|
||||
|
||||
class ChangeUsernameForm extends AccountActionForm {
|
||||
|
||||
public $username;
|
||||
|
||||
public $password;
|
||||
|
||||
public function rules(): array {
|
||||
return [
|
||||
['username', UsernameValidator::class, 'accountCallback' => function() {
|
||||
return $this->getAccount()->id;
|
||||
}],
|
||||
['password', PasswordRequiredValidator::class, 'account' => $this->getAccount()],
|
||||
];
|
||||
}
|
||||
|
||||
public function performAction(): bool {
|
||||
if (!$this->validate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$account = $this->getAccount();
|
||||
if ($this->username === $account->username) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$transaction = Yii::$app->db->beginTransaction();
|
||||
|
||||
$oldNickname = $account->username;
|
||||
$account->username = $this->username;
|
||||
if (!$account->save()) {
|
||||
throw new ThisShouldNotHappenException('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();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: вынести это в отдельную сущность, т.к. эта команда используется внутри формы регистрации
|
||||
*
|
||||
* @param integer $accountId
|
||||
* @param string $newNickname
|
||||
* @param string $oldNickname
|
||||
*
|
||||
* @throws \PhpAmqpLib\Exception\AMQPExceptionInterface|\yii\base\Exception
|
||||
*/
|
||||
public function createEventTask($accountId, $newNickname, $oldNickname): void {
|
||||
$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);
|
||||
}
|
||||
|
||||
}
|
45
api/modules/accounts/models/DisableTwoFactorAuthForm.php
Normal file
45
api/modules/accounts/models/DisableTwoFactorAuthForm.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
namespace api\modules\accounts\models;
|
||||
|
||||
use api\exceptions\ThisShouldNotHappenException;
|
||||
use api\validators\PasswordRequiredValidator;
|
||||
use api\validators\TotpValidator;
|
||||
use common\helpers\Error as E;
|
||||
|
||||
class DisableTwoFactorAuthForm extends AccountActionForm {
|
||||
|
||||
public $totp;
|
||||
|
||||
public $password;
|
||||
|
||||
public function rules(): array {
|
||||
return [
|
||||
['account', 'validateOtpEnabled'],
|
||||
['totp', 'required', 'message' => E::TOTP_REQUIRED],
|
||||
['totp', TotpValidator::class, 'account' => $this->getAccount()],
|
||||
['password', PasswordRequiredValidator::class, 'account' => $this->getAccount()],
|
||||
];
|
||||
}
|
||||
|
||||
public function performAction(): bool {
|
||||
if (!$this->validate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$account = $this->getAccount();
|
||||
$account->is_otp_enabled = false;
|
||||
$account->otp_secret = null;
|
||||
if (!$account->save()) {
|
||||
throw new ThisShouldNotHappenException('Cannot disable otp for account');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function validateOtpEnabled($attribute): void {
|
||||
if (!$this->getAccount()->is_otp_enabled) {
|
||||
$this->addError($attribute, E::OTP_NOT_ENABLED);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
52
api/modules/accounts/models/EnableTwoFactorAuthForm.php
Normal file
52
api/modules/accounts/models/EnableTwoFactorAuthForm.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
namespace api\modules\accounts\models;
|
||||
|
||||
use api\components\User\Component;
|
||||
use api\exceptions\ThisShouldNotHappenException;
|
||||
use api\validators\PasswordRequiredValidator;
|
||||
use api\validators\TotpValidator;
|
||||
use common\helpers\Error as E;
|
||||
use Yii;
|
||||
|
||||
class EnableTwoFactorAuthForm extends AccountActionForm {
|
||||
|
||||
public $totp;
|
||||
|
||||
public $password;
|
||||
|
||||
public function rules(): array {
|
||||
return [
|
||||
['account', 'validateOtpDisabled'],
|
||||
['totp', 'required', 'message' => E::TOTP_REQUIRED],
|
||||
['totp', TotpValidator::class, 'account' => $this->getAccount()],
|
||||
['password', PasswordRequiredValidator::class, 'account' => $this->getAccount()],
|
||||
];
|
||||
}
|
||||
|
||||
public function performAction(): bool {
|
||||
if (!$this->validate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$transaction = Yii::$app->db->beginTransaction();
|
||||
|
||||
$account = $this->getAccount();
|
||||
$account->is_otp_enabled = true;
|
||||
if (!$account->save()) {
|
||||
throw new ThisShouldNotHappenException('Cannot enable otp for account');
|
||||
}
|
||||
|
||||
Yii::$app->user->terminateSessions($account, Component::KEEP_CURRENT_SESSION);
|
||||
|
||||
$transaction->commit();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function validateOtpDisabled($attribute): void {
|
||||
if ($this->getAccount()->is_otp_enabled) {
|
||||
$this->addError($attribute, E::OTP_ALREADY_ENABLED);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
<?php
|
||||
namespace api\modules\internal\models;
|
||||
namespace api\modules\accounts\models;
|
||||
|
||||
use api\models\base\ApiForm;
|
||||
use api\modules\internal\helpers\Error as E;
|
||||
use common\helpers\Amqp;
|
||||
use common\models\Account;
|
||||
@ -10,12 +9,7 @@ use PhpAmqpLib\Message\AMQPMessage;
|
||||
use Yii;
|
||||
use yii\base\ErrorException;
|
||||
|
||||
class PardonForm extends ApiForm {
|
||||
|
||||
/**
|
||||
* @var Account
|
||||
*/
|
||||
private $account;
|
||||
class PardonAccountForm extends AccountActionForm {
|
||||
|
||||
public function rules(): array {
|
||||
return [
|
||||
@ -23,24 +17,20 @@ class PardonForm extends ApiForm {
|
||||
];
|
||||
}
|
||||
|
||||
public function getAccount(): Account {
|
||||
return $this->account;
|
||||
}
|
||||
|
||||
public function validateAccountBanned(): void {
|
||||
if ($this->account->status !== Account::STATUS_BANNED) {
|
||||
if ($this->getAccount()->status !== Account::STATUS_BANNED) {
|
||||
$this->addError('account', E::ACCOUNT_NOT_BANNED);
|
||||
}
|
||||
}
|
||||
|
||||
public function pardon(): bool {
|
||||
public function performAction(): bool {
|
||||
if (!$this->validate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$transaction = Yii::$app->db->beginTransaction();
|
||||
|
||||
$account = $this->account;
|
||||
$account = $this->getAccount();
|
||||
$account->status = Account::STATUS_ACTIVE;
|
||||
if (!$account->save()) {
|
||||
throw new ErrorException('Cannot pardon account');
|
||||
@ -55,7 +45,7 @@ class PardonForm extends ApiForm {
|
||||
|
||||
public function createTask(): void {
|
||||
$model = new AccountPardoned();
|
||||
$model->accountId = $this->account->id;
|
||||
$model->accountId = $this->getAccount()->id;
|
||||
|
||||
$message = Amqp::getInstance()->prepareMessage($model, [
|
||||
'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
|
||||
@ -64,9 +54,4 @@ class PardonForm extends ApiForm {
|
||||
Amqp::sendToEventsExchange('accounts.account-pardoned', $message);
|
||||
}
|
||||
|
||||
public function __construct(Account $account, array $config = []) {
|
||||
$this->account = $account;
|
||||
parent::__construct($config);
|
||||
}
|
||||
|
||||
}
|
92
api/modules/accounts/models/SendEmailVerificationForm.php
Normal file
92
api/modules/accounts/models/SendEmailVerificationForm.php
Normal file
@ -0,0 +1,92 @@
|
||||
<?php
|
||||
namespace api\modules\accounts\models;
|
||||
|
||||
use api\exceptions\ThisShouldNotHappenException;
|
||||
use common\emails\EmailHelper;
|
||||
use api\validators\PasswordRequiredValidator;
|
||||
use common\helpers\Error as E;
|
||||
use common\models\confirmations\CurrentEmailConfirmation;
|
||||
use common\models\EmailActivation;
|
||||
use Yii;
|
||||
|
||||
class SendEmailVerificationForm extends AccountActionForm {
|
||||
|
||||
public $password;
|
||||
|
||||
/**
|
||||
* @var null meta-поле, чтобы заставить yii валидировать и публиковать ошибки, связанные с отправленными email
|
||||
*/
|
||||
public $email;
|
||||
|
||||
public function rules(): array {
|
||||
return [
|
||||
['email', 'validateFrequency', 'skipOnEmpty' => false],
|
||||
['password', PasswordRequiredValidator::class, 'account' => $this->getAccount()],
|
||||
];
|
||||
}
|
||||
|
||||
public function validateFrequency($attribute): void {
|
||||
if (!$this->hasErrors()) {
|
||||
$emailConfirmation = $this->getEmailActivation();
|
||||
if ($emailConfirmation !== null && !$emailConfirmation->canRepeat()) {
|
||||
$this->addError($attribute, E::RECENTLY_SENT_MESSAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function performAction(): bool {
|
||||
if (!$this->validate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$transaction = Yii::$app->db->beginTransaction();
|
||||
|
||||
$this->removeOldCode();
|
||||
$activation = $this->createCode();
|
||||
|
||||
EmailHelper::changeEmailConfirmCurrent($activation);
|
||||
|
||||
$transaction->commit();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function createCode(): CurrentEmailConfirmation {
|
||||
$account = $this->getAccount();
|
||||
$emailActivation = new CurrentEmailConfirmation();
|
||||
$emailActivation->account_id = $account->id;
|
||||
if (!$emailActivation->save()) {
|
||||
throw new ThisShouldNotHappenException('Cannot save email activation model');
|
||||
}
|
||||
|
||||
return $emailActivation;
|
||||
}
|
||||
|
||||
public function removeOldCode(): void {
|
||||
$emailActivation = $this->getEmailActivation();
|
||||
if ($emailActivation === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$emailActivation->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает E-mail активацию, которая использовалась внутри процесса для перехода на следующий шаг.
|
||||
* Метод предназначен для проверки, не слишком ли часто отправляются письма о смене E-mail.
|
||||
* Проверяем тип подтверждения нового E-mail, поскольку при переходе на этот этап, активация предыдущего
|
||||
* шага удаляется.
|
||||
*/
|
||||
public function getEmailActivation(): ?EmailActivation {
|
||||
return $this->getAccount()
|
||||
->getEmailActivations()
|
||||
->andWhere([
|
||||
'type' => [
|
||||
EmailActivation::TYPE_CURRENT_EMAIL_CONFIRMATION,
|
||||
EmailActivation::TYPE_NEW_EMAIL_CONFIRMATION,
|
||||
],
|
||||
])
|
||||
->one();
|
||||
}
|
||||
|
||||
}
|
56
api/modules/accounts/models/SendNewEmailVerificationForm.php
Normal file
56
api/modules/accounts/models/SendNewEmailVerificationForm.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
namespace api\modules\accounts\models;
|
||||
|
||||
use api\exceptions\ThisShouldNotHappenException;
|
||||
use common\emails\EmailHelper;
|
||||
use api\validators\EmailActivationKeyValidator;
|
||||
use common\models\confirmations\NewEmailConfirmation;
|
||||
use common\models\EmailActivation;
|
||||
use common\validators\EmailValidator;
|
||||
use Yii;
|
||||
|
||||
class SendNewEmailVerificationForm extends AccountActionForm {
|
||||
|
||||
public $key;
|
||||
|
||||
public $email;
|
||||
|
||||
public function rules(): array {
|
||||
return [
|
||||
['key', EmailActivationKeyValidator::class, 'type' => EmailActivation::TYPE_CURRENT_EMAIL_CONFIRMATION],
|
||||
['email', EmailValidator::class],
|
||||
];
|
||||
}
|
||||
|
||||
public function performAction(): 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;
|
||||
}
|
||||
|
||||
public function createCode(): NewEmailConfirmation {
|
||||
$emailActivation = new NewEmailConfirmation();
|
||||
$emailActivation->account_id = $this->getAccount()->id;
|
||||
$emailActivation->newEmail = $this->email;
|
||||
if (!$emailActivation->save()) {
|
||||
throw new ThisShouldNotHappenException('Cannot save email activation model');
|
||||
}
|
||||
|
||||
return $emailActivation;
|
||||
}
|
||||
|
||||
}
|
20
api/modules/accounts/models/TotpHelper.php
Normal file
20
api/modules/accounts/models/TotpHelper.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
namespace api\modules\accounts\models;
|
||||
|
||||
use common\models\Account;
|
||||
use OTPHP\TOTP;
|
||||
|
||||
trait TotpHelper {
|
||||
|
||||
protected function getTotp(): TOTP {
|
||||
$account = $this->getAccount();
|
||||
$totp = TOTP::create($account->otp_secret);
|
||||
$totp->setLabel($account->email);
|
||||
$totp->setIssuer('Ely.by');
|
||||
|
||||
return $totp;
|
||||
}
|
||||
|
||||
abstract public function getAccount(): Account;
|
||||
|
||||
}
|
80
api/modules/accounts/models/TwoFactorAuthInfo.php
Normal file
80
api/modules/accounts/models/TwoFactorAuthInfo.php
Normal file
@ -0,0 +1,80 @@
|
||||
<?php
|
||||
namespace api\modules\accounts\models;
|
||||
|
||||
use api\exceptions\ThisShouldNotHappenException;
|
||||
use api\models\base\BaseAccountForm;
|
||||
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 ParagonIE\ConstantTime\Base32;
|
||||
|
||||
class TwoFactorAuthInfo extends BaseAccountForm {
|
||||
use TotpHelper;
|
||||
|
||||
public function getCredentials(): array {
|
||||
if (empty($this->getAccount()->otp_secret)) {
|
||||
$this->setOtpSecret();
|
||||
}
|
||||
|
||||
$provisioningUri = $this->getTotp()->getProvisioningUri();
|
||||
|
||||
return [
|
||||
'qr' => 'data:image/svg+xml,' . trim($this->drawQrCode($provisioningUri)),
|
||||
'uri' => $provisioningUri,
|
||||
'secret' => $this->getAccount()->otp_secret,
|
||||
];
|
||||
}
|
||||
|
||||
private function drawQrCode(string $content): string {
|
||||
$content = $this->forceMinimalQrContentLength($content);
|
||||
|
||||
$renderer = new Svg();
|
||||
$renderer->setForegroundColor(new Rgb(32, 126, 92));
|
||||
$renderer->setMargin(0);
|
||||
$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 ThisShouldNotHappenException
|
||||
*/
|
||||
private function setOtpSecret(int $length = 24): void {
|
||||
$account = $this->getAccount();
|
||||
$randomBytesLength = ceil($length / 1.6);
|
||||
$randomBase32 = trim(Base32::encodeUpper(random_bytes($randomBytesLength)), '=');
|
||||
$account->otp_secret = substr($randomBase32, 0, $length);
|
||||
if (!$account->save()) {
|
||||
throw new ThisShouldNotHappenException('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, '#');
|
||||
}
|
||||
|
||||
}
|
@ -7,7 +7,7 @@ use Yii;
|
||||
|
||||
class AuthenticationController extends Controller {
|
||||
|
||||
public function behaviors() {
|
||||
public function behaviors(): array {
|
||||
$behaviors = parent::behaviors();
|
||||
unset($behaviors['authenticator']);
|
||||
|
||||
|
@ -1,58 +1,42 @@
|
||||
<?php
|
||||
namespace api\modules\internal\controllers;
|
||||
|
||||
use api\components\ApiUser\AccessControl;
|
||||
use api\controllers\Controller;
|
||||
use api\modules\internal\models\BanForm;
|
||||
use api\modules\internal\models\PardonForm;
|
||||
use common\models\Account;
|
||||
use common\models\OauthScope as S;
|
||||
use Yii;
|
||||
use common\rbac\Permissions as P;
|
||||
use yii\filters\AccessControl;
|
||||
use yii\helpers\ArrayHelper;
|
||||
use yii\web\BadRequestHttpException;
|
||||
use yii\web\NotFoundHttpException;
|
||||
|
||||
class AccountsController extends Controller {
|
||||
|
||||
public function behaviors() {
|
||||
public function behaviors(): array {
|
||||
return ArrayHelper::merge(parent::behaviors(), [
|
||||
'authenticator' => [
|
||||
'user' => Yii::$app->apiUser,
|
||||
],
|
||||
'access' => [
|
||||
'class' => AccessControl::class,
|
||||
'rules' => [
|
||||
[
|
||||
'actions' => ['ban'],
|
||||
'allow' => true,
|
||||
'roles' => [S::ACCOUNT_BLOCK],
|
||||
],
|
||||
[
|
||||
'actions' => ['info'],
|
||||
'allow' => true,
|
||||
'roles' => [S::INTERNAL_ACCOUNT_INFO],
|
||||
'roles' => [P::OBTAIN_EXTENDED_ACCOUNT_INFO],
|
||||
'roleParams' => function() {
|
||||
return [
|
||||
'accountId' => 0,
|
||||
];
|
||||
},
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function verbs() {
|
||||
public function verbs(): array {
|
||||
return [
|
||||
'ban' => ['POST', 'DELETE'],
|
||||
'info' => ['GET'],
|
||||
];
|
||||
}
|
||||
|
||||
public function actionBan(int $accountId) {
|
||||
$account = $this->findAccount($accountId);
|
||||
if (Yii::$app->request->isPost) {
|
||||
return $this->banAccount($account);
|
||||
} else {
|
||||
return $this->pardonAccount($account);
|
||||
}
|
||||
}
|
||||
|
||||
public function actionInfo(int $id = null, string $username = null, string $uuid = null) {
|
||||
if ($id !== null) {
|
||||
$account = Account::findOne($id);
|
||||
@ -76,43 +60,4 @@ class AccountsController extends Controller {
|
||||
];
|
||||
}
|
||||
|
||||
private function banAccount(Account $account) {
|
||||
$model = new BanForm($account);
|
||||
$model->load(Yii::$app->request->post());
|
||||
if (!$model->ban()) {
|
||||
return [
|
||||
'success' => false,
|
||||
'errors' => $model->getFirstErrors(),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
];
|
||||
}
|
||||
|
||||
private function pardonAccount(Account $account) {
|
||||
$model = new PardonForm($account);
|
||||
$model->load(Yii::$app->request->post());
|
||||
if (!$model->pardon()) {
|
||||
return [
|
||||
'success' => false,
|
||||
'errors' => $model->getFirstErrors(),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
];
|
||||
}
|
||||
|
||||
private function findAccount(int $accountId): Account {
|
||||
$account = Account::findOne($accountId);
|
||||
if ($account === null) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
return $account;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ use yii\web\Response;
|
||||
|
||||
class ApiController extends Controller {
|
||||
|
||||
public function behaviors() {
|
||||
public function behaviors(): array {
|
||||
$behaviors = parent::behaviors();
|
||||
unset($behaviors['authenticator']);
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?php
|
||||
namespace api\modules\session\controllers;
|
||||
|
||||
use api\controllers\ApiController;
|
||||
use api\controllers\Controller;
|
||||
use api\modules\session\exceptions\ForbiddenOperationException;
|
||||
use api\modules\session\exceptions\IllegalArgumentException;
|
||||
use api\modules\session\exceptions\SessionServerException;
|
||||
@ -17,9 +17,9 @@ use Ramsey\Uuid\Uuid;
|
||||
use Yii;
|
||||
use yii\web\Response;
|
||||
|
||||
class SessionController extends ApiController {
|
||||
class SessionController extends Controller {
|
||||
|
||||
public function behaviors() {
|
||||
public function behaviors(): array {
|
||||
$behaviors = parent::behaviors();
|
||||
unset($behaviors['authenticator']);
|
||||
$behaviors['rateLimiting'] = [
|
||||
|
@ -18,7 +18,7 @@ class HasJoinedForm extends Model {
|
||||
parent::__construct($config);
|
||||
}
|
||||
|
||||
public function hasJoined() : Account {
|
||||
public function hasJoined(): Account {
|
||||
if (!$this->protocol->validate()) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
@ -7,10 +7,10 @@ use api\modules\session\models\protocols\JoinInterface;
|
||||
use api\modules\session\Module as Session;
|
||||
use api\modules\session\validators\RequiredValidator;
|
||||
use common\helpers\StringHelper;
|
||||
use common\models\OauthScope as S;
|
||||
use common\validators\UuidValidator;
|
||||
use common\rbac\Permissions as P;
|
||||
use common\models\Account;
|
||||
use common\models\MinecraftAccessKey;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Yii;
|
||||
use yii\base\ErrorException;
|
||||
use yii\base\Model;
|
||||
@ -84,16 +84,7 @@ class JoinForm extends Model {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($attribute === 'selectedProfile' && !StringHelper::isUuid($this->selectedProfile)) {
|
||||
// Это нормально. Там может быть ник игрока, если это Legacy авторизация
|
||||
return;
|
||||
}
|
||||
|
||||
$validator = new UuidValidator();
|
||||
$validator->allowNil = false;
|
||||
$validator->validateAttribute($this, $attribute);
|
||||
|
||||
if ($this->hasErrors($attribute)) {
|
||||
if ($this->$attribute === Uuid::NIL) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
@ -105,9 +96,17 @@ class JoinForm extends Model {
|
||||
$accessToken = $this->accessToken;
|
||||
/** @var MinecraftAccessKey|null $accessModel */
|
||||
$accessModel = MinecraftAccessKey::findOne($accessToken);
|
||||
if ($accessModel === null) {
|
||||
if ($accessModel !== null) {
|
||||
/** @var MinecraftAccessKey|\api\components\OAuth2\Entities\AccessTokenEntity $accessModel */
|
||||
if ($accessModel->isExpired()) {
|
||||
Session::error("User with access_token = '{$accessToken}' failed join by expired access_token.");
|
||||
throw new ForbiddenOperationException('Expired access_token.');
|
||||
}
|
||||
|
||||
$account = $accessModel->account;
|
||||
} else {
|
||||
try {
|
||||
$identity = Yii::$app->apiUser->loginByAccessToken($accessToken);
|
||||
$identity = Yii::$app->user->loginByAccessToken($accessToken);
|
||||
} catch (UnauthorizedHttpException $e) {
|
||||
$identity = null;
|
||||
}
|
||||
@ -117,21 +116,12 @@ class JoinForm extends Model {
|
||||
throw new ForbiddenOperationException('Invalid access_token.');
|
||||
}
|
||||
|
||||
if (!Yii::$app->apiUser->can(S::MINECRAFT_SERVER_SESSION)) {
|
||||
if (!Yii::$app->user->can(P::MINECRAFT_SERVER_SESSION)) {
|
||||
Session::error("User with access_token = '{$accessToken}' doesn't have enough scopes to make join.");
|
||||
throw new ForbiddenOperationException('The token does not have required scope.');
|
||||
}
|
||||
|
||||
$accessModel = $identity->getAccessToken();
|
||||
$account = $identity->getAccount();
|
||||
} else {
|
||||
$account = $accessModel->account;
|
||||
}
|
||||
|
||||
/** @var MinecraftAccessKey|\api\components\OAuth2\Entities\AccessTokenEntity $accessModel */
|
||||
if ($accessModel->isExpired()) {
|
||||
Session::error("User with access_token = '{$accessToken}' failed join by expired access_token.");
|
||||
throw new ForbiddenOperationException('Expired access_token.');
|
||||
}
|
||||
|
||||
$selectedProfile = $this->selectedProfile;
|
||||
@ -142,7 +132,9 @@ class JoinForm extends Model {
|
||||
" but access_token issued to account with id = '{$account->uuid}'."
|
||||
);
|
||||
throw new ForbiddenOperationException('Wrong selected_profile.');
|
||||
} elseif (!$isUuid && $account->username !== $selectedProfile) {
|
||||
}
|
||||
|
||||
if (!$isUuid && $account->username !== $selectedProfile) {
|
||||
Session::error(
|
||||
"User with access_token = '{$accessToken}' trying to join with identity = '{$selectedProfile}'," .
|
||||
" but access_token issued to account with username = '{$account->username}'."
|
||||
@ -153,10 +145,7 @@ class JoinForm extends Model {
|
||||
$this->account = $account;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Account|null
|
||||
*/
|
||||
protected function getAccount() {
|
||||
protected function getAccount(): Account {
|
||||
return $this->account;
|
||||
}
|
||||
|
||||
|
@ -17,24 +17,16 @@ class SessionModel {
|
||||
$this->serverId = $serverId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $username
|
||||
* @param $serverId
|
||||
*
|
||||
* @return static|null
|
||||
*/
|
||||
public static function find($username, $serverId) {
|
||||
public static function find(string $username, string $serverId): ?self {
|
||||
$key = static::buildKey($username, $serverId);
|
||||
$result = Yii::$app->redis->executeCommand('GET', [$key]);
|
||||
if (!$result) {
|
||||
/** @noinspection PhpIncompatibleReturnTypeInspection шторм что-то сума сходит, когда видит static */
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = json_decode($result, true);
|
||||
$model = new static($data['username'], $data['serverId']);
|
||||
|
||||
return $model;
|
||||
return new static($data['username'], $data['serverId']);
|
||||
}
|
||||
|
||||
public function save() {
|
||||
@ -51,15 +43,11 @@ class SessionModel {
|
||||
return Yii::$app->redis->executeCommand('DEL', [static::buildKey($this->username, $this->serverId)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Account|null
|
||||
* TODO: после перехода на PHP 7.1 установить тип как ?Account
|
||||
*/
|
||||
public function getAccount() {
|
||||
public function getAccount(): ?Account {
|
||||
return Account::findOne(['username' => $this->username]);
|
||||
}
|
||||
|
||||
protected static function buildKey($username, $serverId) : string {
|
||||
protected static function buildKey($username, $serverId): string {
|
||||
return md5('minecraft:join-server:' . mb_strtolower($username) . ':' . $serverId);
|
||||
}
|
||||
|
||||
|
@ -1,31 +1,30 @@
|
||||
<?php
|
||||
namespace api\modules\session\models\protocols;
|
||||
|
||||
use yii\validators\RequiredValidator;
|
||||
|
||||
abstract class BaseHasJoined implements HasJoinedInterface {
|
||||
|
||||
private $username;
|
||||
private $serverId;
|
||||
|
||||
public function __construct(string $username, string $serverId) {
|
||||
$this->username = $username;
|
||||
$this->serverId = $serverId;
|
||||
$this->username = trim($username);
|
||||
$this->serverId = trim($serverId);
|
||||
}
|
||||
|
||||
public function getUsername() : string {
|
||||
public function getUsername(): string {
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
public function getServerId() : string {
|
||||
public function getServerId(): string {
|
||||
return $this->serverId;
|
||||
}
|
||||
|
||||
public function validate() : bool {
|
||||
$validator = new RequiredValidator();
|
||||
public function validate(): bool {
|
||||
return !$this->isEmpty($this->username) && !$this->isEmpty($this->serverId);
|
||||
}
|
||||
|
||||
return $validator->validate($this->username)
|
||||
&& $validator->validate($this->serverId);
|
||||
private function isEmpty($value): bool {
|
||||
return $value === null || $value === '';
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,12 +3,8 @@ namespace api\modules\session\models\protocols;
|
||||
|
||||
abstract class BaseJoin implements JoinInterface {
|
||||
|
||||
abstract public function getAccessToken() : string;
|
||||
|
||||
abstract public function getSelectedProfile() : string;
|
||||
|
||||
abstract public function getServerId() : string;
|
||||
|
||||
abstract public function validate() : bool;
|
||||
protected function isEmpty($value): bool {
|
||||
return $value === null || $value === '';
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,10 +3,10 @@ namespace api\modules\session\models\protocols;
|
||||
|
||||
interface HasJoinedInterface {
|
||||
|
||||
public function getUsername() : string;
|
||||
public function getUsername(): string;
|
||||
|
||||
public function getServerId() : string;
|
||||
public function getServerId(): string;
|
||||
|
||||
public function validate() : bool;
|
||||
public function validate(): bool;
|
||||
|
||||
}
|
||||
|
@ -3,13 +3,12 @@ namespace api\modules\session\models\protocols;
|
||||
|
||||
interface JoinInterface {
|
||||
|
||||
public function getAccessToken() : string;
|
||||
public function getAccessToken(): string;
|
||||
|
||||
// TODO: после перехода на PHP 7.1 сменить тип на ?string и возвращать null, если параметр не передан
|
||||
public function getSelectedProfile() : string;
|
||||
public function getSelectedProfile(): string;
|
||||
|
||||
public function getServerId() : string;
|
||||
public function getServerId(): string;
|
||||
|
||||
public function validate() : bool;
|
||||
public function validate(): bool;
|
||||
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
<?php
|
||||
namespace api\modules\session\models\protocols;
|
||||
|
||||
use yii\validators\RequiredValidator;
|
||||
|
||||
class LegacyJoin extends BaseJoin {
|
||||
|
||||
private $user;
|
||||
@ -13,9 +11,9 @@ class LegacyJoin extends BaseJoin {
|
||||
private $uuid;
|
||||
|
||||
public function __construct(string $user, string $sessionId, string $serverId) {
|
||||
$this->user = $user;
|
||||
$this->sessionId = $sessionId;
|
||||
$this->serverId = $serverId;
|
||||
$this->user = trim($user);
|
||||
$this->sessionId = trim($sessionId);
|
||||
$this->serverId = trim($serverId);
|
||||
|
||||
$this->parseSessionId($this->sessionId);
|
||||
}
|
||||
@ -24,23 +22,19 @@ class LegacyJoin extends BaseJoin {
|
||||
return $this->accessToken;
|
||||
}
|
||||
|
||||
public function getSelectedProfile() : string {
|
||||
public function getSelectedProfile(): string {
|
||||
return $this->uuid ?: $this->user;
|
||||
}
|
||||
|
||||
public function getServerId() : string {
|
||||
public function getServerId(): string {
|
||||
return $this->serverId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function validate() : bool {
|
||||
$validator = new RequiredValidator();
|
||||
|
||||
return $validator->validate($this->accessToken)
|
||||
&& $validator->validate($this->user)
|
||||
&& $validator->validate($this->serverId);
|
||||
public function validate(): bool {
|
||||
return !$this->isEmpty($this->accessToken) && !$this->isEmpty($this->user) && !$this->isEmpty($this->serverId);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -50,7 +44,7 @@ class LegacyJoin extends BaseJoin {
|
||||
* Бьём по ':' для учёта авторизации в современных лаунчерах и входе на более старую
|
||||
* версию игры. Там sessionId передаётся как "token:{accessToken}:{uuid}", так что это нужно обработать
|
||||
*/
|
||||
protected function parseSessionId(string $sessionId) {
|
||||
private function parseSessionId(string $sessionId) {
|
||||
$parts = explode(':', $sessionId);
|
||||
if (count($parts) === 3) {
|
||||
$this->accessToken = $parts[1];
|
||||
|
@ -1,8 +1,6 @@
|
||||
<?php
|
||||
namespace api\modules\session\models\protocols;
|
||||
|
||||
use yii\validators\RequiredValidator;
|
||||
|
||||
class ModernJoin extends BaseJoin {
|
||||
|
||||
private $accessToken;
|
||||
@ -10,29 +8,25 @@ class ModernJoin extends BaseJoin {
|
||||
private $serverId;
|
||||
|
||||
public function __construct(string $accessToken, string $selectedProfile, string $serverId) {
|
||||
$this->accessToken = $accessToken;
|
||||
$this->selectedProfile = $selectedProfile;
|
||||
$this->serverId = $serverId;
|
||||
$this->accessToken = trim($accessToken);
|
||||
$this->selectedProfile = trim($selectedProfile);
|
||||
$this->serverId = trim($serverId);
|
||||
}
|
||||
|
||||
public function getAccessToken() : string {
|
||||
public function getAccessToken(): string {
|
||||
return $this->accessToken;
|
||||
}
|
||||
|
||||
public function getSelectedProfile() : string {
|
||||
public function getSelectedProfile(): string {
|
||||
return $this->selectedProfile;
|
||||
}
|
||||
|
||||
public function getServerId() : string {
|
||||
public function getServerId(): string {
|
||||
return $this->serverId;
|
||||
}
|
||||
|
||||
public function validate() : bool {
|
||||
$validator = new RequiredValidator();
|
||||
|
||||
return $validator->validate($this->accessToken)
|
||||
&& $validator->validate($this->selectedProfile)
|
||||
&& $validator->validate($this->serverId);
|
||||
public function validate(): bool {
|
||||
return !$this->isEmpty($this->accessToken) && !$this->isEmpty($this->selectedProfile) && !$this->isEmpty($this->serverId);
|
||||
}
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user