mirror of
https://github.com/elyby/accounts.git
synced 2025-01-10 14:07:55 +05:30
Merge branch 'develop'
This commit is contained in:
commit
aaae317a11
@ -1,4 +1,4 @@
|
|||||||
FROM registry.ely.by/elyby/accounts-php:1.2.0
|
FROM registry.ely.by/elyby/accounts-php:1.3.0
|
||||||
|
|
||||||
# Вносим конфигурации для крона и воркеров
|
# Вносим конфигурации для крона и воркеров
|
||||||
COPY docker/cron/* /etc/cron.d/
|
COPY docker/cron/* /etc/cron.d/
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM registry.ely.by/elyby/accounts-php:1.2.0-dev
|
FROM registry.ely.by/elyby/accounts-php:1.3.0-dev
|
||||||
|
|
||||||
# Вносим конфигурации для крона и воркеров
|
# Вносим конфигурации для крона и воркеров
|
||||||
COPY docker/cron/* /etc/cron.d/
|
COPY docker/cron/* /etc/cron.d/
|
||||||
|
@ -6,8 +6,8 @@ use yii\web\User as YiiUserComponent;
|
|||||||
/**
|
/**
|
||||||
* @property Identity|null $identity
|
* @property Identity|null $identity
|
||||||
*
|
*
|
||||||
* @method Identity|null getIdentity()
|
* @method Identity|null getIdentity($autoRenew = true)
|
||||||
* @method Identity|null loginByAccessToken(string $token, $type = null)
|
* @method Identity|null loginByAccessToken($token, $type = null)
|
||||||
*/
|
*/
|
||||||
class Component extends YiiUserComponent {
|
class Component extends YiiUserComponent {
|
||||||
|
|
||||||
|
@ -26,7 +26,8 @@ class Identity implements IdentityInterface {
|
|||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
public static function findIdentityByAccessToken($token, $type = null) {
|
public static function findIdentityByAccessToken($token, $type = null): self {
|
||||||
|
/** @var AccessTokenEntity|null $model */
|
||||||
$model = Yii::$app->oauth->getAuthServer()->getAccessTokenStorage()->get($token);
|
$model = Yii::$app->oauth->getAuthServer()->getAccessTokenStorage()->get($token);
|
||||||
if ($model === null) {
|
if ($model === null) {
|
||||||
throw new UnauthorizedHttpException('Incorrect token');
|
throw new UnauthorizedHttpException('Incorrect token');
|
||||||
@ -41,19 +42,19 @@ class Identity implements IdentityInterface {
|
|||||||
$this->_accessToken = $accessToken;
|
$this->_accessToken = $accessToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAccount() : Account {
|
public function getAccount(): Account {
|
||||||
return $this->getSession()->account;
|
return $this->getSession()->account;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getClient() : OauthClient {
|
public function getClient(): OauthClient {
|
||||||
return $this->getSession()->client;
|
return $this->getSession()->client;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSession() : OauthSession {
|
public function getSession(): OauthSession {
|
||||||
return OauthSession::findOne($this->_accessToken->getSessionId());
|
return OauthSession::findOne($this->_accessToken->getSessionId());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAccessToken() : AccessTokenEntity {
|
public function getAccessToken(): AccessTokenEntity {
|
||||||
return $this->_accessToken;
|
return $this->_accessToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,7 +63,7 @@ class Identity implements IdentityInterface {
|
|||||||
* У нас права привязываются к токенам, так что возвращаем именно его id.
|
* У нас права привязываются к токенам, так что возвращаем именно его id.
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
public function getId() {
|
public function getId(): string {
|
||||||
return $this->_accessToken->getId();
|
return $this->_accessToken->getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,8 @@ namespace api\components\OAuth2\Entities;
|
|||||||
|
|
||||||
class ClientEntity extends \League\OAuth2\Server\Entity\ClientEntity {
|
class ClientEntity extends \League\OAuth2\Server\Entity\ClientEntity {
|
||||||
|
|
||||||
|
private $isTrusted;
|
||||||
|
|
||||||
public function setId(string $id) {
|
public function setId(string $id) {
|
||||||
$this->id = $id;
|
$this->id = $id;
|
||||||
}
|
}
|
||||||
@ -19,4 +21,12 @@ class ClientEntity extends \League\OAuth2\Server\Entity\ClientEntity {
|
|||||||
$this->redirectUri = $redirectUri;
|
$this->redirectUri = $redirectUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setIsTrusted(bool $isTrusted) {
|
||||||
|
$this->isTrusted = $isTrusted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isTrusted(): bool {
|
||||||
|
return $this->isTrusted;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
20
api/components/OAuth2/Grants/ClientCredentialsGrant.php
Normal file
20
api/components/OAuth2/Grants/ClientCredentialsGrant.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
namespace api\components\OAuth2\Grants;
|
||||||
|
|
||||||
|
use api\components\OAuth2\Entities;
|
||||||
|
|
||||||
|
class ClientCredentialsGrant extends \League\OAuth2\Server\Grant\ClientCredentialsGrant {
|
||||||
|
|
||||||
|
protected function createAccessTokenEntity() {
|
||||||
|
return new Entities\AccessTokenEntity($this->server);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createRefreshTokenEntity() {
|
||||||
|
return new Entities\RefreshTokenEntity($this->server);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createSessionEntity() {
|
||||||
|
return new Entities\SessionEntity($this->server);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -74,6 +74,7 @@ class ClientStorage extends AbstractStorage implements ClientInterface {
|
|||||||
$entity->setId($model->id);
|
$entity->setId($model->id);
|
||||||
$entity->setName($model->name);
|
$entity->setName($model->name);
|
||||||
$entity->setSecret($model->secret);
|
$entity->setSecret($model->secret);
|
||||||
|
$entity->setIsTrusted($model->is_trusted);
|
||||||
$entity->setRedirectUri($model->redirect_uri);
|
$entity->setRedirectUri($model->redirect_uri);
|
||||||
|
|
||||||
return $entity;
|
return $entity;
|
||||||
|
@ -18,6 +18,9 @@ class RefreshTokenStorage extends AbstractStorage implements RefreshTokenInterfa
|
|||||||
|
|
||||||
public function get($token) {
|
public function get($token) {
|
||||||
$result = Json::decode((new Key($this->dataTable, $token))->getValue());
|
$result = Json::decode((new Key($this->dataTable, $token))->getValue());
|
||||||
|
if ($result === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
$entity = new RefreshTokenEntity($this->server);
|
$entity = new RefreshTokenEntity($this->server);
|
||||||
$entity->setId($result['id']);
|
$entity->setId($result['id']);
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace api\components\OAuth2\Storage;
|
namespace api\components\OAuth2\Storage;
|
||||||
|
|
||||||
|
use api\components\OAuth2\Entities\ClientEntity;
|
||||||
use api\components\OAuth2\Entities\ScopeEntity;
|
use api\components\OAuth2\Entities\ScopeEntity;
|
||||||
use common\models\OauthScope;
|
use common\models\OauthScope;
|
||||||
use League\OAuth2\Server\Storage\AbstractStorage;
|
use League\OAuth2\Server\Storage\AbstractStorage;
|
||||||
use League\OAuth2\Server\Storage\ScopeInterface;
|
use League\OAuth2\Server\Storage\ScopeInterface;
|
||||||
|
use yii\base\ErrorException;
|
||||||
|
|
||||||
class ScopeStorage extends AbstractStorage implements ScopeInterface {
|
class ScopeStorage extends AbstractStorage implements ScopeInterface {
|
||||||
|
|
||||||
@ -12,7 +14,28 @@ class ScopeStorage extends AbstractStorage implements ScopeInterface {
|
|||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
public function get($scope, $grantType = null, $clientId = null) {
|
public function get($scope, $grantType = null, $clientId = null) {
|
||||||
if (!in_array($scope, OauthScope::getScopes(), true)) {
|
$query = OauthScope::find();
|
||||||
|
if ($grantType === 'authorization_code') {
|
||||||
|
$query->onlyPublic()->usersScopes();
|
||||||
|
} elseif ($grantType === 'client_credentials') {
|
||||||
|
$query->machineScopes();
|
||||||
|
$isTrusted = false;
|
||||||
|
if ($clientId !== null) {
|
||||||
|
$client = $this->server->getClientStorage()->get($clientId);
|
||||||
|
if (!$client instanceof ClientEntity) {
|
||||||
|
throw new ErrorException('client storage must return instance of ' . ClientEntity::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
$isTrusted = $client->isTrusted();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$isTrusted) {
|
||||||
|
$query->onlyPublic();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scopes = $query->all();
|
||||||
|
if (!in_array($scope, $scopes, true)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ $params = array_merge(
|
|||||||
return [
|
return [
|
||||||
'id' => 'accounts-site-api',
|
'id' => 'accounts-site-api',
|
||||||
'basePath' => dirname(__DIR__),
|
'basePath' => dirname(__DIR__),
|
||||||
'bootstrap' => ['log', 'authserver'],
|
'bootstrap' => ['log', 'authserver', 'internal'],
|
||||||
'controllerNamespace' => 'api\controllers',
|
'controllerNamespace' => 'api\controllers',
|
||||||
'params' => $params,
|
'params' => $params,
|
||||||
'components' => [
|
'components' => [
|
||||||
@ -73,14 +73,6 @@ return [
|
|||||||
'response' => [
|
'response' => [
|
||||||
'format' => yii\web\Response::FORMAT_JSON,
|
'format' => yii\web\Response::FORMAT_JSON,
|
||||||
],
|
],
|
||||||
'oauth' => [
|
|
||||||
'class' => api\components\OAuth2\Component::class,
|
|
||||||
'grantTypes' => ['authorization_code'],
|
|
||||||
'grantMap' => [
|
|
||||||
'authorization_code' => api\components\OAuth2\Grants\AuthCodeGrant::class,
|
|
||||||
'refresh_token' => api\components\OAuth2\Grants\RefreshTokenGrant::class,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'errorHandler' => [
|
'errorHandler' => [
|
||||||
'class' => api\components\ErrorHandler::class,
|
'class' => api\components\ErrorHandler::class,
|
||||||
],
|
],
|
||||||
@ -96,5 +88,8 @@ return [
|
|||||||
'mojang' => [
|
'mojang' => [
|
||||||
'class' => api\modules\mojang\Module::class,
|
'class' => api\modules\mojang\Module::class,
|
||||||
],
|
],
|
||||||
|
'internal' => [
|
||||||
|
'class' => api\modules\internal\Module::class,
|
||||||
|
],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
@ -7,6 +7,9 @@ return [
|
|||||||
'/accounts/change-email/submit-new-email' => 'accounts/change-email-submit-new-email',
|
'/accounts/change-email/submit-new-email' => 'accounts/change-email-submit-new-email',
|
||||||
'/accounts/change-email/confirm-new-email' => 'accounts/change-email-confirm-new-email',
|
'/accounts/change-email/confirm-new-email' => 'accounts/change-email-confirm-new-email',
|
||||||
|
|
||||||
|
'POST /two-factor-auth' => 'two-factor-auth/activate',
|
||||||
|
'DELETE /two-factor-auth' => 'two-factor-auth/disable',
|
||||||
|
|
||||||
'/oauth2/v1/<action>' => 'oauth/<action>',
|
'/oauth2/v1/<action>' => 'oauth/<action>',
|
||||||
|
|
||||||
'/account/v1/info' => 'identity-info/index',
|
'/account/v1/info' => 'identity-info/index',
|
||||||
|
@ -69,6 +69,7 @@ class AccountsController extends Controller {
|
|||||||
'passwordChangedAt' => $account->password_changed_at,
|
'passwordChangedAt' => $account->password_changed_at,
|
||||||
'hasMojangUsernameCollision' => $account->hasMojangUsernameCollision(),
|
'hasMojangUsernameCollision' => $account->hasMojangUsernameCollision(),
|
||||||
'shouldAcceptRules' => !$account->isAgreedWithActualRules(),
|
'shouldAcceptRules' => !$account->isAgreedWithActualRules(),
|
||||||
|
'isOtpEnabled' => (bool)$account->is_otp_enabled,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,9 @@ use api\components\OAuth2\Exception\AccessDeniedException;
|
|||||||
use common\models\Account;
|
use common\models\Account;
|
||||||
use common\models\OauthClient;
|
use common\models\OauthClient;
|
||||||
use common\models\OauthScope;
|
use common\models\OauthScope;
|
||||||
|
use League\OAuth2\Server\AuthorizationServer;
|
||||||
use League\OAuth2\Server\Exception\OAuthException;
|
use League\OAuth2\Server\Exception\OAuthException;
|
||||||
|
use League\OAuth2\Server\Grant\AuthCodeGrant;
|
||||||
use Yii;
|
use Yii;
|
||||||
use yii\filters\AccessControl;
|
use yii\filters\AccessControl;
|
||||||
use yii\helpers\ArrayHelper;
|
use yii\helpers\ArrayHelper;
|
||||||
@ -274,17 +276,12 @@ class OauthController extends Controller {
|
|||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function getServer(): AuthorizationServer {
|
||||||
* @return \League\OAuth2\Server\AuthorizationServer
|
|
||||||
*/
|
|
||||||
private function getServer() {
|
|
||||||
return Yii::$app->oauth->authServer;
|
return Yii::$app->oauth->authServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function getGrantType(): AuthCodeGrant {
|
||||||
* @return \League\OAuth2\Server\Grant\AuthCodeGrant
|
/** @noinspection PhpIncompatibleReturnTypeInspection */
|
||||||
*/
|
|
||||||
private function getGrantType() {
|
|
||||||
return $this->getServer()->getGrantType('authorization_code');
|
return $this->getServer()->getGrantType('authorization_code');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
75
api/controllers/TwoFactorAuthController.php
Normal file
75
api/controllers/TwoFactorAuthController.php
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
namespace api\controllers;
|
||||||
|
|
||||||
|
use api\filters\ActiveUserRule;
|
||||||
|
use api\models\profile\TwoFactorAuthForm;
|
||||||
|
use Yii;
|
||||||
|
use yii\filters\AccessControl;
|
||||||
|
use yii\helpers\ArrayHelper;
|
||||||
|
|
||||||
|
class TwoFactorAuthController extends Controller {
|
||||||
|
|
||||||
|
public $defaultAction = 'credentials';
|
||||||
|
|
||||||
|
public function behaviors() {
|
||||||
|
return ArrayHelper::merge(parent::behaviors(), [
|
||||||
|
'access' => [
|
||||||
|
'class' => AccessControl::class,
|
||||||
|
'rules' => [
|
||||||
|
[
|
||||||
|
'allow' => true,
|
||||||
|
'class' => ActiveUserRule::class,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function verbs() {
|
||||||
|
return [
|
||||||
|
'credentials' => ['GET'],
|
||||||
|
'activate' => ['POST'],
|
||||||
|
'disable' => ['DELETE'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function actionCredentials() {
|
||||||
|
$account = Yii::$app->user->identity;
|
||||||
|
$model = new TwoFactorAuthForm($account);
|
||||||
|
|
||||||
|
return $model->getCredentials();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function actionActivate() {
|
||||||
|
$account = Yii::$app->user->identity;
|
||||||
|
$model = new TwoFactorAuthForm($account, ['scenario' => TwoFactorAuthForm::SCENARIO_ACTIVATE]);
|
||||||
|
$model->load(Yii::$app->request->post());
|
||||||
|
if (!$model->activate()) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'errors' => $model->getFirstErrors(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function actionDisable() {
|
||||||
|
$account = Yii::$app->user->identity;
|
||||||
|
$model = new TwoFactorAuthForm($account, ['scenario' => TwoFactorAuthForm::SCENARIO_DISABLE]);
|
||||||
|
$model->load(Yii::$app->request->getBodyParams());
|
||||||
|
if (!$model->disable()) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'errors' => $model->getFirstErrors(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
namespace api\models\authentication;
|
namespace api\models\authentication;
|
||||||
|
|
||||||
use api\models\base\ApiForm;
|
use api\models\base\ApiForm;
|
||||||
|
use api\validators\TotpValidator;
|
||||||
use common\helpers\Error as E;
|
use common\helpers\Error as E;
|
||||||
use api\traits\AccountFinder;
|
use api\traits\AccountFinder;
|
||||||
use common\components\UserFriendlyRandomKey;
|
use common\components\UserFriendlyRandomKey;
|
||||||
@ -16,11 +17,16 @@ class ForgotPasswordForm extends ApiForm {
|
|||||||
use AccountFinder;
|
use AccountFinder;
|
||||||
|
|
||||||
public $login;
|
public $login;
|
||||||
|
public $token;
|
||||||
|
|
||||||
public function rules() {
|
public function rules() {
|
||||||
return [
|
return [
|
||||||
['login', 'required', 'message' => E::LOGIN_REQUIRED],
|
['login', 'required', 'message' => E::LOGIN_REQUIRED],
|
||||||
['login', 'validateLogin'],
|
['login', 'validateLogin'],
|
||||||
|
['token', 'required', 'when' => function(self $model) {
|
||||||
|
return !$this->hasErrors() && $model->getAccount()->is_otp_enabled;
|
||||||
|
}, 'message' => E::OTP_TOKEN_REQUIRED],
|
||||||
|
['token', 'validateTotpToken'],
|
||||||
['login', 'validateActivity'],
|
['login', 'validateActivity'],
|
||||||
['login', 'validateFrequency'],
|
['login', 'validateFrequency'],
|
||||||
];
|
];
|
||||||
@ -34,6 +40,20 @@ class ForgotPasswordForm extends ApiForm {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function validateTotpToken($attribute) {
|
||||||
|
if ($this->hasErrors()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$account = $this->getAccount();
|
||||||
|
if (!$account->is_otp_enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$validator = new TotpValidator(['account' => $account]);
|
||||||
|
$validator->validateAttribute($this, $attribute);
|
||||||
|
}
|
||||||
|
|
||||||
public function validateActivity($attribute) {
|
public function validateActivity($attribute) {
|
||||||
if (!$this->hasErrors()) {
|
if (!$this->hasErrors()) {
|
||||||
$account = $this->getAccount();
|
$account = $this->getAccount();
|
||||||
|
@ -3,6 +3,7 @@ namespace api\models\authentication;
|
|||||||
|
|
||||||
use api\models\AccountIdentity;
|
use api\models\AccountIdentity;
|
||||||
use api\models\base\ApiForm;
|
use api\models\base\ApiForm;
|
||||||
|
use api\validators\TotpValidator;
|
||||||
use common\helpers\Error as E;
|
use common\helpers\Error as E;
|
||||||
use api\traits\AccountFinder;
|
use api\traits\AccountFinder;
|
||||||
use common\models\Account;
|
use common\models\Account;
|
||||||
@ -16,6 +17,7 @@ class LoginForm extends ApiForm {
|
|||||||
|
|
||||||
public $login;
|
public $login;
|
||||||
public $password;
|
public $password;
|
||||||
|
public $token;
|
||||||
public $rememberMe = false;
|
public $rememberMe = false;
|
||||||
|
|
||||||
public function rules() {
|
public function rules() {
|
||||||
@ -28,6 +30,11 @@ class LoginForm extends ApiForm {
|
|||||||
}, 'message' => E::PASSWORD_REQUIRED],
|
}, 'message' => E::PASSWORD_REQUIRED],
|
||||||
['password', 'validatePassword'],
|
['password', 'validatePassword'],
|
||||||
|
|
||||||
|
['token', 'required', 'when' => function(self $model) {
|
||||||
|
return !$model->hasErrors() && $model->getAccount()->is_otp_enabled;
|
||||||
|
}, 'message' => E::OTP_TOKEN_REQUIRED],
|
||||||
|
['token', 'validateTotpToken'],
|
||||||
|
|
||||||
['login', 'validateActivity'],
|
['login', 'validateActivity'],
|
||||||
|
|
||||||
['rememberMe', 'boolean'],
|
['rememberMe', 'boolean'],
|
||||||
@ -51,6 +58,20 @@ class LoginForm extends ApiForm {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function validateTotpToken($attribute) {
|
||||||
|
if ($this->hasErrors()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$account = $this->getAccount();
|
||||||
|
if (!$account->is_otp_enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$validator = new TotpValidator(['account' => $account]);
|
||||||
|
$validator->validateAttribute($this, $attribute);
|
||||||
|
}
|
||||||
|
|
||||||
public function validateActivity($attribute) {
|
public function validateActivity($attribute) {
|
||||||
if (!$this->hasErrors()) {
|
if (!$this->hasErrors()) {
|
||||||
$account = $this->getAccount();
|
$account = $this->getAccount();
|
||||||
|
138
api/models/profile/TwoFactorAuthForm.php
Normal file
138
api/models/profile/TwoFactorAuthForm.php
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
<?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 Base32\Base32;
|
||||||
|
use common\components\Qr\ElyDecorator;
|
||||||
|
use common\helpers\Error as E;
|
||||||
|
use common\models\Account;
|
||||||
|
use OTPHP\TOTP;
|
||||||
|
use yii\base\ErrorException;
|
||||||
|
|
||||||
|
class TwoFactorAuthForm extends ApiForm {
|
||||||
|
|
||||||
|
const SCENARIO_ACTIVATE = 'enable';
|
||||||
|
const SCENARIO_DISABLE = 'disable';
|
||||||
|
|
||||||
|
public $token;
|
||||||
|
|
||||||
|
public $password;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Account
|
||||||
|
*/
|
||||||
|
private $account;
|
||||||
|
|
||||||
|
public function __construct(Account $account, array $config = []) {
|
||||||
|
$this->account = $account;
|
||||||
|
parent::__construct($config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules() {
|
||||||
|
$bothScenarios = [self::SCENARIO_ACTIVATE, self::SCENARIO_DISABLE];
|
||||||
|
return [
|
||||||
|
['account', 'validateOtpDisabled', 'on' => self::SCENARIO_ACTIVATE],
|
||||||
|
['account', 'validateOtpEnabled', 'on' => self::SCENARIO_DISABLE],
|
||||||
|
['token', 'required', 'message' => E::OTP_TOKEN_REQUIRED, 'on' => $bothScenarios],
|
||||||
|
['token', TotpValidator::class, 'account' => $this->account, 'window' => 30, 'on' => $bothScenarios],
|
||||||
|
['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' => base64_encode($this->drawQrCode($provisioningUri)),
|
||||||
|
'uri' => $provisioningUri,
|
||||||
|
'secret' => $this->account->otp_secret,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function activate(): bool {
|
||||||
|
if ($this->scenario !== self::SCENARIO_ACTIVATE || !$this->validate()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$account = $this->account;
|
||||||
|
$account->is_otp_enabled = true;
|
||||||
|
if (!$account->save()) {
|
||||||
|
throw new ErrorException('Cannot enable otp for account');
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = new TOTP($this->account->email, $this->account->otp_secret);
|
||||||
|
$totp->setIssuer('Ely.by');
|
||||||
|
|
||||||
|
return $totp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function drawQrCode(string $content): string {
|
||||||
|
$renderer = new Svg();
|
||||||
|
$renderer->setHeight(256);
|
||||||
|
$renderer->setWidth(256);
|
||||||
|
$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);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setOtpSecret(): void {
|
||||||
|
$this->account->otp_secret = trim(Base32::encode(random_bytes(32)), '=');
|
||||||
|
if (!$this->account->save()) {
|
||||||
|
throw new ErrorException('Cannot set account otp_secret');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
19
api/modules/internal/Module.php
Normal file
19
api/modules/internal/Module.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
namespace api\modules\internal;
|
||||||
|
|
||||||
|
use yii\base\BootstrapInterface;
|
||||||
|
|
||||||
|
class Module extends \yii\base\Module implements BootstrapInterface {
|
||||||
|
|
||||||
|
public $id = 'internal';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \yii\base\Application $app the application currently running
|
||||||
|
*/
|
||||||
|
public function bootstrap($app) {
|
||||||
|
$app->getUrlManager()->addRules([
|
||||||
|
'/internal/<controller>/<accountId>/<action>' => "{$this->id}/<controller>/<action>",
|
||||||
|
], false);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
88
api/modules/internal/controllers/AccountsController.php
Normal file
88
api/modules/internal/controllers/AccountsController.php
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<?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 yii\helpers\ArrayHelper;
|
||||||
|
use yii\web\NotFoundHttpException;
|
||||||
|
|
||||||
|
class AccountsController extends Controller {
|
||||||
|
|
||||||
|
public function behaviors() {
|
||||||
|
return ArrayHelper::merge(parent::behaviors(), [
|
||||||
|
'authenticator' => [
|
||||||
|
'user' => Yii::$app->apiUser,
|
||||||
|
],
|
||||||
|
'access' => [
|
||||||
|
'class' => AccessControl::class,
|
||||||
|
'rules' => [
|
||||||
|
[
|
||||||
|
'actions' => ['ban'],
|
||||||
|
'allow' => true,
|
||||||
|
'roles' => [S::ACCOUNT_BLOCK],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function verbs() {
|
||||||
|
return [
|
||||||
|
'ban' => ['POST', 'DELETE'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function actionBan(int $accountId) {
|
||||||
|
$account = $this->findAccount($accountId);
|
||||||
|
if (Yii::$app->request->isPost) {
|
||||||
|
return $this->banAccount($account);
|
||||||
|
} else {
|
||||||
|
return $this->pardonAccount($account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
9
api/modules/internal/helpers/Error.php
Normal file
9
api/modules/internal/helpers/Error.php
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
namespace api\modules\internal\helpers;
|
||||||
|
|
||||||
|
final class Error {
|
||||||
|
|
||||||
|
public const ACCOUNT_ALREADY_BANNED = 'error.account_already_banned';
|
||||||
|
public const ACCOUNT_NOT_BANNED = 'error.account_not_banned';
|
||||||
|
|
||||||
|
}
|
95
api/modules/internal/models/BanForm.php
Normal file
95
api/modules/internal/models/BanForm.php
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
namespace api\modules\internal\models;
|
||||||
|
|
||||||
|
use api\models\base\ApiForm;
|
||||||
|
use api\modules\internal\helpers\Error as E;
|
||||||
|
use common\helpers\Amqp;
|
||||||
|
use common\models\Account;
|
||||||
|
use common\models\amqp\AccountBanned;
|
||||||
|
use PhpAmqpLib\Message\AMQPMessage;
|
||||||
|
use Yii;
|
||||||
|
use yii\base\ErrorException;
|
||||||
|
|
||||||
|
class BanForm extends ApiForm {
|
||||||
|
|
||||||
|
public const DURATION_FOREVER = -1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Нереализованный функционал блокировки аккаунта на определённый период времени.
|
||||||
|
* Сейчас установка этого параметра ничего не даст, аккаунт будет заблокирован навечно,
|
||||||
|
* но, по задумке, здесь можно передать количество секунд, на которое будет
|
||||||
|
* заблокирован аккаунт пользователя.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
public $duration = self::DURATION_FOREVER;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Нереализованный функционал указания причины блокировки аккаунта.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $message = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Account
|
||||||
|
*/
|
||||||
|
private $account;
|
||||||
|
|
||||||
|
public function rules(): array {
|
||||||
|
return [
|
||||||
|
[['duration'], 'integer', 'min' => self::DURATION_FOREVER],
|
||||||
|
[['message'], 'string'],
|
||||||
|
[['account'], 'validateAccountActivity'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAccount(): Account {
|
||||||
|
return $this->account;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validateAccountActivity() {
|
||||||
|
if ($this->account->status === Account::STATUS_BANNED) {
|
||||||
|
$this->addError('account', E::ACCOUNT_ALREADY_BANNED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ban(): bool {
|
||||||
|
if (!$this->validate()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$transaction = Yii::$app->db->beginTransaction();
|
||||||
|
|
||||||
|
$account = $this->account;
|
||||||
|
$account->status = Account::STATUS_BANNED;
|
||||||
|
if (!$account->save()) {
|
||||||
|
throw new ErrorException('Cannot ban account');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->createTask();
|
||||||
|
|
||||||
|
$transaction->commit();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createTask(): void {
|
||||||
|
$model = new AccountBanned();
|
||||||
|
$model->accountId = $this->account->id;
|
||||||
|
$model->duration = $this->duration;
|
||||||
|
$model->message = $this->message;
|
||||||
|
|
||||||
|
$message = Amqp::getInstance()->prepareMessage($model, [
|
||||||
|
'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Amqp::sendToEventsExchange('accounts.account-banned', $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __construct(Account $account, array $config = []) {
|
||||||
|
$this->account = $account;
|
||||||
|
parent::__construct($config);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
72
api/modules/internal/models/PardonForm.php
Normal file
72
api/modules/internal/models/PardonForm.php
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
namespace api\modules\internal\models;
|
||||||
|
|
||||||
|
use api\models\base\ApiForm;
|
||||||
|
use api\modules\internal\helpers\Error as E;
|
||||||
|
use common\helpers\Amqp;
|
||||||
|
use common\models\Account;
|
||||||
|
use common\models\amqp\AccountPardoned;
|
||||||
|
use PhpAmqpLib\Message\AMQPMessage;
|
||||||
|
use Yii;
|
||||||
|
use yii\base\ErrorException;
|
||||||
|
|
||||||
|
class PardonForm extends ApiForm {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Account
|
||||||
|
*/
|
||||||
|
private $account;
|
||||||
|
|
||||||
|
public function rules(): array {
|
||||||
|
return [
|
||||||
|
[['account'], 'validateAccountBanned'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAccount(): Account {
|
||||||
|
return $this->account;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validateAccountBanned(): void {
|
||||||
|
if ($this->account->status !== Account::STATUS_BANNED) {
|
||||||
|
$this->addError('account', E::ACCOUNT_NOT_BANNED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pardon(): bool {
|
||||||
|
if (!$this->validate()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$transaction = Yii::$app->db->beginTransaction();
|
||||||
|
|
||||||
|
$account = $this->account;
|
||||||
|
$account->status = Account::STATUS_ACTIVE;
|
||||||
|
if (!$account->save()) {
|
||||||
|
throw new ErrorException('Cannot pardon account');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->createTask();
|
||||||
|
|
||||||
|
$transaction->commit();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createTask(): void {
|
||||||
|
$model = new AccountPardoned();
|
||||||
|
$model->accountId = $this->account->id;
|
||||||
|
|
||||||
|
$message = Amqp::getInstance()->prepareMessage($model, [
|
||||||
|
'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Amqp::sendToEventsExchange('accounts.account-pardoned', $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __construct(Account $account, array $config = []) {
|
||||||
|
$this->account = $account;
|
||||||
|
parent::__construct($config);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -89,15 +89,12 @@ class SessionController extends ApiController {
|
|||||||
$hasJoinedForm = new HasJoinedForm($protocol);
|
$hasJoinedForm = new HasJoinedForm($protocol);
|
||||||
try {
|
try {
|
||||||
$hasJoinedForm->hasJoined();
|
$hasJoinedForm->hasJoined();
|
||||||
|
} catch (ForbiddenOperationException $e) {
|
||||||
|
return 'NO';
|
||||||
} catch (SessionServerException $e) {
|
} catch (SessionServerException $e) {
|
||||||
Yii::$app->response->statusCode = $e->statusCode;
|
Yii::$app->response->statusCode = $e->statusCode;
|
||||||
if ($e instanceof ForbiddenOperationException) {
|
|
||||||
$message = 'NO';
|
|
||||||
} else {
|
|
||||||
$message = $e->getMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $message;
|
return $e->getMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'YES';
|
return 'YES';
|
||||||
|
51
api/validators/TotpValidator.php
Normal file
51
api/validators/TotpValidator.php
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
namespace api\validators;
|
||||||
|
|
||||||
|
use common\helpers\Error as E;
|
||||||
|
use common\models\Account;
|
||||||
|
use OTPHP\TOTP;
|
||||||
|
use Yii;
|
||||||
|
use yii\base\InvalidConfigException;
|
||||||
|
use yii\validators\Validator;
|
||||||
|
|
||||||
|
class TotpValidator extends Validator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Account
|
||||||
|
*/
|
||||||
|
public $account;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int|null Задаёт окно, в промежуток которого будет проверяться код.
|
||||||
|
* Позволяет избежать ситуации, когда пользователь ввёл код в последнюю секунду
|
||||||
|
* его существования и пока шёл запрос, тот протух.
|
||||||
|
*/
|
||||||
|
public $window;
|
||||||
|
|
||||||
|
public $skipOnEmpty = false;
|
||||||
|
|
||||||
|
public function init() {
|
||||||
|
parent::init();
|
||||||
|
if ($this->account === null) {
|
||||||
|
$this->account = Yii::$app->user->identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->account instanceof Account) {
|
||||||
|
throw new InvalidConfigException('account should be instance of ' . Account::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($this->account->otp_secret)) {
|
||||||
|
throw new InvalidConfigException('account should have not empty otp_secret');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function validateValue($value) {
|
||||||
|
$totp = new TOTP(null, $this->account->otp_secret);
|
||||||
|
if (!$totp->verify((string)$value, null, $this->window)) {
|
||||||
|
return [E::OTP_TOKEN_INCORRECT, []];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -16,12 +16,13 @@ class Yii extends \yii\BaseYii {
|
|||||||
* Class BaseApplication
|
* Class BaseApplication
|
||||||
* Used for properties that are identical for both WebApplication and ConsoleApplication
|
* Used for properties that are identical for both WebApplication and ConsoleApplication
|
||||||
*
|
*
|
||||||
* @property \yii\swiftmailer\Mailer $mailer
|
* @property \yii\swiftmailer\Mailer $mailer
|
||||||
* @property \common\components\Redis\Connection $redis
|
* @property \common\components\Redis\Connection $redis
|
||||||
* @property \common\components\RabbitMQ\Component $amqp
|
* @property \common\components\RabbitMQ\Component $amqp
|
||||||
* @property \GuzzleHttp\Client $guzzle
|
* @property \GuzzleHttp\Client $guzzle
|
||||||
* @property \common\components\EmailRenderer $emailRenderer
|
* @property \common\components\EmailRenderer $emailRenderer
|
||||||
* @property \mito\sentry\Component $sentry
|
* @property \mito\sentry\Component $sentry
|
||||||
|
* @property \api\components\OAuth2\Component $oauth
|
||||||
*/
|
*/
|
||||||
abstract class BaseApplication extends yii\base\Application {
|
abstract class BaseApplication extends yii\base\Application {
|
||||||
}
|
}
|
||||||
@ -33,7 +34,6 @@ abstract class BaseApplication extends yii\base\Application {
|
|||||||
* @property \api\components\User\Component $user User component.
|
* @property \api\components\User\Component $user User component.
|
||||||
* @property \api\components\ApiUser\Component $apiUser Api User component.
|
* @property \api\components\ApiUser\Component $apiUser Api User component.
|
||||||
* @property \api\components\ReCaptcha\Component $reCaptcha
|
* @property \api\components\ReCaptcha\Component $reCaptcha
|
||||||
* @property \api\components\OAuth2\Component $oauth
|
|
||||||
*
|
*
|
||||||
* @method \api\components\User\Component getUser()
|
* @method \api\components\User\Component getUser()
|
||||||
*/
|
*/
|
||||||
|
18
common/components/Annotations/Reader.php
Normal file
18
common/components/Annotations/Reader.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
namespace common\components\Annotations;
|
||||||
|
|
||||||
|
class Reader extends \Minime\Annotations\Reader {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Поначаду я думал кэшировать эту штуку, но потом забил, т.к. всё всё равно завернул
|
||||||
|
* в Yii::$app->cache и как-то надобность в отдельном кэше отпала, так что пока забьём
|
||||||
|
* и оставим как заготовку на будущее
|
||||||
|
*
|
||||||
|
* @return \Minime\Annotations\Interfaces\ReaderInterface
|
||||||
|
*/
|
||||||
|
public static function createFromDefaults() {
|
||||||
|
return parent::createFromDefaults();
|
||||||
|
//return new self(new \Minime\Annotations\Parser(), new RedisCache());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
65
common/components/Annotations/RedisCache.php
Normal file
65
common/components/Annotations/RedisCache.php
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
namespace common\components\Annotations;
|
||||||
|
|
||||||
|
use common\components\Redis\Key;
|
||||||
|
use common\components\Redis\Set;
|
||||||
|
use Minime\Annotations\Interfaces\CacheInterface;
|
||||||
|
use yii\helpers\Json;
|
||||||
|
|
||||||
|
class RedisCache implements CacheInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates uuid for a given docblock string
|
||||||
|
* @param string $docblock docblock string
|
||||||
|
* @return string uuid that maps to the given docblock
|
||||||
|
*/
|
||||||
|
public function getKey($docblock) {
|
||||||
|
return md5($docblock);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an annotation AST to cache
|
||||||
|
*
|
||||||
|
* @param string $key cache entry uuid
|
||||||
|
* @param array $annotations annotation AST
|
||||||
|
*/
|
||||||
|
public function set($key, array $annotations) {
|
||||||
|
$this->getRedisKey($key)->setValue(Json::encode($annotations))->expire(3600);
|
||||||
|
$this->getRedisKeysSet()->add($key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves cached annotations from docblock uuid
|
||||||
|
*
|
||||||
|
* @param string $key cache entry uuid
|
||||||
|
* @return array cached annotation AST
|
||||||
|
*/
|
||||||
|
public function get($key) {
|
||||||
|
$result = $this->getRedisKey($key)->getValue();
|
||||||
|
if ($result === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Json::decode($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets cache
|
||||||
|
*/
|
||||||
|
public function clear() {
|
||||||
|
/** @var array $keys */
|
||||||
|
$keys = $this->getRedisKeysSet()->getValue();
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
$this->getRedisKey($key)->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getRedisKey(string $key): Key {
|
||||||
|
return new Key('annotations', 'cache', $key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getRedisKeysSet(): Set {
|
||||||
|
return new Set('annotations', 'cache', 'keys');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
116
common/components/Qr/ElyDecorator.php
Normal file
116
common/components/Qr/ElyDecorator.php
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
<?php
|
||||||
|
namespace common\components\Qr;
|
||||||
|
|
||||||
|
use BaconQrCode\Common\ErrorCorrectionLevel;
|
||||||
|
use BaconQrCode\Encoder\QrCode;
|
||||||
|
use BaconQrCode\Renderer\Image\Decorator\DecoratorInterface;
|
||||||
|
use BaconQrCode\Renderer\Image\RendererInterface;
|
||||||
|
use BaconQrCode\Renderer\Image\Svg;
|
||||||
|
use Imagick;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use ReflectionClass;
|
||||||
|
|
||||||
|
class ElyDecorator implements DecoratorInterface {
|
||||||
|
|
||||||
|
private const LOGO = __DIR__ . '/resources/logo.svg';
|
||||||
|
|
||||||
|
private const CORRECTION_MAP = [
|
||||||
|
ErrorCorrectionLevel::L => 7,
|
||||||
|
ErrorCorrectionLevel::M => 15,
|
||||||
|
ErrorCorrectionLevel::Q => 25,
|
||||||
|
ErrorCorrectionLevel::H => 30,
|
||||||
|
];
|
||||||
|
|
||||||
|
public function preProcess(
|
||||||
|
QrCode $qrCode,
|
||||||
|
RendererInterface $renderer,
|
||||||
|
$outputWidth,
|
||||||
|
$outputHeight,
|
||||||
|
$leftPadding,
|
||||||
|
$topPadding,
|
||||||
|
$multiple
|
||||||
|
) {
|
||||||
|
if (!$renderer instanceof Svg) {
|
||||||
|
throw new InvalidArgumentException('$renderer must by instance of ' . Svg::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
$correctionLevel = self::CORRECTION_MAP[$qrCode->getErrorCorrectionLevel()->get()];
|
||||||
|
$sizeMultiplier = $correctionLevel + floor($correctionLevel / 3);
|
||||||
|
$count = $qrCode->getMatrix()->getWidth();
|
||||||
|
|
||||||
|
$countToRemoveX = floor($count * $sizeMultiplier / 100);
|
||||||
|
$countToRemoveY = floor($count * $sizeMultiplier / 100);
|
||||||
|
|
||||||
|
$startX = $leftPadding + round(($count - $countToRemoveX) / 2 * $multiple);
|
||||||
|
$startY = $topPadding + round(($count - $countToRemoveY) / 2 * $multiple);
|
||||||
|
$width = $countToRemoveX * $multiple;
|
||||||
|
$height = $countToRemoveY * $multiple;
|
||||||
|
|
||||||
|
$reflection = new ReflectionClass($renderer);
|
||||||
|
$property = $reflection->getProperty('svg');
|
||||||
|
$property->setAccessible(true);
|
||||||
|
/** @var \SimpleXMLElement $svg */
|
||||||
|
$svg = $property->getValue($renderer);
|
||||||
|
|
||||||
|
$image = $svg->addChild('image');
|
||||||
|
$image->addAttribute('x', $startX);
|
||||||
|
$image->addAttribute('y', $startY);
|
||||||
|
$image->addAttribute('width', $width);
|
||||||
|
$image->addAttribute('height', $height);
|
||||||
|
$image->addAttribute('xlink:href', $this->encodeSvgToBase64(self::LOGO));
|
||||||
|
|
||||||
|
$logo = new Imagick();
|
||||||
|
$logo->readImage(self::LOGO);
|
||||||
|
$logo->scaleImage($width, $height);
|
||||||
|
|
||||||
|
$foundedPixels = [];
|
||||||
|
foreach ($logo->getPixelIterator() as $row => $pixels) {
|
||||||
|
/** @var \ImagickPixel[] $pixels */
|
||||||
|
foreach ($pixels as $column => $pixel) {
|
||||||
|
$color = $pixel->getColorAsString();
|
||||||
|
if ($color !== 'srgb(255,255,255)') {
|
||||||
|
$foundedPixels[] = [(int)($startX + $column), (int)($startY + $row)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$logo->clear();
|
||||||
|
$logo->destroy();
|
||||||
|
|
||||||
|
$padding = $multiple - 2;
|
||||||
|
if ($padding < 0) {
|
||||||
|
$padding = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($foundedPixels as $coordinates) {
|
||||||
|
[$x, $y] = $coordinates;
|
||||||
|
$x -= $leftPadding;
|
||||||
|
$y -= $topPadding;
|
||||||
|
|
||||||
|
for ($i = $x - $padding; $i <= $x + $padding; $i++) {
|
||||||
|
for ($j = $y - $padding; $j <= $y + $padding; $j++) {
|
||||||
|
$matrixX = floor($i / $multiple);
|
||||||
|
$matrixY = floor($j / $multiple);
|
||||||
|
$qrCode->getMatrix()->set($matrixX, $matrixY, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function postProcess(
|
||||||
|
QrCode $qrCode,
|
||||||
|
RendererInterface $renderer,
|
||||||
|
$outputWidth,
|
||||||
|
$outputHeight,
|
||||||
|
$leftPadding,
|
||||||
|
$topPadding,
|
||||||
|
$multiple
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private function encodeSvgToBase64(string $filePath): string {
|
||||||
|
return 'data:image/svg+xml;base64,' . base64_encode(file_get_contents($filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
6
common/components/Qr/resources/logo.svg
Normal file
6
common/components/Qr/resources/logo.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 389 365.16" style="fill:#207e5c">
|
||||||
|
<polygon points="130 161 37 161 37 257 145 257 145 285 0 285 0 19 144 19 144 47 37 47 37 133 130 133 130 161 130 161" />
|
||||||
|
<polygon points="213 285 177 285 177 0 213 0 213 285 213 285" />
|
||||||
|
<path d="M319,246.42l34-142h42l-66,227c-5.07,17.07-10.54,30.61-20,39.49S291.1,382.58,279,382.58c-4.67,0-13.89-1.65-21-3.16l4.11-27s18.89,1.78,22.69-4.37c4.54-4.26,8.71-13.46,12.21-24.48l6-23.16-58-196h41l33,142h0Z" transform="translate(-6 -17.42)" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 582 B |
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
return [
|
return [
|
||||||
'version' => '1.1.5',
|
'version' => '1.1.6',
|
||||||
'vendorPath' => dirname(dirname(__DIR__)) . '/vendor',
|
'vendorPath' => dirname(dirname(__DIR__)) . '/vendor',
|
||||||
'components' => [
|
'components' => [
|
||||||
'cache' => [
|
'cache' => [
|
||||||
@ -69,6 +69,15 @@ return [
|
|||||||
'class' => common\components\EmailRenderer::class,
|
'class' => common\components\EmailRenderer::class,
|
||||||
'basePath' => '/images/emails',
|
'basePath' => '/images/emails',
|
||||||
],
|
],
|
||||||
|
'oauth' => [
|
||||||
|
'class' => api\components\OAuth2\Component::class,
|
||||||
|
'grantTypes' => ['authorization_code', 'client_credentials'],
|
||||||
|
'grantMap' => [
|
||||||
|
'authorization_code' => api\components\OAuth2\Grants\AuthCodeGrant::class,
|
||||||
|
'refresh_token' => api\components\OAuth2\Grants\RefreshTokenGrant::class,
|
||||||
|
'client_credentials' => api\components\OAuth2\Grants\ClientCredentialsGrant::class,
|
||||||
|
],
|
||||||
|
],
|
||||||
],
|
],
|
||||||
'aliases' => [
|
'aliases' => [
|
||||||
'@bower' => '@vendor/bower-asset',
|
'@bower' => '@vendor/bower-asset',
|
||||||
|
@ -54,4 +54,9 @@ final class Error {
|
|||||||
const SUBJECT_REQUIRED = 'error.subject_required';
|
const SUBJECT_REQUIRED = 'error.subject_required';
|
||||||
const MESSAGE_REQUIRED = 'error.message_required';
|
const MESSAGE_REQUIRED = 'error.message_required';
|
||||||
|
|
||||||
|
const OTP_TOKEN_REQUIRED = 'error.token_required';
|
||||||
|
const OTP_TOKEN_INCORRECT = 'error.token_incorrect';
|
||||||
|
const OTP_ALREADY_ENABLED = 'error.otp_already_enabled';
|
||||||
|
const OTP_NOT_ENABLED = 'error.otp_not_enabled';
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,8 @@ use const common\LATEST_RULES_VERSION;
|
|||||||
* @property integer $status
|
* @property integer $status
|
||||||
* @property integer $rules_agreement_version
|
* @property integer $rules_agreement_version
|
||||||
* @property string $registration_ip
|
* @property string $registration_ip
|
||||||
|
* @property string $otp_secret
|
||||||
|
* @property integer $is_otp_enabled
|
||||||
* @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
|
||||||
@ -29,10 +31,11 @@ use const common\LATEST_RULES_VERSION;
|
|||||||
* @property string $profileLink ссылка на профиль на Ely без поддержки static url (только для записи)
|
* @property string $profileLink ссылка на профиль на Ely без поддержки static url (только для записи)
|
||||||
*
|
*
|
||||||
* Отношения:
|
* Отношения:
|
||||||
* @property EmailActivation[] $emailActivations
|
* @property EmailActivation[] $emailActivations
|
||||||
* @property OauthSession[] $oauthSessions
|
* @property OauthSession[] $oauthSessions
|
||||||
* @property UsernameHistory[] $usernameHistory
|
* @property UsernameHistory[] $usernameHistory
|
||||||
* @property AccountSession[] $sessions
|
* @property AccountSession[] $sessions
|
||||||
|
* @property MinecraftAccessKey[] $minecraftAccessKeys
|
||||||
*
|
*
|
||||||
* Поведения:
|
* Поведения:
|
||||||
* @mixin TimestampBehavior
|
* @mixin TimestampBehavior
|
||||||
@ -99,7 +102,7 @@ class Account extends ActiveRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function getOauthSessions() {
|
public function getOauthSessions() {
|
||||||
return $this->hasMany(OauthSession::class, ['owner_id' => 'id']);
|
return $this->hasMany(OauthSession::class, ['owner_id' => 'id'])->andWhere(['owner_type' => 'user']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getUsernameHistory() {
|
public function getUsernameHistory() {
|
||||||
@ -110,6 +113,10 @@ class Account extends ActiveRecord {
|
|||||||
return $this->hasMany(AccountSession::class, ['account_id' => 'id']);
|
return $this->hasMany(AccountSession::class, ['account_id' => 'id']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getMinecraftAccessKeys() {
|
||||||
|
return $this->hasMany(MinecraftAccessKey::class, ['account_id' => 'id']);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Выполняет проверку, принадлежит ли этому нику аккаунт у Mojang
|
* Выполняет проверку, принадлежит ли этому нику аккаунт у Mojang
|
||||||
*
|
*
|
||||||
|
@ -1,20 +1,62 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace common\models;
|
namespace common\models;
|
||||||
|
|
||||||
|
use common\components\Annotations\Reader;
|
||||||
|
use ReflectionClass;
|
||||||
|
use Yii;
|
||||||
|
|
||||||
class OauthScope {
|
class OauthScope {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @owner user
|
||||||
|
*/
|
||||||
const OFFLINE_ACCESS = 'offline_access';
|
const OFFLINE_ACCESS = 'offline_access';
|
||||||
|
/**
|
||||||
|
* @owner user
|
||||||
|
*/
|
||||||
const MINECRAFT_SERVER_SESSION = 'minecraft_server_session';
|
const MINECRAFT_SERVER_SESSION = 'minecraft_server_session';
|
||||||
|
/**
|
||||||
|
* @owner user
|
||||||
|
*/
|
||||||
const ACCOUNT_INFO = 'account_info';
|
const ACCOUNT_INFO = 'account_info';
|
||||||
|
/**
|
||||||
|
* @owner user
|
||||||
|
*/
|
||||||
const ACCOUNT_EMAIL = 'account_email';
|
const ACCOUNT_EMAIL = 'account_email';
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* @owner machine
|
||||||
|
*/
|
||||||
|
const ACCOUNT_BLOCK = 'account_block';
|
||||||
|
|
||||||
public static function getScopes() : array {
|
public static function find(): OauthScopeQuery {
|
||||||
return [
|
return new OauthScopeQuery(static::queryScopes());
|
||||||
self::OFFLINE_ACCESS,
|
}
|
||||||
self::MINECRAFT_SERVER_SESSION,
|
|
||||||
self::ACCOUNT_INFO,
|
private static function queryScopes(): array {
|
||||||
self::ACCOUNT_EMAIL,
|
$cacheKey = 'oauth-scopes-list';
|
||||||
];
|
$scopes = false;
|
||||||
|
if ($scopes === false) {
|
||||||
|
$scopes = [];
|
||||||
|
$reflection = new ReflectionClass(static::class);
|
||||||
|
$constants = $reflection->getConstants();
|
||||||
|
$reader = Reader::createFromDefaults();
|
||||||
|
foreach ($constants as $constName => $value) {
|
||||||
|
$annotations = $reader->getConstantAnnotations(static::class, $constName);
|
||||||
|
$isInternal = $annotations->get('internal', false);
|
||||||
|
$owner = $annotations->get('owner', 'user');
|
||||||
|
$keyValue = [
|
||||||
|
'value' => $value,
|
||||||
|
'internal' => $isInternal,
|
||||||
|
'owner' => $owner,
|
||||||
|
];
|
||||||
|
$scopes[$constName] = $keyValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Yii::$app->cache->set($cacheKey, $scopes, 3600);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $scopes;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
50
common/models/OauthScopeQuery.php
Normal file
50
common/models/OauthScopeQuery.php
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
namespace common\models;
|
||||||
|
|
||||||
|
use yii\helpers\ArrayHelper;
|
||||||
|
|
||||||
|
class OauthScopeQuery {
|
||||||
|
|
||||||
|
private $scopes;
|
||||||
|
|
||||||
|
private $internal;
|
||||||
|
|
||||||
|
private $owner;
|
||||||
|
|
||||||
|
public function onlyPublic(): self {
|
||||||
|
$this->internal = false;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onlyInternal(): self {
|
||||||
|
$this->internal = true;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function usersScopes(): self {
|
||||||
|
$this->owner = 'user';
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function machineScopes(): self {
|
||||||
|
$this->owner = 'machine';
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function all(): array {
|
||||||
|
return ArrayHelper::getColumn(array_filter($this->scopes, function($value) {
|
||||||
|
$shouldCheckInternal = $this->internal !== null;
|
||||||
|
$isInternalMatch = $value['internal'] === $this->internal;
|
||||||
|
$shouldCheckOwner = $this->owner !== null;
|
||||||
|
$isOwnerMatch = $value['owner'] === $this->owner;
|
||||||
|
|
||||||
|
return (!$shouldCheckInternal || $isInternalMatch)
|
||||||
|
&& (!$shouldCheckOwner || $isOwnerMatch);
|
||||||
|
}), 'value');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __construct(array $scopes) {
|
||||||
|
$this->scopes = $scopes;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
14
common/models/amqp/AccountBanned.php
Normal file
14
common/models/amqp/AccountBanned.php
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
namespace common\models\amqp;
|
||||||
|
|
||||||
|
use yii\base\Object;
|
||||||
|
|
||||||
|
class AccountBanned extends Object {
|
||||||
|
|
||||||
|
public $accountId;
|
||||||
|
|
||||||
|
public $duration = -1;
|
||||||
|
|
||||||
|
public $message = '';
|
||||||
|
|
||||||
|
}
|
10
common/models/amqp/AccountPardoned.php
Normal file
10
common/models/amqp/AccountPardoned.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
namespace common\models\amqp;
|
||||||
|
|
||||||
|
use yii\base\Object;
|
||||||
|
|
||||||
|
class AccountPardoned extends Object {
|
||||||
|
|
||||||
|
public $accountId;
|
||||||
|
|
||||||
|
}
|
@ -18,7 +18,7 @@
|
|||||||
"yiisoft/yii2": "2.0.10",
|
"yiisoft/yii2": "2.0.10",
|
||||||
"yiisoft/yii2-swiftmailer": "*",
|
"yiisoft/yii2-swiftmailer": "*",
|
||||||
"ramsey/uuid": "^3.5.0",
|
"ramsey/uuid": "^3.5.0",
|
||||||
"league/oauth2-server": "dev-improvements#b9277ccd664dcb80a766b73674d21de686cb9dda",
|
"league/oauth2-server": "dev-improvements#fbaa9b0bd3d8050235ba7dde90f731764122bc20",
|
||||||
"yiisoft/yii2-redis": "~2.0.0",
|
"yiisoft/yii2-redis": "~2.0.0",
|
||||||
"guzzlehttp/guzzle": "^6.0.0",
|
"guzzlehttp/guzzle": "^6.0.0",
|
||||||
"php-amqplib/php-amqplib": "^2.6.2",
|
"php-amqplib/php-amqplib": "^2.6.2",
|
||||||
@ -27,7 +27,10 @@
|
|||||||
"ely/amqp-controller": "dev-master#d7f8cdbc66c45e477c9c7d5d509bc0c1b11fd3ec",
|
"ely/amqp-controller": "dev-master#d7f8cdbc66c45e477c9c7d5d509bc0c1b11fd3ec",
|
||||||
"ely/email-renderer": "dev-master#ef1cb3f7a13196524b97ca5aa0a2d5867f2d9207",
|
"ely/email-renderer": "dev-master#ef1cb3f7a13196524b97ca5aa0a2d5867f2d9207",
|
||||||
"predis/predis": "^1.0",
|
"predis/predis": "^1.0",
|
||||||
"mito/yii2-sentry": "dev-fix_init#27f00805cb906f73b2c6f8181c1c655decb9be70"
|
"mito/yii2-sentry": "dev-fix_init#27f00805cb906f73b2c6f8181c1c655decb9be70",
|
||||||
|
"minime/annotations": "~3.0",
|
||||||
|
"spomky-labs/otphp": "^8.3",
|
||||||
|
"bacon/bacon-qr-code": "^1.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"yiisoft/yii2-codeception": "*",
|
"yiisoft/yii2-codeception": "*",
|
||||||
@ -49,11 +52,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git@gitlab.com:elyby/amqp-controller.git"
|
"url": "git@gitlab.ely.by:elyby/amqp-controller.git"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git@gitlab.com:elyby/email-renderer.git"
|
"url": "git@gitlab.ely.by:elyby/email-renderer.git"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -3,10 +3,13 @@ namespace console\controllers;
|
|||||||
|
|
||||||
use common\components\Mojang\Api as MojangApi;
|
use common\components\Mojang\Api as MojangApi;
|
||||||
use common\components\Mojang\exceptions\NoContentException;
|
use common\components\Mojang\exceptions\NoContentException;
|
||||||
|
use common\models\Account;
|
||||||
|
use common\models\amqp\AccountBanned;
|
||||||
use common\models\amqp\UsernameChanged;
|
use common\models\amqp\UsernameChanged;
|
||||||
use common\models\MojangUsername;
|
use common\models\MojangUsername;
|
||||||
use Ely\Amqp\Builder\Configurator;
|
use Ely\Amqp\Builder\Configurator;
|
||||||
use GuzzleHttp\Exception\RequestException;
|
use GuzzleHttp\Exception\RequestException;
|
||||||
|
use Yii;
|
||||||
|
|
||||||
class AccountQueueController extends AmqpController {
|
class AccountQueueController extends AmqpController {
|
||||||
|
|
||||||
@ -17,16 +20,18 @@ class AccountQueueController extends AmqpController {
|
|||||||
public function configure(Configurator $configurator) {
|
public function configure(Configurator $configurator) {
|
||||||
$configurator->exchange->topic()->durable();
|
$configurator->exchange->topic()->durable();
|
||||||
$configurator->queue->name('accounts-accounts-events')->durable();
|
$configurator->queue->name('accounts-accounts-events')->durable();
|
||||||
$configurator->bind->routingKey('accounts.username-changed');
|
$configurator->bind->routingKey('accounts.username-changed')
|
||||||
|
->add()->routingKey('account.account-banned');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getRoutesMap() {
|
public function getRoutesMap() {
|
||||||
return [
|
return [
|
||||||
'accounts.username-changed' => 'routeUsernameChanged',
|
'accounts.username-changed' => 'routeUsernameChanged',
|
||||||
|
'accounts.account-banned' => 'routeAccountBanned',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function routeUsernameChanged(UsernameChanged $body) {
|
public function routeUsernameChanged(UsernameChanged $body): bool {
|
||||||
$mojangApi = $this->createMojangApi();
|
$mojangApi = $this->createMojangApi();
|
||||||
try {
|
try {
|
||||||
$response = $mojangApi->usernameToUUID($body->newUsername);
|
$response = $mojangApi->usernameToUUID($body->newUsername);
|
||||||
@ -58,10 +63,32 @@ class AccountQueueController extends AmqpController {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function routeAccountBanned(AccountBanned $body): bool {
|
||||||
|
$account = Account::findOne($body->accountId);
|
||||||
|
if ($account === null) {
|
||||||
|
Yii::warning('Cannot find banned account ' . $body->accountId . '. Skipping.');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($account->sessions as $authSession) {
|
||||||
|
$authSession->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($account->minecraftAccessKeys as $key) {
|
||||||
|
$key->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($account->oauthSessions as $oauthSession) {
|
||||||
|
$oauthSession->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return MojangApi
|
* @return MojangApi
|
||||||
*/
|
*/
|
||||||
protected function createMojangApi() : MojangApi {
|
protected function createMojangApi(): MojangApi {
|
||||||
return new MojangApi();
|
return new MojangApi();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use console\db\Migration;
|
||||||
|
|
||||||
|
class m161228_101022_oauth_clients_allow_null_redirect_uri extends Migration {
|
||||||
|
|
||||||
|
public function safeUp() {
|
||||||
|
$this->alterColumn('{{%oauth_clients}}', 'redirect_uri', $this->string());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function safeDown() {
|
||||||
|
$this->alterColumn('{{%oauth_clients}}', 'redirect_uri', $this->string()->notNull());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
17
console/migrations/m170118_224937_account_otp_secrets.php
Normal file
17
console/migrations/m170118_224937_account_otp_secrets.php
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use console\db\Migration;
|
||||||
|
|
||||||
|
class m170118_224937_account_otp_secrets extends Migration {
|
||||||
|
|
||||||
|
public function safeUp() {
|
||||||
|
$this->addColumn('{{%accounts}}', 'otp_secret', $this->string()->after('registration_ip'));
|
||||||
|
$this->addColumn('{{%accounts}}', 'is_otp_enabled', $this->boolean()->notNull()->defaultValue(false)->after('otp_secret'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function safeDown() {
|
||||||
|
$this->dropColumn('{{%accounts}}', 'otp_secret');
|
||||||
|
$this->dropColumn('{{%accounts}}', 'is_otp_enabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -8,15 +8,23 @@ use yii\codeception\BasePage;
|
|||||||
*/
|
*/
|
||||||
class AuthenticationRoute extends BasePage {
|
class AuthenticationRoute extends BasePage {
|
||||||
|
|
||||||
public function login($login = '', $password = '', $rememberMe = false) {
|
/**
|
||||||
|
* @param string $login
|
||||||
|
* @param string $password
|
||||||
|
* @param string|bool|null $rememberMeOrToken
|
||||||
|
* @param bool $rememberMe
|
||||||
|
*/
|
||||||
|
public function login($login = '', $password = '', $rememberMeOrToken = null, $rememberMe = false) {
|
||||||
$this->route = ['authentication/login'];
|
$this->route = ['authentication/login'];
|
||||||
$params = [
|
$params = [
|
||||||
'login' => $login,
|
'login' => $login,
|
||||||
'password' => $password,
|
'password' => $password,
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($rememberMe) {
|
if ((is_bool($rememberMeOrToken) && $rememberMeOrToken) || $rememberMe) {
|
||||||
$params['rememberMe'] = 1;
|
$params['rememberMe'] = 1;
|
||||||
|
} elseif ($rememberMeOrToken !== null) {
|
||||||
|
$params['token'] = $rememberMeOrToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->actor->sendPOST($this->getUrl(), $params);
|
$this->actor->sendPOST($this->getUrl(), $params);
|
||||||
@ -27,10 +35,11 @@ class AuthenticationRoute extends BasePage {
|
|||||||
$this->actor->sendPOST($this->getUrl());
|
$this->actor->sendPOST($this->getUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function forgotPassword($login = '') {
|
public function forgotPassword($login = null, $token = null) {
|
||||||
$this->route = ['authentication/forgot-password'];
|
$this->route = ['authentication/forgot-password'];
|
||||||
$this->actor->sendPOST($this->getUrl(), [
|
$this->actor->sendPOST($this->getUrl(), [
|
||||||
'login' => $login,
|
'login' => $login,
|
||||||
|
'token' => $token,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
21
tests/codeception/api/_pages/InternalRoute.php
Normal file
21
tests/codeception/api/_pages/InternalRoute.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
namespace tests\codeception\api\_pages;
|
||||||
|
|
||||||
|
use yii\codeception\BasePage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property \tests\codeception\api\FunctionalTester $actor
|
||||||
|
*/
|
||||||
|
class InternalRoute extends BasePage {
|
||||||
|
|
||||||
|
public function ban($accountId) {
|
||||||
|
$this->route = '/internal/accounts/' . $accountId . '/ban';
|
||||||
|
$this->actor->sendPOST($this->getUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pardon($accountId) {
|
||||||
|
$this->route = '/internal/accounts/' . $accountId . '/ban';
|
||||||
|
$this->actor->sendDELETE($this->getUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
31
tests/codeception/api/_pages/TwoFactorAuthRoute.php
Normal file
31
tests/codeception/api/_pages/TwoFactorAuthRoute.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
namespace tests\codeception\api\_pages;
|
||||||
|
|
||||||
|
use yii\codeception\BasePage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property \tests\codeception\api\FunctionalTester $actor
|
||||||
|
*/
|
||||||
|
class TwoFactorAuthRoute extends BasePage {
|
||||||
|
|
||||||
|
public $route = '/two-factor-auth';
|
||||||
|
|
||||||
|
public function credentials() {
|
||||||
|
$this->actor->sendGET($this->getUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function enable($token = null, $password = null) {
|
||||||
|
$this->actor->sendPOST($this->getUrl(), [
|
||||||
|
'token' => $token,
|
||||||
|
'password' => $password,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function disable($token = null, $password = null) {
|
||||||
|
$this->actor->sendDELETE($this->getUrl(), [
|
||||||
|
'token' => $token,
|
||||||
|
'password' => $password,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,9 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace tests\codeception\api;
|
namespace tests\codeception\api;
|
||||||
|
|
||||||
|
use api\models\AccountIdentity;
|
||||||
use Codeception\Actor;
|
use Codeception\Actor;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use tests\codeception\api\_pages\AuthenticationRoute;
|
use Yii;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inherited Methods
|
* Inherited Methods
|
||||||
@ -23,20 +24,15 @@ use tests\codeception\api\_pages\AuthenticationRoute;
|
|||||||
class FunctionalTester extends Actor {
|
class FunctionalTester extends Actor {
|
||||||
use _generated\FunctionalTesterActions;
|
use _generated\FunctionalTesterActions;
|
||||||
|
|
||||||
public function loggedInAsActiveAccount($login = null, $password = null) {
|
public function amAuthenticated(string $asUsername = 'admin') {
|
||||||
$route = new AuthenticationRoute($this);
|
/** @var AccountIdentity $account */
|
||||||
if ($login === null) {
|
$account = AccountIdentity::findOne(['username' => $asUsername]);
|
||||||
$route->login('Admin', 'password_0');
|
if ($account === null) {
|
||||||
} elseif ($login !== null && $password !== null) {
|
throw new InvalidArgumentException("Cannot find account for username \"$asUsername\"");
|
||||||
$route->login($login, $password);
|
|
||||||
} else {
|
|
||||||
throw new InvalidArgumentException('login and password should be presented both.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->canSeeResponseIsJson();
|
$result = Yii::$app->user->login($account);
|
||||||
$this->canSeeAuthCredentials(false);
|
$this->amBearerAuthenticated($result->getJwt());
|
||||||
$jwt = $this->grabDataFromResponseByJsonPath('$.access_token')[0];
|
|
||||||
$this->amBearerAuthenticated($jwt);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function notLoggedIn() {
|
public function notLoggedIn() {
|
||||||
|
@ -16,7 +16,7 @@ class AccountsAcceptRulesCest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function testCurrent(FunctionalTester $I) {
|
public function testCurrent(FunctionalTester $I) {
|
||||||
$I->loggedInAsActiveAccount('Veleyaba', 'password_0');
|
$I->amAuthenticated('Veleyaba');
|
||||||
$this->route->acceptRules();
|
$this->route->acceptRules();
|
||||||
$I->canSeeResponseCodeIs(200);
|
$I->canSeeResponseCodeIs(200);
|
||||||
$I->canSeeResponseIsJson();
|
$I->canSeeResponseIsJson();
|
||||||
|
@ -17,7 +17,7 @@ class AccountsChangeEmailConfirmNewEmailCest {
|
|||||||
|
|
||||||
public function testConfirmNewEmail(FunctionalTester $I) {
|
public function testConfirmNewEmail(FunctionalTester $I) {
|
||||||
$I->wantTo('change my email and get changed value');
|
$I->wantTo('change my email and get changed value');
|
||||||
$I->loggedInAsActiveAccount('CrafterGameplays', 'password_0');
|
$I->amAuthenticated('CrafterGameplays');
|
||||||
|
|
||||||
$this->route->changeEmailConfirmNewEmail('H28HBDCHHAG2HGHGHS');
|
$this->route->changeEmailConfirmNewEmail('H28HBDCHHAG2HGHGHS');
|
||||||
$I->canSeeResponseCodeIs(200);
|
$I->canSeeResponseCodeIs(200);
|
||||||
|
@ -17,7 +17,7 @@ class AccountsChangeEmailInitializeCest {
|
|||||||
|
|
||||||
public function testChangeEmailInitialize(FunctionalTester $I) {
|
public function testChangeEmailInitialize(FunctionalTester $I) {
|
||||||
$I->wantTo('send current email confirmation');
|
$I->wantTo('send current email confirmation');
|
||||||
$I->loggedInAsActiveAccount();
|
$I->amAuthenticated();
|
||||||
|
|
||||||
$this->route->changeEmailInitialize('password_0');
|
$this->route->changeEmailInitialize('password_0');
|
||||||
$I->canSeeResponseCodeIs(200);
|
$I->canSeeResponseCodeIs(200);
|
||||||
@ -29,7 +29,7 @@ class AccountsChangeEmailInitializeCest {
|
|||||||
|
|
||||||
public function testChangeEmailInitializeFrequencyError(FunctionalTester $I) {
|
public function testChangeEmailInitializeFrequencyError(FunctionalTester $I) {
|
||||||
$I->wantTo('see change email request frequency error');
|
$I->wantTo('see change email request frequency error');
|
||||||
$I->loggedInAsActiveAccount('ILLIMUNATI', 'password_0');
|
$I->amAuthenticated('ILLIMUNATI');
|
||||||
|
|
||||||
$this->route->changeEmailInitialize('password_0');
|
$this->route->changeEmailInitialize('password_0');
|
||||||
$I->canSeeResponseContainsJson([
|
$I->canSeeResponseContainsJson([
|
||||||
|
@ -18,7 +18,7 @@ class AccountsChangeEmailSubmitNewEmailCest {
|
|||||||
|
|
||||||
public function testSubmitNewEmail(FunctionalTester $I) {
|
public function testSubmitNewEmail(FunctionalTester $I) {
|
||||||
$I->wantTo('submit new email');
|
$I->wantTo('submit new email');
|
||||||
$I->loggedInAsActiveAccount('ILLIMUNATI', 'password_0');
|
$I->amAuthenticated('ILLIMUNATI');
|
||||||
|
|
||||||
$this->route->changeEmailSubmitNewEmail('H27HBDCHHAG2HGHGHS', 'my-new-email@ely.by');
|
$this->route->changeEmailSubmitNewEmail('H27HBDCHHAG2HGHGHS', 'my-new-email@ely.by');
|
||||||
$I->canSeeResponseCodeIs(200);
|
$I->canSeeResponseCodeIs(200);
|
||||||
|
@ -18,7 +18,7 @@ class AccountsChangeLangCest {
|
|||||||
|
|
||||||
public function testSubmitNewEmail(FunctionalTester $I) {
|
public function testSubmitNewEmail(FunctionalTester $I) {
|
||||||
$I->wantTo('change my account language');
|
$I->wantTo('change my account language');
|
||||||
$I->loggedInAsActiveAccount();
|
$I->amAuthenticated();
|
||||||
|
|
||||||
$this->route->changeLang('ru');
|
$this->route->changeLang('ru');
|
||||||
$I->canSeeResponseCodeIs(200);
|
$I->canSeeResponseCodeIs(200);
|
||||||
|
@ -27,7 +27,7 @@ class AccountsChangePasswordCest {
|
|||||||
|
|
||||||
public function testChangePassword(FunctionalTester $I) {
|
public function testChangePassword(FunctionalTester $I) {
|
||||||
$I->wantTo('change my password');
|
$I->wantTo('change my password');
|
||||||
$I->loggedInAsActiveAccount();
|
$I->amAuthenticated();
|
||||||
|
|
||||||
$this->route->changePassword('password_0', 'new-password', 'new-password');
|
$this->route->changePassword('password_0', 'new-password', 'new-password');
|
||||||
$I->canSeeResponseCodeIs(200);
|
$I->canSeeResponseCodeIs(200);
|
||||||
|
@ -26,7 +26,7 @@ class AccountsChangeUsernameCest {
|
|||||||
|
|
||||||
public function testChangeUsername(FunctionalTester $I) {
|
public function testChangeUsername(FunctionalTester $I) {
|
||||||
$I->wantTo('change my nickname');
|
$I->wantTo('change my nickname');
|
||||||
$I->loggedInAsActiveAccount();
|
$I->amAuthenticated();
|
||||||
|
|
||||||
$this->route->changeUsername('password_0', 'bruce_wayne');
|
$this->route->changeUsername('password_0', 'bruce_wayne');
|
||||||
$I->canSeeResponseCodeIs(200);
|
$I->canSeeResponseCodeIs(200);
|
||||||
@ -38,7 +38,7 @@ class AccountsChangeUsernameCest {
|
|||||||
|
|
||||||
public function testChangeUsernameNotAvailable(FunctionalTester $I) {
|
public function testChangeUsernameNotAvailable(FunctionalTester $I) {
|
||||||
$I->wantTo('see, that nickname "in use" is not available');
|
$I->wantTo('see, that nickname "in use" is not available');
|
||||||
$I->loggedInAsActiveAccount();
|
$I->amAuthenticated();
|
||||||
|
|
||||||
$this->route->changeUsername('password_0', 'Jon');
|
$this->route->changeUsername('password_0', 'Jon');
|
||||||
$I->canSeeResponseCodeIs(200);
|
$I->canSeeResponseCodeIs(200);
|
||||||
|
@ -16,7 +16,7 @@ class AccountsCurrentCest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function testCurrent(FunctionalTester $I) {
|
public function testCurrent(FunctionalTester $I) {
|
||||||
$I->loggedInAsActiveAccount();
|
$I->amAuthenticated();
|
||||||
|
|
||||||
$this->route->current();
|
$this->route->current();
|
||||||
$I->canSeeResponseCodeIs(200);
|
$I->canSeeResponseCodeIs(200);
|
||||||
@ -29,6 +29,7 @@ class AccountsCurrentCest {
|
|||||||
'isActive' => true,
|
'isActive' => true,
|
||||||
'hasMojangUsernameCollision' => false,
|
'hasMojangUsernameCollision' => false,
|
||||||
'shouldAcceptRules' => false,
|
'shouldAcceptRules' => false,
|
||||||
|
'isOtpEnabled' => false,
|
||||||
]);
|
]);
|
||||||
$I->canSeeResponseJsonMatchesJsonPath('$.passwordChangedAt');
|
$I->canSeeResponseJsonMatchesJsonPath('$.passwordChangedAt');
|
||||||
}
|
}
|
||||||
|
@ -1,41 +1,87 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace codeception\api\functional;
|
namespace codeception\api\functional;
|
||||||
|
|
||||||
|
use OTPHP\TOTP;
|
||||||
use tests\codeception\api\_pages\AuthenticationRoute;
|
use tests\codeception\api\_pages\AuthenticationRoute;
|
||||||
use tests\codeception\api\FunctionalTester;
|
use tests\codeception\api\FunctionalTester;
|
||||||
|
|
||||||
class ForgotPasswordCest {
|
class ForgotPasswordCest {
|
||||||
|
|
||||||
public function testForgotPasswordByEmail(FunctionalTester $I) {
|
/**
|
||||||
$route = new AuthenticationRoute($I);
|
* @var AuthenticationRoute
|
||||||
|
*/
|
||||||
|
private $route;
|
||||||
|
|
||||||
$I->wantTo('create new password recover request by passing email');
|
public function _before(FunctionalTester $I) {
|
||||||
$route->forgotPassword('admin@ely.by');
|
$this->route = new AuthenticationRoute($I);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWrongInput(FunctionalTester $I) {
|
||||||
|
$I->wantTo('see reaction on invalid input');
|
||||||
|
|
||||||
|
$this->route->forgotPassword();
|
||||||
$I->canSeeResponseContainsJson([
|
$I->canSeeResponseContainsJson([
|
||||||
'success' => true,
|
'success' => false,
|
||||||
|
'errors' => [
|
||||||
|
'login' => 'error.login_required',
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
$I->canSeeResponseJsonMatchesJsonPath('$.data.canRepeatIn');
|
|
||||||
$I->canSeeResponseJsonMatchesJsonPath('$.data.repeatFrequency');
|
$this->route->forgotPassword('becauseimbatman!');
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => false,
|
||||||
|
'errors' => [
|
||||||
|
'login' => 'error.login_not_exist',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->route->forgotPassword('AccountWithEnabledOtp');
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => false,
|
||||||
|
'errors' => [
|
||||||
|
'token' => 'error.token_required',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->route->forgotPassword('AccountWithEnabledOtp');
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => false,
|
||||||
|
'errors' => [
|
||||||
|
'token' => 'error.token_required',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->route->forgotPassword('AccountWithEnabledOtp', '123456');
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => false,
|
||||||
|
'errors' => [
|
||||||
|
'token' => 'error.token_incorrect',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testForgotPasswordByEmail(FunctionalTester $I) {
|
||||||
|
$I->wantTo('create new password recover request by passing email');
|
||||||
|
$this->route->forgotPassword('admin@ely.by');
|
||||||
|
$this->assertSuccessResponse($I, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testForgotPasswordByUsername(FunctionalTester $I) {
|
public function testForgotPasswordByUsername(FunctionalTester $I) {
|
||||||
$route = new AuthenticationRoute($I);
|
|
||||||
|
|
||||||
$I->wantTo('create new password recover request by passing username');
|
$I->wantTo('create new password recover request by passing username');
|
||||||
$route->forgotPassword('Admin');
|
$this->route->forgotPassword('Admin');
|
||||||
$I->canSeeResponseContainsJson([
|
$this->assertSuccessResponse($I, true);
|
||||||
'success' => true,
|
}
|
||||||
]);
|
|
||||||
$I->canSeeResponseJsonMatchesJsonPath('$.data.canRepeatIn');
|
public function testForgotPasswordByAccountWithOtp(FunctionalTester $I) {
|
||||||
$I->canSeeResponseJsonMatchesJsonPath('$.data.repeatFrequency');
|
$I->wantTo('create new password recover request by passing username and otp token');
|
||||||
$I->canSeeResponseJsonMatchesJsonPath('$.data.emailMask');
|
$totp = new TOTP(null, 'secret-secret-secret');
|
||||||
|
$this->route->forgotPassword('AccountWithEnabledOtp', $totp->now());
|
||||||
|
$this->assertSuccessResponse($I, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDataForFrequencyError(FunctionalTester $I) {
|
public function testDataForFrequencyError(FunctionalTester $I) {
|
||||||
$route = new AuthenticationRoute($I);
|
|
||||||
|
|
||||||
$I->wantTo('get info about time to repeat recover password request');
|
$I->wantTo('get info about time to repeat recover password request');
|
||||||
$route->forgotPassword('Notch');
|
$this->route->forgotPassword('Notch');
|
||||||
$I->canSeeResponseContainsJson([
|
$I->canSeeResponseContainsJson([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'errors' => [
|
'errors' => [
|
||||||
@ -46,4 +92,18 @@ class ForgotPasswordCest {
|
|||||||
$I->canSeeResponseJsonMatchesJsonPath('$.data.repeatFrequency');
|
$I->canSeeResponseJsonMatchesJsonPath('$.data.repeatFrequency');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param FunctionalTester $I
|
||||||
|
*/
|
||||||
|
private function assertSuccessResponse(FunctionalTester $I, bool $expectEmailMask = false): void {
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => true,
|
||||||
|
]);
|
||||||
|
$I->canSeeResponseJsonMatchesJsonPath('$.data.canRepeatIn');
|
||||||
|
$I->canSeeResponseJsonMatchesJsonPath('$.data.repeatFrequency');
|
||||||
|
if ($expectEmailMask) {
|
||||||
|
$I->canSeeResponseJsonMatchesJsonPath('$.data.emailMask');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace tests\codeception\api;
|
namespace tests\codeception\api;
|
||||||
|
|
||||||
|
use OTPHP\TOTP;
|
||||||
use tests\codeception\api\_pages\AuthenticationRoute;
|
use tests\codeception\api\_pages\AuthenticationRoute;
|
||||||
|
|
||||||
class LoginCest {
|
class LoginCest {
|
||||||
@ -103,6 +104,56 @@ class LoginCest {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testLoginToken(FunctionalTester $I) {
|
||||||
|
$route = new AuthenticationRoute($I);
|
||||||
|
|
||||||
|
$I->wantTo('see token don\'t have errors if email, username or token not set');
|
||||||
|
$route->login();
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => false,
|
||||||
|
]);
|
||||||
|
$I->cantSeeResponseJsonMatchesJsonPath('$.errors.token');
|
||||||
|
|
||||||
|
$I->wantTo('see token don\'t have errors if username not exists in database');
|
||||||
|
$route->login('non-exist-username', 'random-password');
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => false,
|
||||||
|
]);
|
||||||
|
$I->cantSeeResponseJsonMatchesJsonPath('$.errors.token');
|
||||||
|
|
||||||
|
$I->wantTo('see token don\'t has errors if email not exists in database');
|
||||||
|
$route->login('not-exist@user.com', 'random-password');
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => false,
|
||||||
|
]);
|
||||||
|
$I->cantSeeResponseJsonMatchesJsonPath('$.errors.token');
|
||||||
|
|
||||||
|
$I->wantTo('see token don\'t has errors if email correct, but password wrong');
|
||||||
|
$route->login('not-exist@user.com', 'random-password');
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => false,
|
||||||
|
]);
|
||||||
|
$I->cantSeeResponseJsonMatchesJsonPath('$.errors.token');
|
||||||
|
|
||||||
|
$I->wantTo('see error.token_required if username and password correct, but account have enable otp');
|
||||||
|
$route->login('AccountWithEnabledOtp', 'password_0');
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => false,
|
||||||
|
'errors' => [
|
||||||
|
'token' => 'error.token_required',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$I->wantTo('see error.token_incorrect if username and password correct, but token wrong');
|
||||||
|
$route->login('AccountWithEnabledOtp', 'password_0', '123456');
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => false,
|
||||||
|
'errors' => [
|
||||||
|
'token' => 'error.token_incorrect',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function testLoginByUsernameCorrect(FunctionalTester $I) {
|
public function testLoginByUsernameCorrect(FunctionalTester $I) {
|
||||||
$route = new AuthenticationRoute($I);
|
$route = new AuthenticationRoute($I);
|
||||||
|
|
||||||
@ -151,4 +202,16 @@ class LoginCest {
|
|||||||
$I->canSeeAuthCredentials(true);
|
$I->canSeeAuthCredentials(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testLoginByAccountWithOtp(FunctionalTester $I) {
|
||||||
|
$route = new AuthenticationRoute($I);
|
||||||
|
|
||||||
|
$I->wantTo('login into account with enabled otp');
|
||||||
|
$route->login('AccountWithEnabledOtp', 'password_0', (new TOTP(null, 'secret-secret-secret'))->now());
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => true,
|
||||||
|
]);
|
||||||
|
$I->cantSeeResponseJsonMatchesJsonPath('$.errors');
|
||||||
|
$I->canSeeAuthCredentials(false);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ class LogoutCest {
|
|||||||
public function testLoginEmailOrUsername(FunctionalTester $I) {
|
public function testLoginEmailOrUsername(FunctionalTester $I) {
|
||||||
$route = new AuthenticationRoute($I);
|
$route = new AuthenticationRoute($I);
|
||||||
|
|
||||||
$I->loggedInAsActiveAccount();
|
$I->amAuthenticated();
|
||||||
$route->logout();
|
$route->logout();
|
||||||
$I->canSeeResponseContainsJson([
|
$I->canSeeResponseContainsJson([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
|
@ -16,12 +16,13 @@ class OauthAccessTokenCest {
|
|||||||
$this->route = new OauthRoute($I);
|
$this->route = new OauthRoute($I);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testIssueTokenWithWrongArgs(FunctionalTester $I) {
|
public function testIssueTokenWithWrongArgs(OauthSteps $I) {
|
||||||
$I->wantTo('check behavior on on request without any credentials');
|
$I->wantTo('check behavior on on request without any credentials');
|
||||||
$this->route->issueToken();
|
$this->route->issueToken();
|
||||||
$I->canSeeResponseCodeIs(400);
|
$I->canSeeResponseCodeIs(400);
|
||||||
$I->canSeeResponseContainsJson([
|
$I->canSeeResponseContainsJson([
|
||||||
'error' => 'invalid_request',
|
'error' => 'invalid_request',
|
||||||
|
'message' => 'The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Check the "grant_type" parameter.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$I->wantTo('check behavior on passing invalid auth code');
|
$I->wantTo('check behavior on passing invalid auth code');
|
||||||
@ -34,6 +35,21 @@ class OauthAccessTokenCest {
|
|||||||
$I->canSeeResponseCodeIs(400);
|
$I->canSeeResponseCodeIs(400);
|
||||||
$I->canSeeResponseContainsJson([
|
$I->canSeeResponseContainsJson([
|
||||||
'error' => 'invalid_request',
|
'error' => 'invalid_request',
|
||||||
|
'message' => 'The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Check the "code" parameter.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$authCode = $I->getAuthCode();
|
||||||
|
$I->wantTo('check behavior on passing invalid redirect_uri');
|
||||||
|
$this->route->issueToken($this->buildParams(
|
||||||
|
$authCode,
|
||||||
|
'ely',
|
||||||
|
'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||||
|
'http://some-other.domain'
|
||||||
|
));
|
||||||
|
$I->canSeeResponseCodeIs(401);
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'error' => 'invalid_client',
|
||||||
|
'message' => 'Client authentication failed.',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ class OauthAuthCodeCest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function testValidateWithDescriptionReplaceRequest(FunctionalTester $I) {
|
public function testValidateWithDescriptionReplaceRequest(FunctionalTester $I) {
|
||||||
$I->loggedInAsActiveAccount();
|
$I->amAuthenticated();
|
||||||
$I->wantTo('validate and get information with description replacement');
|
$I->wantTo('validate and get information with description replacement');
|
||||||
$this->route->validate($this->buildQueryParams(
|
$this->route->validate($this->buildQueryParams(
|
||||||
'ely',
|
'ely',
|
||||||
@ -73,13 +73,13 @@ class OauthAuthCodeCest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function testCompleteValidationAction(FunctionalTester $I) {
|
public function testCompleteValidationAction(FunctionalTester $I) {
|
||||||
$I->loggedInAsActiveAccount();
|
$I->amAuthenticated();
|
||||||
$I->wantTo('validate all oAuth params on complete request');
|
$I->wantTo('validate all oAuth params on complete request');
|
||||||
$this->testOauthParamsValidation($I, 'complete');
|
$this->testOauthParamsValidation($I, 'complete');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCompleteActionOnWrongConditions(FunctionalTester $I) {
|
public function testCompleteActionOnWrongConditions(FunctionalTester $I) {
|
||||||
$I->loggedInAsActiveAccount();
|
$I->amAuthenticated();
|
||||||
|
|
||||||
$I->wantTo('get accept_required if I don\'t require any scope, but this is first time request');
|
$I->wantTo('get accept_required if I don\'t require any scope, but this is first time request');
|
||||||
$this->route->complete($this->buildQueryParams(
|
$this->route->complete($this->buildQueryParams(
|
||||||
@ -112,7 +112,7 @@ class OauthAuthCodeCest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function testCompleteActionSuccess(FunctionalTester $I) {
|
public function testCompleteActionSuccess(FunctionalTester $I) {
|
||||||
$I->loggedInAsActiveAccount();
|
$I->amAuthenticated();
|
||||||
$I->wantTo('get auth code if I require some scope and pass accept field');
|
$I->wantTo('get auth code if I require some scope and pass accept field');
|
||||||
$this->route->complete($this->buildQueryParams(
|
$this->route->complete($this->buildQueryParams(
|
||||||
'ely',
|
'ely',
|
||||||
@ -155,7 +155,7 @@ class OauthAuthCodeCest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function testAcceptRequiredOnNewScope(FunctionalTester $I) {
|
public function testAcceptRequiredOnNewScope(FunctionalTester $I) {
|
||||||
$I->loggedInAsActiveAccount();
|
$I->amAuthenticated();
|
||||||
$I->wantTo('get accept_required if I have previous successful request, but now require some new scope');
|
$I->wantTo('get accept_required if I have previous successful request, but now require some new scope');
|
||||||
$this->route->complete($this->buildQueryParams(
|
$this->route->complete($this->buildQueryParams(
|
||||||
'ely',
|
'ely',
|
||||||
@ -179,7 +179,7 @@ class OauthAuthCodeCest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function testCompleteActionWithDismissState(FunctionalTester $I) {
|
public function testCompleteActionWithDismissState(FunctionalTester $I) {
|
||||||
$I->loggedInAsActiveAccount();
|
$I->amAuthenticated();
|
||||||
$I->wantTo('get access_denied error if I pass accept in false state');
|
$I->wantTo('get access_denied error if I pass accept in false state');
|
||||||
$this->route->complete($this->buildQueryParams(
|
$this->route->complete($this->buildQueryParams(
|
||||||
'ely',
|
'ely',
|
||||||
@ -281,6 +281,21 @@ class OauthAuthCodeCest {
|
|||||||
'statusCode' => 400,
|
'statusCode' => 400,
|
||||||
]);
|
]);
|
||||||
$I->canSeeResponseJsonMatchesJsonPath('$.redirectUri');
|
$I->canSeeResponseJsonMatchesJsonPath('$.redirectUri');
|
||||||
|
|
||||||
|
$I->wantTo('check behavior on request internal scope');
|
||||||
|
$this->route->$action($this->buildQueryParams('ely', 'http://ely.by', 'code', [
|
||||||
|
S::MINECRAFT_SERVER_SESSION,
|
||||||
|
S::ACCOUNT_BLOCK,
|
||||||
|
]));
|
||||||
|
$I->canSeeResponseCodeIs(400);
|
||||||
|
$I->canSeeResponseIsJson();
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'invalid_scope',
|
||||||
|
'parameter' => S::ACCOUNT_BLOCK,
|
||||||
|
'statusCode' => 400,
|
||||||
|
]);
|
||||||
|
$I->canSeeResponseJsonMatchesJsonPath('$.redirectUri');
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,120 @@
|
|||||||
|
<?php
|
||||||
|
namespace tests\codeception\api;
|
||||||
|
|
||||||
|
use common\models\OauthScope as S;
|
||||||
|
use tests\codeception\api\_pages\OauthRoute;
|
||||||
|
use tests\codeception\api\functional\_steps\OauthSteps;
|
||||||
|
|
||||||
|
class OauthClientCredentialsGrantCest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var OauthRoute
|
||||||
|
*/
|
||||||
|
private $route;
|
||||||
|
|
||||||
|
public function _before(FunctionalTester $I) {
|
||||||
|
$this->route = new OauthRoute($I);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIssueTokenWithWrongArgs(FunctionalTester $I) {
|
||||||
|
$I->wantTo('check behavior on on request without any credentials');
|
||||||
|
$this->route->issueToken($this->buildParams());
|
||||||
|
$I->canSeeResponseCodeIs(400);
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'error' => 'invalid_request',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$I->wantTo('check behavior on passing invalid client_id');
|
||||||
|
$this->route->issueToken($this->buildParams(
|
||||||
|
'invalid-client',
|
||||||
|
'invalid-secret',
|
||||||
|
['invalid-scope']
|
||||||
|
));
|
||||||
|
$I->canSeeResponseCodeIs(401);
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'error' => 'invalid_client',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$I->wantTo('check behavior on passing invalid client_secret');
|
||||||
|
$this->route->issueToken($this->buildParams(
|
||||||
|
'ely',
|
||||||
|
'invalid-secret',
|
||||||
|
['invalid-scope']
|
||||||
|
));
|
||||||
|
$I->canSeeResponseCodeIs(401);
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'error' => 'invalid_client',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$I->wantTo('check behavior on passing invalid client_secret');
|
||||||
|
$this->route->issueToken($this->buildParams(
|
||||||
|
'ely',
|
||||||
|
'invalid-secret',
|
||||||
|
['invalid-scope']
|
||||||
|
));
|
||||||
|
$I->canSeeResponseCodeIs(401);
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'error' => 'invalid_client',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIssueTokenWithPublicScopes(OauthSteps $I) {
|
||||||
|
// TODO: у нас пока нет публичных скоупов, поэтому тест прогоняется с пустым набором
|
||||||
|
$this->route->issueToken($this->buildParams(
|
||||||
|
'ely',
|
||||||
|
'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||||
|
[]
|
||||||
|
));
|
||||||
|
$I->canSeeResponseCodeIs(200);
|
||||||
|
$I->canSeeResponseIsJson();
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'token_type' => 'Bearer',
|
||||||
|
]);
|
||||||
|
$I->canSeeResponseJsonMatchesJsonPath('$.access_token');
|
||||||
|
$I->canSeeResponseJsonMatchesJsonPath('$.expires_in');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIssueTokenWithInternalScopes(OauthSteps $I) {
|
||||||
|
$this->route->issueToken($this->buildParams(
|
||||||
|
'ely',
|
||||||
|
'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||||
|
[S::ACCOUNT_BLOCK]
|
||||||
|
));
|
||||||
|
$I->canSeeResponseCodeIs(400);
|
||||||
|
$I->canSeeResponseIsJson();
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'error' => 'invalid_scope',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->route->issueToken($this->buildParams(
|
||||||
|
'trusted-client',
|
||||||
|
'tXBbyvMcyaOgHMOAXBpN2EC7uFoJAaL9',
|
||||||
|
[S::ACCOUNT_BLOCK]
|
||||||
|
));
|
||||||
|
$I->canSeeResponseCodeIs(200);
|
||||||
|
$I->canSeeResponseIsJson();
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'token_type' => 'Bearer',
|
||||||
|
]);
|
||||||
|
$I->canSeeResponseJsonMatchesJsonPath('$.access_token');
|
||||||
|
$I->canSeeResponseJsonMatchesJsonPath('$.expires_in');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildParams($clientId = null, $clientSecret = null, array $scopes = null) {
|
||||||
|
$params = ['grant_type' => 'client_credentials'];
|
||||||
|
if ($clientId !== null) {
|
||||||
|
$params['client_id'] = $clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($clientSecret !== null) {
|
||||||
|
$params['client_secret'] = $clientSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($scopes !== null) {
|
||||||
|
$params['scope'] = implode(',', $scopes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $params;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -16,6 +16,18 @@ class OauthRefreshTokenCest {
|
|||||||
$this->route = new OauthRoute($I);
|
$this->route = new OauthRoute($I);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testInvalidRefreshToken(OauthSteps $I) {
|
||||||
|
$this->route->issueToken($this->buildParams(
|
||||||
|
'some-invalid-refresh-token',
|
||||||
|
'ely',
|
||||||
|
'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM'
|
||||||
|
));
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'error' => 'invalid_request',
|
||||||
|
'message' => 'The refresh token is invalid.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function testRefreshToken(OauthSteps $I) {
|
public function testRefreshToken(OauthSteps $I) {
|
||||||
$refreshToken = $I->getRefreshToken();
|
$refreshToken = $I->getRefreshToken();
|
||||||
$this->route->issueToken($this->buildParams(
|
$this->route->issueToken($this->buildParams(
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
namespace tests\codeception\api\functional;
|
||||||
|
|
||||||
|
use tests\codeception\api\_pages\TwoFactorAuthRoute;
|
||||||
|
use tests\codeception\api\FunctionalTester;
|
||||||
|
|
||||||
|
class TwoFactorAuthCredentialsCest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var TwoFactorAuthRoute
|
||||||
|
*/
|
||||||
|
private $route;
|
||||||
|
|
||||||
|
public function _before(FunctionalTester $I) {
|
||||||
|
$this->route = new TwoFactorAuthRoute($I);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetCredentials(FunctionalTester $I) {
|
||||||
|
$I->amAuthenticated();
|
||||||
|
$this->route->credentials();
|
||||||
|
$I->canSeeResponseCodeIs(200);
|
||||||
|
$I->canSeeResponseIsJson();
|
||||||
|
$I->canSeeResponseJsonMatchesJsonPath('$.secret');
|
||||||
|
$I->canSeeResponseJsonMatchesJsonPath('$.uri');
|
||||||
|
$I->canSeeResponseJsonMatchesJsonPath('$.qr');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
namespace tests\codeception\api\functional;
|
||||||
|
|
||||||
|
use OTPHP\TOTP;
|
||||||
|
use tests\codeception\api\_pages\TwoFactorAuthRoute;
|
||||||
|
use tests\codeception\api\FunctionalTester;
|
||||||
|
|
||||||
|
class TwoFactorAuthDisableCest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var TwoFactorAuthRoute
|
||||||
|
*/
|
||||||
|
private $route;
|
||||||
|
|
||||||
|
public function _before(FunctionalTester $I) {
|
||||||
|
$this->route = new TwoFactorAuthRoute($I);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFails(FunctionalTester $I) {
|
||||||
|
$I->amAuthenticated('AccountWithEnabledOtp');
|
||||||
|
|
||||||
|
$this->route->disable();
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => false,
|
||||||
|
'errors' => [
|
||||||
|
'token' => 'error.token_required',
|
||||||
|
'password' => 'error.password_required',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->route->disable('123456', 'invalid_password');
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => false,
|
||||||
|
'errors' => [
|
||||||
|
'token' => 'error.token_incorrect',
|
||||||
|
'password' => 'error.password_incorrect',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$I->amAuthenticated('AccountWithOtpSecret');
|
||||||
|
$this->route->disable('123456', 'invalid_password');
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => false,
|
||||||
|
'errors' => [
|
||||||
|
'account' => 'error.otp_not_enabled',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSuccessEnable(FunctionalTester $I) {
|
||||||
|
$I->amAuthenticated('AccountWithEnabledOtp');
|
||||||
|
$totp = new TOTP(null, 'secret-secret-secret');
|
||||||
|
$this->route->disable($totp->now(), 'password_0');
|
||||||
|
$I->canSeeResponseCodeIs(200);
|
||||||
|
$I->canSeeResponseIsJson();
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
61
tests/codeception/api/functional/TwoFactorAuthEnableCest.php
Normal file
61
tests/codeception/api/functional/TwoFactorAuthEnableCest.php
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
namespace tests\codeception\api\functional;
|
||||||
|
|
||||||
|
use OTPHP\TOTP;
|
||||||
|
use tests\codeception\api\_pages\TwoFactorAuthRoute;
|
||||||
|
use tests\codeception\api\FunctionalTester;
|
||||||
|
|
||||||
|
class TwoFactorAuthEnableCest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var TwoFactorAuthRoute
|
||||||
|
*/
|
||||||
|
private $route;
|
||||||
|
|
||||||
|
public function _before(FunctionalTester $I) {
|
||||||
|
$this->route = new TwoFactorAuthRoute($I);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFails(FunctionalTester $I) {
|
||||||
|
$I->amAuthenticated('AccountWithOtpSecret');
|
||||||
|
|
||||||
|
$this->route->enable();
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => false,
|
||||||
|
'errors' => [
|
||||||
|
'token' => 'error.token_required',
|
||||||
|
'password' => 'error.password_required',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->route->enable('123456', 'invalid_password');
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => false,
|
||||||
|
'errors' => [
|
||||||
|
'token' => 'error.token_incorrect',
|
||||||
|
'password' => 'error.password_incorrect',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$I->amAuthenticated('AccountWithEnabledOtp');
|
||||||
|
$this->route->enable('123456', 'invalid_password');
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => false,
|
||||||
|
'errors' => [
|
||||||
|
'account' => 'error.otp_already_enabled',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSuccessEnable(FunctionalTester $I) {
|
||||||
|
$I->amAuthenticated('AccountWithOtpSecret');
|
||||||
|
$totp = new TOTP(null, 'some otp secret value');
|
||||||
|
$this->route->enable($totp->now(), 'password_0');
|
||||||
|
$I->canSeeResponseCodeIs(200);
|
||||||
|
$I->canSeeResponseIsJson();
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -7,12 +7,12 @@ use tests\codeception\api\FunctionalTester;
|
|||||||
|
|
||||||
class AuthserverSteps extends FunctionalTester {
|
class AuthserverSteps extends FunctionalTester {
|
||||||
|
|
||||||
public function amAuthenticated() {
|
public function amAuthenticated(string $asUsername = 'admin', string $password = 'password_0') {
|
||||||
$route = new AuthserverRoute($this);
|
$route = new AuthserverRoute($this);
|
||||||
$clientToken = Uuid::uuid4()->toString();
|
$clientToken = Uuid::uuid4()->toString();
|
||||||
$route->authenticate([
|
$route->authenticate([
|
||||||
'username' => 'admin',
|
'username' => $asUsername,
|
||||||
'password' => 'password_0',
|
'password' => $password,
|
||||||
'clientToken' => $clientToken,
|
'clientToken' => $clientToken,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -3,12 +3,13 @@ namespace tests\codeception\api\functional\_steps;
|
|||||||
|
|
||||||
use common\models\OauthScope as S;
|
use common\models\OauthScope as S;
|
||||||
use tests\codeception\api\_pages\OauthRoute;
|
use tests\codeception\api\_pages\OauthRoute;
|
||||||
|
use tests\codeception\api\FunctionalTester;
|
||||||
|
|
||||||
class OauthSteps extends \tests\codeception\api\FunctionalTester {
|
class OauthSteps extends FunctionalTester {
|
||||||
|
|
||||||
public function getAuthCode(array $permissions = []) {
|
public function getAuthCode(array $permissions = []) {
|
||||||
// TODO: по идее можно напрямую сделать зпись в базу, что ускорит процесс тестирования
|
// TODO: по идее можно напрямую сделать запись в базу, что ускорит процесс тестирования
|
||||||
$this->loggedInAsActiveAccount();
|
$this->amAuthenticated();
|
||||||
$route = new OauthRoute($this);
|
$route = new OauthRoute($this);
|
||||||
$route->complete([
|
$route->complete([
|
||||||
'client_id' => 'ely',
|
'client_id' => 'ely',
|
||||||
@ -31,7 +32,7 @@ class OauthSteps extends \tests\codeception\api\FunctionalTester {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function getRefreshToken(array $permissions = []) {
|
public function getRefreshToken(array $permissions = []) {
|
||||||
// TODO: по идее можно напрямую сделать зпись в базу, что ускорит процесс тестирования
|
// TODO: по идее можно напрямую сделать запись в базу, что ускорит процесс тестирования
|
||||||
$authCode = $this->getAuthCode(array_merge([S::OFFLINE_ACCESS], $permissions));
|
$authCode = $this->getAuthCode(array_merge([S::OFFLINE_ACCESS], $permissions));
|
||||||
$response = $this->issueToken($authCode);
|
$response = $this->issueToken($authCode);
|
||||||
|
|
||||||
@ -51,4 +52,18 @@ class OauthSteps extends \tests\codeception\api\FunctionalTester {
|
|||||||
return json_decode($this->grabResponse(), true);
|
return json_decode($this->grabResponse(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getAccessTokenByClientCredentialsGrant(array $permissions = [], $useTrusted = true) {
|
||||||
|
$route = new OauthRoute($this);
|
||||||
|
$route->issueToken([
|
||||||
|
'client_id' => $useTrusted ? 'trusted-client' : 'default-client',
|
||||||
|
'client_secret' => $useTrusted ? 'tXBbyvMcyaOgHMOAXBpN2EC7uFoJAaL9' : 'AzWRy7ZjS1yRQUk2vRBDic8fprOKDB1W',
|
||||||
|
'grant_type' => 'client_credentials',
|
||||||
|
'scope' => implode(',', $permissions),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = json_decode($this->grabResponse(), true);
|
||||||
|
|
||||||
|
return $response['access_token'];
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ class InvalidateCest {
|
|||||||
|
|
||||||
public function invalidate(AuthserverSteps $I) {
|
public function invalidate(AuthserverSteps $I) {
|
||||||
$I->wantTo('invalidate my token');
|
$I->wantTo('invalidate my token');
|
||||||
list($accessToken, $clientToken) = $I->amAuthenticated();
|
[$accessToken, $clientToken] = $I->amAuthenticated();
|
||||||
$this->route->invalidate([
|
$this->route->invalidate([
|
||||||
'accessToken' => $accessToken,
|
'accessToken' => $accessToken,
|
||||||
'clientToken' => $clientToken,
|
'clientToken' => $clientToken,
|
||||||
|
@ -18,7 +18,7 @@ class RefreshCest {
|
|||||||
|
|
||||||
public function refresh(AuthserverSteps $I) {
|
public function refresh(AuthserverSteps $I) {
|
||||||
$I->wantTo('refresh my accessToken');
|
$I->wantTo('refresh my accessToken');
|
||||||
list($accessToken, $clientToken) = $I->amAuthenticated();
|
[$accessToken, $clientToken] = $I->amAuthenticated();
|
||||||
$this->route->refresh([
|
$this->route->refresh([
|
||||||
'accessToken' => $accessToken,
|
'accessToken' => $accessToken,
|
||||||
'clientToken' => $clientToken,
|
'clientToken' => $clientToken,
|
||||||
|
@ -18,7 +18,7 @@ class ValidateCest {
|
|||||||
|
|
||||||
public function validate(AuthserverSteps $I) {
|
public function validate(AuthserverSteps $I) {
|
||||||
$I->wantTo('validate my accessToken');
|
$I->wantTo('validate my accessToken');
|
||||||
list($accessToken) = $I->amAuthenticated();
|
[$accessToken] = $I->amAuthenticated();
|
||||||
$this->route->validate([
|
$this->route->validate([
|
||||||
'accessToken' => $accessToken,
|
'accessToken' => $accessToken,
|
||||||
]);
|
]);
|
||||||
|
47
tests/codeception/api/functional/internal/BanCest.php
Normal file
47
tests/codeception/api/functional/internal/BanCest.php
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
namespace tests\codeception\api\functional\internal;
|
||||||
|
|
||||||
|
use common\models\OauthScope as S;
|
||||||
|
use tests\codeception\api\_pages\InternalRoute;
|
||||||
|
use tests\codeception\api\functional\_steps\OauthSteps;
|
||||||
|
use tests\codeception\api\FunctionalTester;
|
||||||
|
|
||||||
|
class BanCest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var InternalRoute
|
||||||
|
*/
|
||||||
|
private $route;
|
||||||
|
|
||||||
|
public function _before(FunctionalTester $I) {
|
||||||
|
$this->route = new InternalRoute($I);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBanAccount(OauthSteps $I) {
|
||||||
|
$accessToken = $I->getAccessTokenByClientCredentialsGrant([S::ACCOUNT_BLOCK]);
|
||||||
|
$I->amBearerAuthenticated($accessToken);
|
||||||
|
|
||||||
|
$this->route->ban(1);
|
||||||
|
$I->canSeeResponseCodeIs(200);
|
||||||
|
$I->canSeeResponseIsJson();
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBanBannedAccount(OauthSteps $I) {
|
||||||
|
$accessToken = $I->getAccessTokenByClientCredentialsGrant([S::ACCOUNT_BLOCK]);
|
||||||
|
$I->amBearerAuthenticated($accessToken);
|
||||||
|
|
||||||
|
$this->route->ban(10);
|
||||||
|
$I->canSeeResponseCodeIs(200);
|
||||||
|
$I->canSeeResponseIsJson();
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => false,
|
||||||
|
'errors' => [
|
||||||
|
'account' => 'error.account_already_banned',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
47
tests/codeception/api/functional/internal/PardonCest.php
Normal file
47
tests/codeception/api/functional/internal/PardonCest.php
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
namespace tests\codeception\api\functional\internal;
|
||||||
|
|
||||||
|
use common\models\OauthScope as S;
|
||||||
|
use tests\codeception\api\_pages\InternalRoute;
|
||||||
|
use tests\codeception\api\functional\_steps\OauthSteps;
|
||||||
|
use tests\codeception\api\FunctionalTester;
|
||||||
|
|
||||||
|
class PardonCest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var InternalRoute
|
||||||
|
*/
|
||||||
|
private $route;
|
||||||
|
|
||||||
|
public function _before(FunctionalTester $I) {
|
||||||
|
$this->route = new InternalRoute($I);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPardonAccount(OauthSteps $I) {
|
||||||
|
$accessToken = $I->getAccessTokenByClientCredentialsGrant([S::ACCOUNT_BLOCK]);
|
||||||
|
$I->amBearerAuthenticated($accessToken);
|
||||||
|
|
||||||
|
$this->route->pardon(10);
|
||||||
|
$I->canSeeResponseCodeIs(200);
|
||||||
|
$I->canSeeResponseIsJson();
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPardonNotBannedAccount(OauthSteps $I) {
|
||||||
|
$accessToken = $I->getAccessTokenByClientCredentialsGrant([S::ACCOUNT_BLOCK]);
|
||||||
|
$I->amBearerAuthenticated($accessToken);
|
||||||
|
|
||||||
|
$this->route->pardon(1);
|
||||||
|
$I->canSeeResponseCodeIs(200);
|
||||||
|
$I->canSeeResponseIsJson();
|
||||||
|
$I->canSeeResponseContainsJson([
|
||||||
|
'success' => false,
|
||||||
|
'errors' => [
|
||||||
|
'account' => 'error.account_not_banned',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -44,7 +44,7 @@ class HasJoinedLegacyCest {
|
|||||||
'user' => 'random-username',
|
'user' => 'random-username',
|
||||||
'serverId' => Uuid::uuid(),
|
'serverId' => Uuid::uuid(),
|
||||||
]);
|
]);
|
||||||
$I->seeResponseCodeIs(401);
|
$I->seeResponseCodeIs(200);
|
||||||
$I->canSeeResponseEquals('NO');
|
$I->canSeeResponseEquals('NO');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ class JoinCest {
|
|||||||
|
|
||||||
public function joinByLegacyAuthserver(AuthserverSteps $I) {
|
public function joinByLegacyAuthserver(AuthserverSteps $I) {
|
||||||
$I->wantTo('join to server, using legacy authserver access token');
|
$I->wantTo('join to server, using legacy authserver access token');
|
||||||
list($accessToken) = $I->amAuthenticated();
|
[$accessToken] = $I->amAuthenticated();
|
||||||
$this->route->join([
|
$this->route->join([
|
||||||
'accessToken' => $accessToken,
|
'accessToken' => $accessToken,
|
||||||
'selectedProfile' => 'df936908-b2e1-544d-96f8-2977ec213022',
|
'selectedProfile' => 'df936908-b2e1-544d-96f8-2977ec213022',
|
||||||
@ -32,7 +32,7 @@ class JoinCest {
|
|||||||
|
|
||||||
public function joinByPassJsonInPost(AuthserverSteps $I) {
|
public function joinByPassJsonInPost(AuthserverSteps $I) {
|
||||||
$I->wantTo('join to server, passing data in body as encoded json');
|
$I->wantTo('join to server, passing data in body as encoded json');
|
||||||
list($accessToken) = $I->amAuthenticated();
|
[$accessToken] = $I->amAuthenticated();
|
||||||
$this->route->join(json_encode([
|
$this->route->join(json_encode([
|
||||||
'accessToken' => $accessToken,
|
'accessToken' => $accessToken,
|
||||||
'selectedProfile' => 'df936908-b2e1-544d-96f8-2977ec213022',
|
'selectedProfile' => 'df936908-b2e1-544d-96f8-2977ec213022',
|
||||||
|
@ -21,7 +21,7 @@ class JoinLegacyCest {
|
|||||||
|
|
||||||
public function joinByLegacyAuthserver(AuthserverSteps $I) {
|
public function joinByLegacyAuthserver(AuthserverSteps $I) {
|
||||||
$I->wantTo('join to server by legacy protocol, using legacy authserver access token');
|
$I->wantTo('join to server by legacy protocol, using legacy authserver access token');
|
||||||
list($accessToken) = $I->amAuthenticated();
|
[$accessToken] = $I->amAuthenticated();
|
||||||
$this->route->joinLegacy([
|
$this->route->joinLegacy([
|
||||||
'sessionId' => $accessToken,
|
'sessionId' => $accessToken,
|
||||||
'user' => 'Admin',
|
'user' => 'Admin',
|
||||||
@ -32,7 +32,7 @@ class JoinLegacyCest {
|
|||||||
|
|
||||||
public function joinByNewSessionFormat(AuthserverSteps $I) {
|
public function joinByNewSessionFormat(AuthserverSteps $I) {
|
||||||
$I->wantTo('join to server by legacy protocol with new launcher session format, using legacy authserver');
|
$I->wantTo('join to server by legacy protocol with new launcher session format, using legacy authserver');
|
||||||
list($accessToken) = $I->amAuthenticated();
|
[$accessToken] = $I->amAuthenticated();
|
||||||
$this->route->joinLegacy([
|
$this->route->joinLegacy([
|
||||||
'sessionId' => 'token:' . $accessToken . ':' . 'df936908-b2e1-544d-96f8-2977ec213022',
|
'sessionId' => 'token:' . $accessToken . ':' . 'df936908-b2e1-544d-96f8-2977ec213022',
|
||||||
'user' => 'Admin',
|
'user' => 'Admin',
|
||||||
|
@ -4,6 +4,7 @@ namespace codeception\api\unit\models\authentication;
|
|||||||
use api\models\authentication\ForgotPasswordForm;
|
use api\models\authentication\ForgotPasswordForm;
|
||||||
use Codeception\Specify;
|
use Codeception\Specify;
|
||||||
use common\models\EmailActivation;
|
use common\models\EmailActivation;
|
||||||
|
use OTPHP\TOTP;
|
||||||
use tests\codeception\api\unit\TestCase;
|
use tests\codeception\api\unit\TestCase;
|
||||||
use tests\codeception\common\fixtures\AccountFixture;
|
use tests\codeception\common\fixtures\AccountFixture;
|
||||||
use tests\codeception\common\fixtures\EmailActivationFixture;
|
use tests\codeception\common\fixtures\EmailActivationFixture;
|
||||||
@ -18,7 +19,7 @@ class ForgotPasswordFormTest extends TestCase {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testValidateAccount() {
|
public function testValidateLogin() {
|
||||||
$this->specify('error.login_not_exist if login is invalid', function() {
|
$this->specify('error.login_not_exist if login is invalid', function() {
|
||||||
$model = new ForgotPasswordForm(['login' => 'unexist']);
|
$model = new ForgotPasswordForm(['login' => 'unexist']);
|
||||||
$model->validateLogin('login');
|
$model->validateLogin('login');
|
||||||
@ -32,6 +33,21 @@ class ForgotPasswordFormTest extends TestCase {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testValidateTotpToken() {
|
||||||
|
$model = new ForgotPasswordForm();
|
||||||
|
$model->login = 'AccountWithEnabledOtp';
|
||||||
|
$model->token = '123456';
|
||||||
|
$model->validateTotpToken('token');
|
||||||
|
$this->assertEquals(['error.token_incorrect'], $model->getErrors('token'));
|
||||||
|
|
||||||
|
$totp = new TOTP(null, 'secret-secret-secret');
|
||||||
|
$model = new ForgotPasswordForm();
|
||||||
|
$model->login = 'AccountWithEnabledOtp';
|
||||||
|
$model->token = $totp->now();
|
||||||
|
$model->validateTotpToken('token');
|
||||||
|
$this->assertEmpty($model->getErrors('token'));
|
||||||
|
}
|
||||||
|
|
||||||
public function testValidateActivity() {
|
public function testValidateActivity() {
|
||||||
$this->specify('error.account_not_activated if account is not confirmed', function() {
|
$this->specify('error.account_not_activated if account is not confirmed', function() {
|
||||||
$model = new ForgotPasswordForm([
|
$model = new ForgotPasswordForm([
|
||||||
|
@ -6,6 +6,7 @@ use api\models\AccountIdentity;
|
|||||||
use api\models\authentication\LoginForm;
|
use api\models\authentication\LoginForm;
|
||||||
use Codeception\Specify;
|
use Codeception\Specify;
|
||||||
use common\models\Account;
|
use common\models\Account;
|
||||||
|
use OTPHP\TOTP;
|
||||||
use tests\codeception\api\unit\TestCase;
|
use tests\codeception\api\unit\TestCase;
|
||||||
use tests\codeception\common\fixtures\AccountFixture;
|
use tests\codeception\common\fixtures\AccountFixture;
|
||||||
|
|
||||||
@ -38,7 +39,7 @@ class LoginFormTest extends TestCase {
|
|||||||
'account' => null,
|
'account' => null,
|
||||||
]);
|
]);
|
||||||
$model->validateLogin('login');
|
$model->validateLogin('login');
|
||||||
expect($model->getErrors('login'))->equals(['error.login_not_exist']);
|
$this->assertEquals(['error.login_not_exist'], $model->getErrors('login'));
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->specify('no errors if login exists', function () {
|
$this->specify('no errors if login exists', function () {
|
||||||
@ -47,7 +48,7 @@ class LoginFormTest extends TestCase {
|
|||||||
'account' => new AccountIdentity(),
|
'account' => new AccountIdentity(),
|
||||||
]);
|
]);
|
||||||
$model->validateLogin('login');
|
$model->validateLogin('login');
|
||||||
expect($model->getErrors('login'))->isEmpty();
|
$this->assertEmpty($model->getErrors('login'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +59,7 @@ class LoginFormTest extends TestCase {
|
|||||||
'account' => new AccountIdentity(['password' => '12345678']),
|
'account' => new AccountIdentity(['password' => '12345678']),
|
||||||
]);
|
]);
|
||||||
$model->validatePassword('password');
|
$model->validatePassword('password');
|
||||||
expect($model->getErrors('password'))->equals(['error.password_incorrect']);
|
$this->assertEquals(['error.password_incorrect'], $model->getErrors('password'));
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->specify('no errors if password valid', function () {
|
$this->specify('no errors if password valid', function () {
|
||||||
@ -67,7 +68,35 @@ class LoginFormTest extends TestCase {
|
|||||||
'account' => new AccountIdentity(['password' => '12345678']),
|
'account' => new AccountIdentity(['password' => '12345678']),
|
||||||
]);
|
]);
|
||||||
$model->validatePassword('password');
|
$model->validatePassword('password');
|
||||||
expect($model->getErrors('password'))->isEmpty();
|
$this->assertEmpty($model->getErrors('password'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateTotpToken() {
|
||||||
|
$account = new AccountIdentity(['password' => '12345678']);
|
||||||
|
$account->password = '12345678';
|
||||||
|
$account->is_otp_enabled = true;
|
||||||
|
$account->otp_secret = 'mock secret';
|
||||||
|
|
||||||
|
$this->specify('error.token_incorrect if totp invalid', function() use ($account) {
|
||||||
|
$model = $this->createModel([
|
||||||
|
'password' => '12345678',
|
||||||
|
'token' => '321123',
|
||||||
|
'account' => $account,
|
||||||
|
]);
|
||||||
|
$model->validateTotpToken('token');
|
||||||
|
$this->assertEquals(['error.token_incorrect'], $model->getErrors('token'));
|
||||||
|
});
|
||||||
|
|
||||||
|
$totp = new TOTP(null, 'mock secret');
|
||||||
|
$this->specify('no errors if password valid', function() use ($account, $totp) {
|
||||||
|
$model = $this->createModel([
|
||||||
|
'password' => '12345678',
|
||||||
|
'token' => $totp->now(),
|
||||||
|
'account' => $account,
|
||||||
|
]);
|
||||||
|
$model->validateTotpToken('token');
|
||||||
|
$this->assertEmpty($model->getErrors('token'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,7 +106,7 @@ class LoginFormTest extends TestCase {
|
|||||||
'account' => new AccountIdentity(['status' => Account::STATUS_REGISTERED]),
|
'account' => new AccountIdentity(['status' => Account::STATUS_REGISTERED]),
|
||||||
]);
|
]);
|
||||||
$model->validateActivity('login');
|
$model->validateActivity('login');
|
||||||
expect($model->getErrors('login'))->equals(['error.account_not_activated']);
|
$this->assertEquals(['error.account_not_activated'], $model->getErrors('login'));
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->specify('error.account_banned if account has banned status', function () {
|
$this->specify('error.account_banned if account has banned status', function () {
|
||||||
@ -85,7 +114,7 @@ class LoginFormTest extends TestCase {
|
|||||||
'account' => new AccountIdentity(['status' => Account::STATUS_BANNED]),
|
'account' => new AccountIdentity(['status' => Account::STATUS_BANNED]),
|
||||||
]);
|
]);
|
||||||
$model->validateActivity('login');
|
$model->validateActivity('login');
|
||||||
expect($model->getErrors('login'))->equals(['error.account_banned']);
|
$this->assertEquals(['error.account_banned'], $model->getErrors('login'));
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->specify('no errors if account active', function () {
|
$this->specify('no errors if account active', function () {
|
||||||
@ -93,36 +122,36 @@ class LoginFormTest extends TestCase {
|
|||||||
'account' => new AccountIdentity(['status' => Account::STATUS_ACTIVE]),
|
'account' => new AccountIdentity(['status' => Account::STATUS_ACTIVE]),
|
||||||
]);
|
]);
|
||||||
$model->validateActivity('login');
|
$model->validateActivity('login');
|
||||||
expect($model->getErrors('login'))->isEmpty();
|
$this->assertEmpty($model->getErrors('login'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testLogin() {
|
public function testLogin() {
|
||||||
$this->specify('user should be able to login with correct username and password', function () {
|
$model = $this->createModel([
|
||||||
$model = $this->createModel([
|
'login' => 'erickskrauch',
|
||||||
'login' => 'erickskrauch',
|
'password' => '12345678',
|
||||||
|
'account' => new AccountIdentity([
|
||||||
|
'username' => 'erickskrauch',
|
||||||
'password' => '12345678',
|
'password' => '12345678',
|
||||||
'account' => new AccountIdentity([
|
'status' => Account::STATUS_ACTIVE,
|
||||||
'username' => 'erickskrauch',
|
]),
|
||||||
'password' => '12345678',
|
]);
|
||||||
'status' => Account::STATUS_ACTIVE,
|
$this->assertInstanceOf(LoginResult::class, $model->login(), 'model should login user');
|
||||||
]),
|
$this->assertEmpty($model->getErrors(), 'error message should not be set');
|
||||||
]);
|
|
||||||
expect('model should login user', $model->login())->isInstanceOf(LoginResult::class);
|
|
||||||
expect('error message should not be set', $model->errors)->isEmpty();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testLoginWithRehashing() {
|
public function testLoginWithRehashing() {
|
||||||
$this->specify('user, that login using account with old pass hash strategy should update it automatically', function () {
|
$model = new LoginForm([
|
||||||
$model = new LoginForm([
|
'login' => $this->tester->grabFixture('accounts', 'user-with-old-password-type')['username'],
|
||||||
'login' => $this->tester->grabFixture('accounts', 'user-with-old-password-type')['username'],
|
'password' => '12345678',
|
||||||
'password' => '12345678',
|
]);
|
||||||
]);
|
$this->assertInstanceOf(LoginResult::class, $model->login());
|
||||||
expect($model->login())->isInstanceOf(LoginResult::class);
|
$this->assertEmpty($model->getErrors());
|
||||||
expect($model->errors)->isEmpty();
|
$this->assertEquals(
|
||||||
expect($model->getAccount()->password_hash_strategy)->equals(Account::PASS_HASH_STRATEGY_YII2);
|
Account::PASS_HASH_STRATEGY_YII2,
|
||||||
});
|
$model->getAccount()->password_hash_strategy,
|
||||||
|
'user, that login using account with old pass hash strategy should update it automatically'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -0,0 +1,165 @@
|
|||||||
|
<?php
|
||||||
|
namespace tests\codeception\api\unit\models\profile;
|
||||||
|
|
||||||
|
use api\models\profile\TwoFactorAuthForm;
|
||||||
|
use common\helpers\Error as E;
|
||||||
|
use common\models\Account;
|
||||||
|
use OTPHP\TOTP;
|
||||||
|
use tests\codeception\api\unit\TestCase;
|
||||||
|
|
||||||
|
class TwoFactorAuthFormTest extends TestCase {
|
||||||
|
|
||||||
|
public function testGetCredentials() {
|
||||||
|
/** @var Account|\PHPUnit_Framework_MockObject_MockObject $account */
|
||||||
|
$account = $this->getMockBuilder(Account::class)
|
||||||
|
->setMethods(['save'])
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
$account->expects($this->once())
|
||||||
|
->method('save')
|
||||||
|
->willReturn(true);
|
||||||
|
|
||||||
|
$account->email = 'mock@email.com';
|
||||||
|
$account->otp_secret = null;
|
||||||
|
|
||||||
|
/** @var TwoFactorAuthForm|\PHPUnit_Framework_MockObject_MockObject $model */
|
||||||
|
$model = $this->getMockBuilder(TwoFactorAuthForm::class)
|
||||||
|
->setConstructorArgs([$account])
|
||||||
|
->setMethods(['drawQrCode'])
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
$model->expects($this->once())
|
||||||
|
->method('drawQrCode')
|
||||||
|
->willReturn('this is qr code, trust me');
|
||||||
|
|
||||||
|
$result = $model->getCredentials();
|
||||||
|
$this->assertTrue(is_array($result));
|
||||||
|
$this->assertArrayHasKey('qr', $result);
|
||||||
|
$this->assertArrayHasKey('uri', $result);
|
||||||
|
$this->assertArrayHasKey('secret', $result);
|
||||||
|
$this->assertNotNull($account->otp_secret);
|
||||||
|
$this->assertEquals($account->otp_secret, $result['secret']);
|
||||||
|
$this->assertEquals(base64_encode('this is qr code, trust me'), $result['qr']);
|
||||||
|
|
||||||
|
/** @var Account|\PHPUnit_Framework_MockObject_MockObject $account */
|
||||||
|
$account = $this->getMockBuilder(Account::class)
|
||||||
|
->setMethods(['save'])
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
$account->expects($this->never())
|
||||||
|
->method('save');
|
||||||
|
|
||||||
|
$account->email = 'mock@email.com';
|
||||||
|
$account->otp_secret = 'some valid totp secret value';
|
||||||
|
|
||||||
|
/** @var TwoFactorAuthForm|\PHPUnit_Framework_MockObject_MockObject $model */
|
||||||
|
$model = $this->getMockBuilder(TwoFactorAuthForm::class)
|
||||||
|
->setConstructorArgs([$account])
|
||||||
|
->setMethods(['drawQrCode'])
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
$model->expects($this->once())
|
||||||
|
->method('drawQrCode')
|
||||||
|
->willReturn('this is qr code, trust me');
|
||||||
|
|
||||||
|
$result = $model->getCredentials();
|
||||||
|
$this->assertEquals('some valid totp secret value', $result['secret']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testActivate() {
|
||||||
|
/** @var Account|\PHPUnit_Framework_MockObject_MockObject $account */
|
||||||
|
$account = $this->getMockBuilder(Account::class)
|
||||||
|
->setMethods(['save'])
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
$account->expects($this->once())
|
||||||
|
->method('save')
|
||||||
|
->willReturn(true);
|
||||||
|
|
||||||
|
$account->is_otp_enabled = false;
|
||||||
|
$account->otp_secret = 'mock secret';
|
||||||
|
|
||||||
|
/** @var TwoFactorAuthForm|\PHPUnit_Framework_MockObject_MockObject $model */
|
||||||
|
$model = $this->getMockBuilder(TwoFactorAuthForm::class)
|
||||||
|
->setMethods(['validate'])
|
||||||
|
->setConstructorArgs([$account, ['scenario' => TwoFactorAuthForm::SCENARIO_ACTIVATE]])
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
$model->expects($this->once())
|
||||||
|
->method('validate')
|
||||||
|
->willReturn(true);
|
||||||
|
|
||||||
|
$this->assertTrue($model->activate());
|
||||||
|
$this->assertTrue($account->is_otp_enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDisable() {
|
||||||
|
/** @var Account|\PHPUnit_Framework_MockObject_MockObject $account */
|
||||||
|
$account = $this->getMockBuilder(Account::class)
|
||||||
|
->setMethods(['save'])
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
$account->expects($this->once())
|
||||||
|
->method('save')
|
||||||
|
->willReturn(true);
|
||||||
|
|
||||||
|
$account->is_otp_enabled = true;
|
||||||
|
$account->otp_secret = 'mock secret';
|
||||||
|
|
||||||
|
/** @var TwoFactorAuthForm|\PHPUnit_Framework_MockObject_MockObject $model */
|
||||||
|
$model = $this->getMockBuilder(TwoFactorAuthForm::class)
|
||||||
|
->setMethods(['validate'])
|
||||||
|
->setConstructorArgs([$account, ['scenario' => TwoFactorAuthForm::SCENARIO_DISABLE]])
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
$model->expects($this->once())
|
||||||
|
->method('validate')
|
||||||
|
->willReturn(true);
|
||||||
|
|
||||||
|
$this->assertTrue($model->disable());
|
||||||
|
$this->assertNull($account->otp_secret);
|
||||||
|
$this->assertFalse($account->is_otp_enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateOtpDisabled() {
|
||||||
|
$account = new Account();
|
||||||
|
$account->is_otp_enabled = true;
|
||||||
|
$model = new TwoFactorAuthForm($account);
|
||||||
|
$model->validateOtpDisabled('account');
|
||||||
|
$this->assertEquals([E::OTP_ALREADY_ENABLED], $model->getErrors('account'));
|
||||||
|
|
||||||
|
$account = new Account();
|
||||||
|
$account->is_otp_enabled = false;
|
||||||
|
$model = new TwoFactorAuthForm($account);
|
||||||
|
$model->validateOtpDisabled('account');
|
||||||
|
$this->assertEmpty($model->getErrors('account'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateOtpEnabled() {
|
||||||
|
$account = new Account();
|
||||||
|
$account->is_otp_enabled = false;
|
||||||
|
$model = new TwoFactorAuthForm($account);
|
||||||
|
$model->validateOtpEnabled('account');
|
||||||
|
$this->assertEquals([E::OTP_NOT_ENABLED], $model->getErrors('account'));
|
||||||
|
|
||||||
|
$account = new Account();
|
||||||
|
$account->is_otp_enabled = true;
|
||||||
|
$model = new TwoFactorAuthForm($account);
|
||||||
|
$model->validateOtpEnabled('account');
|
||||||
|
$this->assertEmpty($model->getErrors('account'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetTotp() {
|
||||||
|
$account = new Account();
|
||||||
|
$account->otp_secret = 'mock secret';
|
||||||
|
$account->email = 'check@this.email';
|
||||||
|
|
||||||
|
$model = new TwoFactorAuthForm($account);
|
||||||
|
$totp = $model->getTotp();
|
||||||
|
$this->assertInstanceOf(TOTP::class, $totp);
|
||||||
|
$this->assertEquals('check@this.email', $totp->getLabel());
|
||||||
|
$this->assertEquals('mock secret', $totp->getSecret());
|
||||||
|
$this->assertEquals('Ely.by', $totp->getIssuer());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
namespace tests\codeception\api\unit\modules\internal\models;
|
||||||
|
|
||||||
|
use api\modules\internal\helpers\Error as E;
|
||||||
|
use api\modules\internal\models\BanForm;
|
||||||
|
use common\models\Account;
|
||||||
|
use tests\codeception\api\unit\TestCase;
|
||||||
|
|
||||||
|
class BanFormTest extends TestCase {
|
||||||
|
|
||||||
|
public function testValidateAccountActivity() {
|
||||||
|
$account = new Account();
|
||||||
|
$account->status = Account::STATUS_ACTIVE;
|
||||||
|
$form = new BanForm($account);
|
||||||
|
$form->validateAccountActivity();
|
||||||
|
$this->assertEmpty($form->getErrors('account'));
|
||||||
|
|
||||||
|
$account = new Account();
|
||||||
|
$account->status = Account::STATUS_BANNED;
|
||||||
|
$form = new BanForm($account);
|
||||||
|
$form->validateAccountActivity();
|
||||||
|
$this->assertEquals([E::ACCOUNT_ALREADY_BANNED], $form->getErrors('account'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBan() {
|
||||||
|
/** @var Account|\PHPUnit_Framework_MockObject_MockObject $account */
|
||||||
|
$account = $this->getMockBuilder(Account::class)
|
||||||
|
->setMethods(['save'])
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
$account->expects($this->once())
|
||||||
|
->method('save')
|
||||||
|
->willReturn(true);
|
||||||
|
|
||||||
|
$model = new BanForm($account);
|
||||||
|
$this->assertTrue($model->ban());
|
||||||
|
$this->assertEquals(Account::STATUS_BANNED, $account->status);
|
||||||
|
$this->tester->canSeeAmqpMessageIsCreated('events');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCreateTask() {
|
||||||
|
$account = new Account();
|
||||||
|
$account->id = 3;
|
||||||
|
|
||||||
|
$model = new BanForm($account);
|
||||||
|
$model->createTask();
|
||||||
|
$message = json_decode($this->tester->grabLastSentAmqpMessage('events')->body, true);
|
||||||
|
$this->assertSame(3, $message['accountId']);
|
||||||
|
$this->assertSame(-1, $message['duration']);
|
||||||
|
$this->assertSame('', $message['message']);
|
||||||
|
|
||||||
|
$model = new BanForm($account);
|
||||||
|
$model->duration = 123;
|
||||||
|
$model->message = 'test';
|
||||||
|
$model->createTask();
|
||||||
|
$message = json_decode($this->tester->grabLastSentAmqpMessage('events')->body, true);
|
||||||
|
$this->assertSame(3, $message['accountId']);
|
||||||
|
$this->assertSame(123, $message['duration']);
|
||||||
|
$this->assertSame('test', $message['message']);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
namespace tests\codeception\api\unit\modules\internal\models;
|
||||||
|
|
||||||
|
use api\modules\internal\helpers\Error as E;
|
||||||
|
use api\modules\internal\models\PardonForm;
|
||||||
|
use common\models\Account;
|
||||||
|
use tests\codeception\api\unit\TestCase;
|
||||||
|
|
||||||
|
class PardonFormTest extends TestCase {
|
||||||
|
|
||||||
|
public function testValidateAccountBanned() {
|
||||||
|
$account = new Account();
|
||||||
|
$account->status = Account::STATUS_BANNED;
|
||||||
|
$form = new PardonForm($account);
|
||||||
|
$form->validateAccountBanned();
|
||||||
|
$this->assertEmpty($form->getErrors('account'));
|
||||||
|
|
||||||
|
$account = new Account();
|
||||||
|
$account->status = Account::STATUS_ACTIVE;
|
||||||
|
$form = new PardonForm($account);
|
||||||
|
$form->validateAccountBanned();
|
||||||
|
$this->assertEquals([E::ACCOUNT_NOT_BANNED], $form->getErrors('account'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPardon() {
|
||||||
|
/** @var Account|\PHPUnit_Framework_MockObject_MockObject $account */
|
||||||
|
$account = $this->getMockBuilder(Account::class)
|
||||||
|
->setMethods(['save'])
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
$account->expects($this->once())
|
||||||
|
->method('save')
|
||||||
|
->willReturn(true);
|
||||||
|
|
||||||
|
$account->status = Account::STATUS_BANNED;
|
||||||
|
$model = new PardonForm($account);
|
||||||
|
$this->assertTrue($model->pardon());
|
||||||
|
$this->assertEquals(Account::STATUS_ACTIVE, $account->status);
|
||||||
|
$this->tester->canSeeAmqpMessageIsCreated('events');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCreateTask() {
|
||||||
|
$account = new Account();
|
||||||
|
$account->id = 3;
|
||||||
|
|
||||||
|
$model = new PardonForm($account);
|
||||||
|
$model->createTask();
|
||||||
|
$message = json_decode($this->tester->grabLastSentAmqpMessage('events')->body, true);
|
||||||
|
$this->assertSame(3, $message['accountId']);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
35
tests/codeception/api/unit/validators/TotpValidatorTest.php
Normal file
35
tests/codeception/api/unit/validators/TotpValidatorTest.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
namespace tests\codeception\api\unit\validators;
|
||||||
|
|
||||||
|
use api\validators\TotpValidator;
|
||||||
|
use common\helpers\Error as E;
|
||||||
|
use common\models\Account;
|
||||||
|
use OTPHP\TOTP;
|
||||||
|
use tests\codeception\api\unit\TestCase;
|
||||||
|
use tests\codeception\common\_support\ProtectedCaller;
|
||||||
|
|
||||||
|
class TotpValidatorTest extends TestCase {
|
||||||
|
use ProtectedCaller;
|
||||||
|
|
||||||
|
public function testValidateValue() {
|
||||||
|
$account = new Account();
|
||||||
|
$account->otp_secret = 'some secret';
|
||||||
|
$controlTotp = new TOTP(null, 'some secret');
|
||||||
|
|
||||||
|
$validator = new TotpValidator(['account' => $account]);
|
||||||
|
|
||||||
|
$result = $this->callProtected($validator, 'validateValue', 123456);
|
||||||
|
$this->assertEquals([E::OTP_TOKEN_INCORRECT, []], $result);
|
||||||
|
|
||||||
|
$result = $this->callProtected($validator, 'validateValue', $controlTotp->now());
|
||||||
|
$this->assertNull($result);
|
||||||
|
|
||||||
|
$result = $this->callProtected($validator, 'validateValue', $controlTotp->at(time() - 31));
|
||||||
|
$this->assertEquals([E::OTP_TOKEN_INCORRECT, []], $result);
|
||||||
|
|
||||||
|
$validator->window = 60;
|
||||||
|
$result = $this->callProtected($validator, 'validateValue', $controlTotp->at(time() - 31));
|
||||||
|
$this->assertNull($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -16,4 +16,12 @@ return [
|
|||||||
'created_at' => time(),
|
'created_at' => time(),
|
||||||
'last_refreshed_at' => time(),
|
'last_refreshed_at' => time(),
|
||||||
],
|
],
|
||||||
|
'banned-user-session' => [
|
||||||
|
'id' => 3,
|
||||||
|
'account_id' => 10,
|
||||||
|
'refresh_token' => 'Af7fIuV6eL61tRUHn40yhmDRXN1OQxKR',
|
||||||
|
'last_used_ip' => ip2long('182.123.234.123'),
|
||||||
|
'created_at' => time(),
|
||||||
|
'last_refreshed_at' => time(),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
@ -146,4 +146,34 @@ return [
|
|||||||
'created_at' => 1474404139,
|
'created_at' => 1474404139,
|
||||||
'updated_at' => 1474404149,
|
'updated_at' => 1474404149,
|
||||||
],
|
],
|
||||||
|
'account-with-otp-secret' => [
|
||||||
|
'id' => 12,
|
||||||
|
'uuid' => '9e9dcd11-2322-46dc-a992-e822a422726e',
|
||||||
|
'username' => 'AccountWithOtpSecret',
|
||||||
|
'email' => 'sava-galkin@mail.ru',
|
||||||
|
'password_hash' => '$2y$13$2rYkap5T6jG8z/mMK8a3Ou6aZxJcmAaTha6FEuujvHEmybSHRzW5e', # password_0
|
||||||
|
'password_hash_strategy' => \common\models\Account::PASS_HASH_STRATEGY_YII2,
|
||||||
|
'lang' => 'ru',
|
||||||
|
'status' => \common\models\Account::STATUS_ACTIVE,
|
||||||
|
'rules_agreement_version' => \common\LATEST_RULES_VERSION,
|
||||||
|
'otp_secret' => 'some otp secret value',
|
||||||
|
'is_otp_enabled' => false,
|
||||||
|
'created_at' => 1485124615,
|
||||||
|
'updated_at' => 1485124615,
|
||||||
|
],
|
||||||
|
'account-with-enabled-otp' => [
|
||||||
|
'id' => 13,
|
||||||
|
'uuid' => '15d0afa7-a2bb-44d3-9f31-964cbccc6043',
|
||||||
|
'username' => 'AccountWithEnabledOtp',
|
||||||
|
'email' => 'otp@gmail.com',
|
||||||
|
'password_hash' => '$2y$13$2rYkap5T6jG8z/mMK8a3Ou6aZxJcmAaTha6FEuujvHEmybSHRzW5e', # password_0
|
||||||
|
'password_hash_strategy' => \common\models\Account::PASS_HASH_STRATEGY_YII2,
|
||||||
|
'lang' => 'ru',
|
||||||
|
'status' => \common\models\Account::STATUS_ACTIVE,
|
||||||
|
'rules_agreement_version' => \common\LATEST_RULES_VERSION,
|
||||||
|
'otp_secret' => 'secret-secret-secret',
|
||||||
|
'is_otp_enabled' => true,
|
||||||
|
'created_at' => 1485124685,
|
||||||
|
'updated_at' => 1485124685,
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
@ -30,4 +30,24 @@ return [
|
|||||||
'is_trusted' => 0,
|
'is_trusted' => 0,
|
||||||
'created_at' => 1479937982,
|
'created_at' => 1479937982,
|
||||||
],
|
],
|
||||||
|
'trustedClient' => [
|
||||||
|
'id' => 'trusted-client',
|
||||||
|
'secret' => 'tXBbyvMcyaOgHMOAXBpN2EC7uFoJAaL9',
|
||||||
|
'name' => 'Trusted client',
|
||||||
|
'description' => 'Это клиент, которому мы доверяем',
|
||||||
|
'redirect_uri' => null,
|
||||||
|
'account_id' => null,
|
||||||
|
'is_trusted' => 1,
|
||||||
|
'created_at' => 1482922663,
|
||||||
|
],
|
||||||
|
'defaultClient' => [
|
||||||
|
'id' => 'default-client',
|
||||||
|
'secret' => 'AzWRy7ZjS1yRQUk2vRBDic8fprOKDB1W',
|
||||||
|
'name' => 'Default client',
|
||||||
|
'description' => 'Это обычный клиент, каких может быть много',
|
||||||
|
'redirect_uri' => null,
|
||||||
|
'account_id' => null,
|
||||||
|
'is_trusted' => 0,
|
||||||
|
'created_at' => 1482922711,
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
@ -7,4 +7,11 @@ return [
|
|||||||
'client_id' => 'test1',
|
'client_id' => 'test1',
|
||||||
'client_redirect_uri' => 'http://test1.net/oauth',
|
'client_redirect_uri' => 'http://test1.net/oauth',
|
||||||
],
|
],
|
||||||
|
'banned-account-session' => [
|
||||||
|
'id' => 2,
|
||||||
|
'owner_type' => 'user',
|
||||||
|
'owner_id' => 10,
|
||||||
|
'client_id' => 'test1',
|
||||||
|
'client_redirect_uri' => 'http://test1.net/oauth',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
@ -4,6 +4,7 @@ namespace codeception\console\unit\controllers;
|
|||||||
use common\components\Mojang\Api;
|
use common\components\Mojang\Api;
|
||||||
use common\components\Mojang\exceptions\NoContentException;
|
use common\components\Mojang\exceptions\NoContentException;
|
||||||
use common\components\Mojang\response\UsernameToUUIDResponse;
|
use common\components\Mojang\response\UsernameToUUIDResponse;
|
||||||
|
use common\models\amqp\AccountBanned;
|
||||||
use common\models\amqp\UsernameChanged;
|
use common\models\amqp\UsernameChanged;
|
||||||
use common\models\MojangUsername;
|
use common\models\MojangUsername;
|
||||||
use console\controllers\AccountQueueController;
|
use console\controllers\AccountQueueController;
|
||||||
@ -143,4 +144,22 @@ class AccountQueueControllerTest extends TestCase {
|
|||||||
$this->assertNotEquals($mojangInfo->uuid, $mojangUsername->uuid);
|
$this->assertNotEquals($mojangInfo->uuid, $mojangUsername->uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testRouteAccountBanned() {
|
||||||
|
/** @var \common\models\Account $bannedAccount */
|
||||||
|
$bannedAccount = $this->tester->grabFixture('accounts', 'banned-account');
|
||||||
|
$this->tester->haveFixtures([
|
||||||
|
'oauthSessions' => \tests\codeception\common\fixtures\OauthSessionFixture::class,
|
||||||
|
'minecraftAccessKeys' => \tests\codeception\common\fixtures\MinecraftAccessKeyFixture::class,
|
||||||
|
'authSessions' => \tests\codeception\common\fixtures\AccountSessionFixture::class,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$body = new AccountBanned();
|
||||||
|
$body->accountId = $bannedAccount->id;
|
||||||
|
|
||||||
|
$this->controller->routeAccountBanned($body);
|
||||||
|
$this->assertEmpty($bannedAccount->sessions);
|
||||||
|
$this->assertEmpty($bannedAccount->minecraftAccessKeys);
|
||||||
|
$this->assertEmpty($bannedAccount->oauthSessions);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user