mirror of
				https://github.com/elyby/accounts.git
				synced 2025-05-31 14:11:46 +05:30 
			
		
		
		
	Merge branch 'develop'
This commit is contained in:
		| @@ -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/ | ||||
|   | ||||
| @@ -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/ | ||||
|   | ||||
| @@ -6,8 +6,8 @@ use yii\web\User as YiiUserComponent; | ||||
| /** | ||||
|  * @property Identity|null $identity | ||||
|  * | ||||
|  * @method Identity|null getIdentity() | ||||
|  * @method Identity|null loginByAccessToken(string $token, $type = null) | ||||
|  * @method Identity|null getIdentity($autoRenew = true) | ||||
|  * @method Identity|null loginByAccessToken($token, $type = null) | ||||
|  */ | ||||
| class Component extends YiiUserComponent { | ||||
|  | ||||
|   | ||||
| @@ -26,7 +26,8 @@ class Identity implements IdentityInterface { | ||||
|     /** | ||||
|      * @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); | ||||
|         if ($model === null) { | ||||
|             throw new UnauthorizedHttpException('Incorrect token'); | ||||
| @@ -41,19 +42,19 @@ class Identity implements IdentityInterface { | ||||
|         $this->_accessToken = $accessToken; | ||||
|     } | ||||
|  | ||||
|     public function getAccount() : Account { | ||||
|     public function getAccount(): Account { | ||||
|         return $this->getSession()->account; | ||||
|     } | ||||
|  | ||||
|     public function getClient() : OauthClient { | ||||
|     public function getClient(): OauthClient { | ||||
|         return $this->getSession()->client; | ||||
|     } | ||||
|  | ||||
|     public function getSession() : OauthSession { | ||||
|     public function getSession(): OauthSession { | ||||
|         return OauthSession::findOne($this->_accessToken->getSessionId()); | ||||
|     } | ||||
|  | ||||
|     public function getAccessToken() : AccessTokenEntity { | ||||
|     public function getAccessToken(): AccessTokenEntity { | ||||
|         return $this->_accessToken; | ||||
|     } | ||||
|  | ||||
| @@ -62,7 +63,7 @@ class Identity implements IdentityInterface { | ||||
|      * У нас права привязываются к токенам, так что возвращаем именно его id. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     public function getId() { | ||||
|     public function getId(): string { | ||||
|         return $this->_accessToken->getId(); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -3,6 +3,8 @@ namespace api\components\OAuth2\Entities; | ||||
|  | ||||
| class ClientEntity extends \League\OAuth2\Server\Entity\ClientEntity { | ||||
|  | ||||
|     private $isTrusted; | ||||
|  | ||||
|     public function setId(string $id) { | ||||
|         $this->id = $id; | ||||
|     } | ||||
| @@ -19,4 +21,12 @@ class ClientEntity extends \League\OAuth2\Server\Entity\ClientEntity { | ||||
|         $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->setName($model->name); | ||||
|         $entity->setSecret($model->secret); | ||||
|         $entity->setIsTrusted($model->is_trusted); | ||||
|         $entity->setRedirectUri($model->redirect_uri); | ||||
|  | ||||
|         return $entity; | ||||
|   | ||||
| @@ -18,6 +18,9 @@ class RefreshTokenStorage extends AbstractStorage implements RefreshTokenInterfa | ||||
|  | ||||
|     public function get($token) { | ||||
|         $result = Json::decode((new Key($this->dataTable, $token))->getValue()); | ||||
|         if ($result === null) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         $entity = new RefreshTokenEntity($this->server); | ||||
|         $entity->setId($result['id']); | ||||
|   | ||||
| @@ -1,10 +1,12 @@ | ||||
| <?php | ||||
| namespace api\components\OAuth2\Storage; | ||||
|  | ||||
| use api\components\OAuth2\Entities\ClientEntity; | ||||
| use api\components\OAuth2\Entities\ScopeEntity; | ||||
| use common\models\OauthScope; | ||||
| use League\OAuth2\Server\Storage\AbstractStorage; | ||||
| use League\OAuth2\Server\Storage\ScopeInterface; | ||||
| use yii\base\ErrorException; | ||||
|  | ||||
| class ScopeStorage extends AbstractStorage implements ScopeInterface { | ||||
|  | ||||
| @@ -12,7 +14,28 @@ class ScopeStorage extends AbstractStorage implements ScopeInterface { | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     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; | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -7,7 +7,7 @@ $params = array_merge( | ||||
| return [ | ||||
|     'id' => 'accounts-site-api', | ||||
|     'basePath' => dirname(__DIR__), | ||||
|     'bootstrap' => ['log', 'authserver'], | ||||
|     'bootstrap' => ['log', 'authserver', 'internal'], | ||||
|     'controllerNamespace' => 'api\controllers', | ||||
|     'params' => $params, | ||||
|     'components' => [ | ||||
| @@ -73,14 +73,6 @@ return [ | ||||
|         'response' => [ | ||||
|             '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' => [ | ||||
|             'class' => api\components\ErrorHandler::class, | ||||
|         ], | ||||
| @@ -96,5 +88,8 @@ return [ | ||||
|         'mojang' => [ | ||||
|             '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/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>', | ||||
|  | ||||
|     '/account/v1/info' => 'identity-info/index', | ||||
|   | ||||
| @@ -69,6 +69,7 @@ class AccountsController extends Controller { | ||||
|             'passwordChangedAt' => $account->password_changed_at, | ||||
|             'hasMojangUsernameCollision' => $account->hasMojangUsernameCollision(), | ||||
|             '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\OauthClient; | ||||
| use common\models\OauthScope; | ||||
| use League\OAuth2\Server\AuthorizationServer; | ||||
| use League\OAuth2\Server\Exception\OAuthException; | ||||
| use League\OAuth2\Server\Grant\AuthCodeGrant; | ||||
| use Yii; | ||||
| use yii\filters\AccessControl; | ||||
| use yii\helpers\ArrayHelper; | ||||
| @@ -274,17 +276,12 @@ class OauthController extends Controller { | ||||
|         return $response; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return \League\OAuth2\Server\AuthorizationServer | ||||
|      */ | ||||
|     private function getServer() { | ||||
|     private function getServer(): AuthorizationServer { | ||||
|         return Yii::$app->oauth->authServer; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @return \League\OAuth2\Server\Grant\AuthCodeGrant | ||||
|      */ | ||||
|     private function getGrantType() { | ||||
|     private function getGrantType(): AuthCodeGrant { | ||||
|         /** @noinspection PhpIncompatibleReturnTypeInspection */ | ||||
|         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; | ||||
|  | ||||
| use api\models\base\ApiForm; | ||||
| use api\validators\TotpValidator; | ||||
| use common\helpers\Error as E; | ||||
| use api\traits\AccountFinder; | ||||
| use common\components\UserFriendlyRandomKey; | ||||
| @@ -16,11 +17,16 @@ class ForgotPasswordForm extends ApiForm { | ||||
|     use AccountFinder; | ||||
|  | ||||
|     public $login; | ||||
|     public $token; | ||||
|  | ||||
|     public function rules() { | ||||
|         return [ | ||||
|             ['login', 'required', 'message' => E::LOGIN_REQUIRED], | ||||
|             ['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', '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) { | ||||
|         if (!$this->hasErrors()) { | ||||
|             $account = $this->getAccount(); | ||||
|   | ||||
| @@ -3,6 +3,7 @@ namespace api\models\authentication; | ||||
|  | ||||
| use api\models\AccountIdentity; | ||||
| use api\models\base\ApiForm; | ||||
| use api\validators\TotpValidator; | ||||
| use common\helpers\Error as E; | ||||
| use api\traits\AccountFinder; | ||||
| use common\models\Account; | ||||
| @@ -16,6 +17,7 @@ class LoginForm extends ApiForm { | ||||
|  | ||||
|     public $login; | ||||
|     public $password; | ||||
|     public $token; | ||||
|     public $rememberMe = false; | ||||
|  | ||||
|     public function rules() { | ||||
| @@ -28,6 +30,11 @@ class LoginForm extends ApiForm { | ||||
|             }, 'message' => E::PASSWORD_REQUIRED], | ||||
|             ['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'], | ||||
|  | ||||
|             ['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) { | ||||
|         if (!$this->hasErrors()) { | ||||
|             $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); | ||||
|         try { | ||||
|             $hasJoinedForm->hasJoined(); | ||||
|         } catch (ForbiddenOperationException $e) { | ||||
|             return 'NO'; | ||||
|         } catch (SessionServerException $e) { | ||||
|             Yii::$app->response->statusCode = $e->statusCode; | ||||
|             if ($e instanceof ForbiddenOperationException) { | ||||
|                 $message = 'NO'; | ||||
|             } else { | ||||
|                 $message = $e->getMessage(); | ||||
|             } | ||||
|  | ||||
|             return $message; | ||||
|             return $e->getMessage(); | ||||
|         } | ||||
|  | ||||
|         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 | ||||
|  * Used for properties that are identical for both WebApplication and ConsoleApplication | ||||
|  * | ||||
|  * @property \yii\swiftmailer\Mailer $mailer | ||||
|  * @property \common\components\Redis\Connection $redis | ||||
|  * @property \yii\swiftmailer\Mailer               $mailer | ||||
|  * @property \common\components\Redis\Connection   $redis | ||||
|  * @property \common\components\RabbitMQ\Component $amqp | ||||
|  * @property \GuzzleHttp\Client $guzzle | ||||
|  * @property \common\components\EmailRenderer $emailRenderer | ||||
|  * @property \mito\sentry\Component $sentry | ||||
|  * @property \GuzzleHttp\Client                    $guzzle | ||||
|  * @property \common\components\EmailRenderer      $emailRenderer | ||||
|  * @property \mito\sentry\Component                $sentry | ||||
|  * @property \api\components\OAuth2\Component      $oauth | ||||
|  */ | ||||
| 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\ApiUser\Component   $apiUser Api User component. | ||||
|  * @property \api\components\ReCaptcha\Component $reCaptcha | ||||
|  * @property \api\components\OAuth2\Component    $oauth | ||||
|  * | ||||
|  * @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 | ||||
| return [ | ||||
|     'version' => '1.1.5', | ||||
|     'version' => '1.1.6', | ||||
|     'vendorPath' => dirname(dirname(__DIR__)) . '/vendor', | ||||
|     'components' => [ | ||||
|         'cache' => [ | ||||
| @@ -69,6 +69,15 @@ return [ | ||||
|             'class' => common\components\EmailRenderer::class, | ||||
|             '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' => [ | ||||
|         '@bower' => '@vendor/bower-asset', | ||||
|   | ||||
| @@ -54,4 +54,9 @@ final class Error { | ||||
|     const SUBJECT_REQUIRED = 'error.subject_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 $rules_agreement_version | ||||
|  * @property string  $registration_ip | ||||
|  * @property string  $otp_secret | ||||
|  * @property integer $is_otp_enabled | ||||
|  * @property integer $created_at | ||||
|  * @property integer $updated_at | ||||
|  * @property integer $password_changed_at | ||||
| @@ -29,10 +31,11 @@ use const common\LATEST_RULES_VERSION; | ||||
|  * @property string  $profileLink ссылка на профиль на Ely без поддержки static url (только для записи) | ||||
|  * | ||||
|  * Отношения: | ||||
|  * @property EmailActivation[] $emailActivations | ||||
|  * @property OauthSession[]    $oauthSessions | ||||
|  * @property UsernameHistory[] $usernameHistory | ||||
|  * @property AccountSession[]  $sessions | ||||
|  * @property EmailActivation[]    $emailActivations | ||||
|  * @property OauthSession[]       $oauthSessions | ||||
|  * @property UsernameHistory[]    $usernameHistory | ||||
|  * @property AccountSession[]     $sessions | ||||
|  * @property MinecraftAccessKey[] $minecraftAccessKeys | ||||
|  * | ||||
|  * Поведения: | ||||
|  * @mixin TimestampBehavior | ||||
| @@ -99,7 +102,7 @@ class Account extends ActiveRecord { | ||||
|     } | ||||
|  | ||||
|     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() { | ||||
| @@ -110,6 +113,10 @@ class Account extends ActiveRecord { | ||||
|         return $this->hasMany(AccountSession::class, ['account_id' => 'id']); | ||||
|     } | ||||
|  | ||||
|     public function getMinecraftAccessKeys() { | ||||
|         return $this->hasMany(MinecraftAccessKey::class, ['account_id' => 'id']); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Выполняет проверку, принадлежит ли этому нику аккаунт у Mojang | ||||
|      * | ||||
|   | ||||
| @@ -1,20 +1,62 @@ | ||||
| <?php | ||||
| namespace common\models; | ||||
|  | ||||
| use common\components\Annotations\Reader; | ||||
| use ReflectionClass; | ||||
| use Yii; | ||||
|  | ||||
| class OauthScope { | ||||
|  | ||||
|     /** | ||||
|      * @owner user | ||||
|      */ | ||||
|     const OFFLINE_ACCESS = 'offline_access'; | ||||
|     /** | ||||
|      * @owner user | ||||
|      */ | ||||
|     const MINECRAFT_SERVER_SESSION = 'minecraft_server_session'; | ||||
|     /** | ||||
|      * @owner user | ||||
|      */ | ||||
|     const ACCOUNT_INFO = 'account_info'; | ||||
|     /** | ||||
|      * @owner user | ||||
|      */ | ||||
|     const ACCOUNT_EMAIL = 'account_email'; | ||||
|     /** | ||||
|      * @internal | ||||
|      * @owner machine | ||||
|      */ | ||||
|     const ACCOUNT_BLOCK = 'account_block'; | ||||
|  | ||||
|     public static function getScopes() : array { | ||||
|         return [ | ||||
|             self::OFFLINE_ACCESS, | ||||
|             self::MINECRAFT_SERVER_SESSION, | ||||
|             self::ACCOUNT_INFO, | ||||
|             self::ACCOUNT_EMAIL, | ||||
|         ]; | ||||
|     public static function find(): OauthScopeQuery { | ||||
|         return new OauthScopeQuery(static::queryScopes()); | ||||
|     } | ||||
|  | ||||
|     private static function queryScopes(): array { | ||||
|         $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-swiftmailer": "*", | ||||
|         "ramsey/uuid": "^3.5.0", | ||||
|         "league/oauth2-server": "dev-improvements#b9277ccd664dcb80a766b73674d21de686cb9dda", | ||||
|         "league/oauth2-server": "dev-improvements#fbaa9b0bd3d8050235ba7dde90f731764122bc20", | ||||
|         "yiisoft/yii2-redis": "~2.0.0", | ||||
|         "guzzlehttp/guzzle": "^6.0.0", | ||||
|         "php-amqplib/php-amqplib": "^2.6.2", | ||||
| @@ -27,7 +27,10 @@ | ||||
|         "ely/amqp-controller": "dev-master#d7f8cdbc66c45e477c9c7d5d509bc0c1b11fd3ec", | ||||
|         "ely/email-renderer": "dev-master#ef1cb3f7a13196524b97ca5aa0a2d5867f2d9207", | ||||
|         "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": { | ||||
|         "yiisoft/yii2-codeception": "*", | ||||
| @@ -49,11 +52,11 @@ | ||||
|         }, | ||||
|         { | ||||
|             "type": "git", | ||||
|             "url": "git@gitlab.com:elyby/amqp-controller.git" | ||||
|             "url": "git@gitlab.ely.by:elyby/amqp-controller.git" | ||||
|         }, | ||||
|         { | ||||
|             "type": "git", | ||||
|             "url": "git@gitlab.com:elyby/email-renderer.git" | ||||
|             "url": "git@gitlab.ely.by:elyby/email-renderer.git" | ||||
|         }, | ||||
|         { | ||||
|             "type": "git", | ||||
|   | ||||
| @@ -3,10 +3,13 @@ namespace console\controllers; | ||||
|  | ||||
| use common\components\Mojang\Api as MojangApi; | ||||
| use common\components\Mojang\exceptions\NoContentException; | ||||
| use common\models\Account; | ||||
| use common\models\amqp\AccountBanned; | ||||
| use common\models\amqp\UsernameChanged; | ||||
| use common\models\MojangUsername; | ||||
| use Ely\Amqp\Builder\Configurator; | ||||
| use GuzzleHttp\Exception\RequestException; | ||||
| use Yii; | ||||
|  | ||||
| class AccountQueueController extends AmqpController { | ||||
|  | ||||
| @@ -17,16 +20,18 @@ class AccountQueueController extends AmqpController { | ||||
|     public function configure(Configurator $configurator) { | ||||
|         $configurator->exchange->topic()->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() { | ||||
|         return [ | ||||
|             'accounts.username-changed' => 'routeUsernameChanged', | ||||
|             'accounts.account-banned' => 'routeAccountBanned', | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     public function routeUsernameChanged(UsernameChanged $body) { | ||||
|     public function routeUsernameChanged(UsernameChanged $body): bool { | ||||
|         $mojangApi = $this->createMojangApi(); | ||||
|         try { | ||||
|             $response = $mojangApi->usernameToUUID($body->newUsername); | ||||
| @@ -58,10 +63,32 @@ class AccountQueueController extends AmqpController { | ||||
|         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 | ||||
|      */ | ||||
|     protected function createMojangApi() : MojangApi { | ||||
|     protected function createMojangApi(): 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 { | ||||
|  | ||||
|     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']; | ||||
|         $params = [ | ||||
|             'login' => $login, | ||||
|             'password' => $password, | ||||
|         ]; | ||||
|  | ||||
|         if ($rememberMe) { | ||||
|         if ((is_bool($rememberMeOrToken) && $rememberMeOrToken) || $rememberMe) { | ||||
|             $params['rememberMe'] = 1; | ||||
|         } elseif ($rememberMeOrToken !== null) { | ||||
|             $params['token'] = $rememberMeOrToken; | ||||
|         } | ||||
|  | ||||
|         $this->actor->sendPOST($this->getUrl(), $params); | ||||
| @@ -27,10 +35,11 @@ class AuthenticationRoute extends BasePage { | ||||
|         $this->actor->sendPOST($this->getUrl()); | ||||
|     } | ||||
|  | ||||
|     public function forgotPassword($login = '') { | ||||
|     public function forgotPassword($login = null, $token = null) { | ||||
|         $this->route = ['authentication/forgot-password']; | ||||
|         $this->actor->sendPOST($this->getUrl(), [ | ||||
|             '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 | ||||
| namespace tests\codeception\api; | ||||
|  | ||||
| use api\models\AccountIdentity; | ||||
| use Codeception\Actor; | ||||
| use InvalidArgumentException; | ||||
| use tests\codeception\api\_pages\AuthenticationRoute; | ||||
| use Yii; | ||||
|  | ||||
| /** | ||||
|  * Inherited Methods | ||||
| @@ -23,20 +24,15 @@ use tests\codeception\api\_pages\AuthenticationRoute; | ||||
| class FunctionalTester extends Actor { | ||||
|     use _generated\FunctionalTesterActions; | ||||
|  | ||||
|     public function loggedInAsActiveAccount($login = null, $password = null) { | ||||
|         $route = new AuthenticationRoute($this); | ||||
|         if ($login === null) { | ||||
|             $route->login('Admin', 'password_0'); | ||||
|         } elseif ($login !== null && $password !== null) { | ||||
|             $route->login($login, $password); | ||||
|         } else { | ||||
|             throw new InvalidArgumentException('login and password should be presented both.'); | ||||
|     public function amAuthenticated(string $asUsername = 'admin') { | ||||
|         /** @var AccountIdentity $account */ | ||||
|         $account = AccountIdentity::findOne(['username' => $asUsername]); | ||||
|         if ($account === null) { | ||||
|             throw new InvalidArgumentException("Cannot find account for username \"$asUsername\""); | ||||
|         } | ||||
|  | ||||
|         $this->canSeeResponseIsJson(); | ||||
|         $this->canSeeAuthCredentials(false); | ||||
|         $jwt = $this->grabDataFromResponseByJsonPath('$.access_token')[0]; | ||||
|         $this->amBearerAuthenticated($jwt); | ||||
|         $result = Yii::$app->user->login($account); | ||||
|         $this->amBearerAuthenticated($result->getJwt()); | ||||
|     } | ||||
|  | ||||
|     public function notLoggedIn() { | ||||
|   | ||||
| @@ -16,7 +16,7 @@ class AccountsAcceptRulesCest { | ||||
|     } | ||||
|  | ||||
|     public function testCurrent(FunctionalTester $I) { | ||||
|         $I->loggedInAsActiveAccount('Veleyaba', 'password_0'); | ||||
|         $I->amAuthenticated('Veleyaba'); | ||||
|         $this->route->acceptRules(); | ||||
|         $I->canSeeResponseCodeIs(200); | ||||
|         $I->canSeeResponseIsJson(); | ||||
|   | ||||
| @@ -17,7 +17,7 @@ class AccountsChangeEmailConfirmNewEmailCest { | ||||
|  | ||||
|     public function testConfirmNewEmail(FunctionalTester $I) { | ||||
|         $I->wantTo('change my email and get changed value'); | ||||
|         $I->loggedInAsActiveAccount('CrafterGameplays', 'password_0'); | ||||
|         $I->amAuthenticated('CrafterGameplays'); | ||||
|  | ||||
|         $this->route->changeEmailConfirmNewEmail('H28HBDCHHAG2HGHGHS'); | ||||
|         $I->canSeeResponseCodeIs(200); | ||||
|   | ||||
| @@ -17,7 +17,7 @@ class AccountsChangeEmailInitializeCest { | ||||
|  | ||||
|     public function testChangeEmailInitialize(FunctionalTester $I) { | ||||
|         $I->wantTo('send current email confirmation'); | ||||
|         $I->loggedInAsActiveAccount(); | ||||
|         $I->amAuthenticated(); | ||||
|  | ||||
|         $this->route->changeEmailInitialize('password_0'); | ||||
|         $I->canSeeResponseCodeIs(200); | ||||
| @@ -29,7 +29,7 @@ class AccountsChangeEmailInitializeCest { | ||||
|  | ||||
|     public function testChangeEmailInitializeFrequencyError(FunctionalTester $I) { | ||||
|         $I->wantTo('see change email request frequency error'); | ||||
|         $I->loggedInAsActiveAccount('ILLIMUNATI', 'password_0'); | ||||
|         $I->amAuthenticated('ILLIMUNATI'); | ||||
|  | ||||
|         $this->route->changeEmailInitialize('password_0'); | ||||
|         $I->canSeeResponseContainsJson([ | ||||
|   | ||||
| @@ -18,7 +18,7 @@ class AccountsChangeEmailSubmitNewEmailCest { | ||||
|  | ||||
|     public function testSubmitNewEmail(FunctionalTester $I) { | ||||
|         $I->wantTo('submit new email'); | ||||
|         $I->loggedInAsActiveAccount('ILLIMUNATI', 'password_0'); | ||||
|         $I->amAuthenticated('ILLIMUNATI'); | ||||
|  | ||||
|         $this->route->changeEmailSubmitNewEmail('H27HBDCHHAG2HGHGHS', 'my-new-email@ely.by'); | ||||
|         $I->canSeeResponseCodeIs(200); | ||||
|   | ||||
| @@ -18,7 +18,7 @@ class AccountsChangeLangCest { | ||||
|  | ||||
|     public function testSubmitNewEmail(FunctionalTester $I) { | ||||
|         $I->wantTo('change my account language'); | ||||
|         $I->loggedInAsActiveAccount(); | ||||
|         $I->amAuthenticated(); | ||||
|  | ||||
|         $this->route->changeLang('ru'); | ||||
|         $I->canSeeResponseCodeIs(200); | ||||
|   | ||||
| @@ -27,7 +27,7 @@ class AccountsChangePasswordCest { | ||||
|  | ||||
|     public function testChangePassword(FunctionalTester $I) { | ||||
|         $I->wantTo('change my password'); | ||||
|         $I->loggedInAsActiveAccount(); | ||||
|         $I->amAuthenticated(); | ||||
|  | ||||
|         $this->route->changePassword('password_0', 'new-password', 'new-password'); | ||||
|         $I->canSeeResponseCodeIs(200); | ||||
|   | ||||
| @@ -26,7 +26,7 @@ class AccountsChangeUsernameCest { | ||||
|  | ||||
|     public function testChangeUsername(FunctionalTester $I) { | ||||
|         $I->wantTo('change my nickname'); | ||||
|         $I->loggedInAsActiveAccount(); | ||||
|         $I->amAuthenticated(); | ||||
|  | ||||
|         $this->route->changeUsername('password_0', 'bruce_wayne'); | ||||
|         $I->canSeeResponseCodeIs(200); | ||||
| @@ -38,7 +38,7 @@ class AccountsChangeUsernameCest { | ||||
|  | ||||
|     public function testChangeUsernameNotAvailable(FunctionalTester $I) { | ||||
|         $I->wantTo('see, that nickname "in use" is not available'); | ||||
|         $I->loggedInAsActiveAccount(); | ||||
|         $I->amAuthenticated(); | ||||
|  | ||||
|         $this->route->changeUsername('password_0', 'Jon'); | ||||
|         $I->canSeeResponseCodeIs(200); | ||||
|   | ||||
| @@ -16,7 +16,7 @@ class AccountsCurrentCest { | ||||
|     } | ||||
|  | ||||
|     public function testCurrent(FunctionalTester $I) { | ||||
|         $I->loggedInAsActiveAccount(); | ||||
|         $I->amAuthenticated(); | ||||
|  | ||||
|         $this->route->current(); | ||||
|         $I->canSeeResponseCodeIs(200); | ||||
| @@ -29,6 +29,7 @@ class AccountsCurrentCest { | ||||
|             'isActive' => true, | ||||
|             'hasMojangUsernameCollision' => false, | ||||
|             'shouldAcceptRules' => false, | ||||
|             'isOtpEnabled' => false, | ||||
|         ]); | ||||
|         $I->canSeeResponseJsonMatchesJsonPath('$.passwordChangedAt'); | ||||
|     } | ||||
|   | ||||
| @@ -1,41 +1,87 @@ | ||||
| <?php | ||||
| namespace codeception\api\functional; | ||||
|  | ||||
| use OTPHP\TOTP; | ||||
| use tests\codeception\api\_pages\AuthenticationRoute; | ||||
| use tests\codeception\api\FunctionalTester; | ||||
|  | ||||
| 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'); | ||||
|         $route->forgotPassword('admin@ely.by'); | ||||
|     public function _before(FunctionalTester $I) { | ||||
|         $this->route = new AuthenticationRoute($I); | ||||
|     } | ||||
|  | ||||
|     public function testWrongInput(FunctionalTester $I) { | ||||
|         $I->wantTo('see reaction on invalid input'); | ||||
|  | ||||
|         $this->route->forgotPassword(); | ||||
|         $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) { | ||||
|         $route = new AuthenticationRoute($I); | ||||
|  | ||||
|         $I->wantTo('create new password recover request by passing username'); | ||||
|         $route->forgotPassword('Admin'); | ||||
|         $I->canSeeResponseContainsJson([ | ||||
|             'success' => true, | ||||
|         ]); | ||||
|         $I->canSeeResponseJsonMatchesJsonPath('$.data.canRepeatIn'); | ||||
|         $I->canSeeResponseJsonMatchesJsonPath('$.data.repeatFrequency'); | ||||
|         $I->canSeeResponseJsonMatchesJsonPath('$.data.emailMask'); | ||||
|         $this->route->forgotPassword('Admin'); | ||||
|         $this->assertSuccessResponse($I, true); | ||||
|     } | ||||
|  | ||||
|     public function testForgotPasswordByAccountWithOtp(FunctionalTester $I) { | ||||
|         $I->wantTo('create new password recover request by passing username and otp token'); | ||||
|         $totp = new TOTP(null, 'secret-secret-secret'); | ||||
|         $this->route->forgotPassword('AccountWithEnabledOtp', $totp->now()); | ||||
|         $this->assertSuccessResponse($I, true); | ||||
|     } | ||||
|  | ||||
|     public function testDataForFrequencyError(FunctionalTester $I) { | ||||
|         $route = new AuthenticationRoute($I); | ||||
|  | ||||
|         $I->wantTo('get info about time to repeat recover password request'); | ||||
|         $route->forgotPassword('Notch'); | ||||
|         $this->route->forgotPassword('Notch'); | ||||
|         $I->canSeeResponseContainsJson([ | ||||
|             'success' => false, | ||||
|             'errors' => [ | ||||
| @@ -46,4 +92,18 @@ class ForgotPasswordCest { | ||||
|         $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 | ||||
| namespace tests\codeception\api; | ||||
|  | ||||
| use OTPHP\TOTP; | ||||
| use tests\codeception\api\_pages\AuthenticationRoute; | ||||
|  | ||||
| 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) { | ||||
|         $route = new AuthenticationRoute($I); | ||||
|  | ||||
| @@ -151,4 +202,16 @@ class LoginCest { | ||||
|         $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) { | ||||
|         $route = new AuthenticationRoute($I); | ||||
|  | ||||
|         $I->loggedInAsActiveAccount(); | ||||
|         $I->amAuthenticated(); | ||||
|         $route->logout(); | ||||
|         $I->canSeeResponseContainsJson([ | ||||
|             'success' => true, | ||||
|   | ||||
| @@ -16,12 +16,13 @@ class OauthAccessTokenCest { | ||||
|         $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'); | ||||
|         $this->route->issueToken(); | ||||
|         $I->canSeeResponseCodeIs(400); | ||||
|         $I->canSeeResponseContainsJson([ | ||||
|             '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'); | ||||
| @@ -34,6 +35,21 @@ class OauthAccessTokenCest { | ||||
|         $I->canSeeResponseCodeIs(400); | ||||
|         $I->canSeeResponseContainsJson([ | ||||
|             '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) { | ||||
|         $I->loggedInAsActiveAccount(); | ||||
|         $I->amAuthenticated(); | ||||
|         $I->wantTo('validate and get information with description replacement'); | ||||
|         $this->route->validate($this->buildQueryParams( | ||||
|             'ely', | ||||
| @@ -73,13 +73,13 @@ class OauthAuthCodeCest { | ||||
|     } | ||||
|  | ||||
|     public function testCompleteValidationAction(FunctionalTester $I) { | ||||
|         $I->loggedInAsActiveAccount(); | ||||
|         $I->amAuthenticated(); | ||||
|         $I->wantTo('validate all oAuth params on complete request'); | ||||
|         $this->testOauthParamsValidation($I, 'complete'); | ||||
|     } | ||||
|  | ||||
|     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'); | ||||
|         $this->route->complete($this->buildQueryParams( | ||||
| @@ -112,7 +112,7 @@ class OauthAuthCodeCest { | ||||
|     } | ||||
|  | ||||
|     public function testCompleteActionSuccess(FunctionalTester $I) { | ||||
|         $I->loggedInAsActiveAccount(); | ||||
|         $I->amAuthenticated(); | ||||
|         $I->wantTo('get auth code if I require some scope and pass accept field'); | ||||
|         $this->route->complete($this->buildQueryParams( | ||||
|             'ely', | ||||
| @@ -155,7 +155,7 @@ class OauthAuthCodeCest { | ||||
|     } | ||||
|  | ||||
|     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'); | ||||
|         $this->route->complete($this->buildQueryParams( | ||||
|             'ely', | ||||
| @@ -179,7 +179,7 @@ class OauthAuthCodeCest { | ||||
|     } | ||||
|  | ||||
|     public function testCompleteActionWithDismissState(FunctionalTester $I) { | ||||
|         $I->loggedInAsActiveAccount(); | ||||
|         $I->amAuthenticated(); | ||||
|         $I->wantTo('get access_denied error if I pass accept in false state'); | ||||
|         $this->route->complete($this->buildQueryParams( | ||||
|             'ely', | ||||
| @@ -281,6 +281,21 @@ class OauthAuthCodeCest { | ||||
|             'statusCode' => 400, | ||||
|         ]); | ||||
|         $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); | ||||
|     } | ||||
|  | ||||
|     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) { | ||||
|         $refreshToken = $I->getRefreshToken(); | ||||
|         $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 { | ||||
|  | ||||
|     public function amAuthenticated() { | ||||
|     public function amAuthenticated(string $asUsername = 'admin', string $password = 'password_0') { | ||||
|         $route = new AuthserverRoute($this); | ||||
|         $clientToken = Uuid::uuid4()->toString(); | ||||
|         $route->authenticate([ | ||||
|             'username' => 'admin', | ||||
|             'password' => 'password_0', | ||||
|             'username' => $asUsername, | ||||
|             'password' => $password, | ||||
|             'clientToken' => $clientToken, | ||||
|         ]); | ||||
|  | ||||
|   | ||||
| @@ -3,12 +3,13 @@ namespace tests\codeception\api\functional\_steps; | ||||
|  | ||||
| use common\models\OauthScope as S; | ||||
| 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 = []) { | ||||
|         // TODO: по идее можно напрямую сделать зпись в базу, что ускорит процесс тестирования | ||||
|         $this->loggedInAsActiveAccount(); | ||||
|         // TODO: по идее можно напрямую сделать запись в базу, что ускорит процесс тестирования | ||||
|         $this->amAuthenticated(); | ||||
|         $route = new OauthRoute($this); | ||||
|         $route->complete([ | ||||
|             'client_id' => 'ely', | ||||
| @@ -31,7 +32,7 @@ class OauthSteps extends \tests\codeception\api\FunctionalTester { | ||||
|     } | ||||
|  | ||||
|     public function getRefreshToken(array $permissions = []) { | ||||
|         // TODO: по идее можно напрямую сделать зпись в базу, что ускорит процесс тестирования | ||||
|         // TODO: по идее можно напрямую сделать запись в базу, что ускорит процесс тестирования | ||||
|         $authCode = $this->getAuthCode(array_merge([S::OFFLINE_ACCESS], $permissions)); | ||||
|         $response = $this->issueToken($authCode); | ||||
|  | ||||
| @@ -51,4 +52,18 @@ class OauthSteps extends \tests\codeception\api\FunctionalTester { | ||||
|         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) { | ||||
|         $I->wantTo('invalidate my token'); | ||||
|         list($accessToken, $clientToken) = $I->amAuthenticated(); | ||||
|         [$accessToken, $clientToken] = $I->amAuthenticated(); | ||||
|         $this->route->invalidate([ | ||||
|             'accessToken' => $accessToken, | ||||
|             'clientToken' => $clientToken, | ||||
|   | ||||
| @@ -18,7 +18,7 @@ class RefreshCest { | ||||
|  | ||||
|     public function refresh(AuthserverSteps $I) { | ||||
|         $I->wantTo('refresh my accessToken'); | ||||
|         list($accessToken, $clientToken) = $I->amAuthenticated(); | ||||
|         [$accessToken, $clientToken] = $I->amAuthenticated(); | ||||
|         $this->route->refresh([ | ||||
|             'accessToken' => $accessToken, | ||||
|             'clientToken' => $clientToken, | ||||
|   | ||||
| @@ -18,7 +18,7 @@ class ValidateCest { | ||||
|  | ||||
|     public function validate(AuthserverSteps $I) { | ||||
|         $I->wantTo('validate my accessToken'); | ||||
|         list($accessToken) = $I->amAuthenticated(); | ||||
|         [$accessToken] = $I->amAuthenticated(); | ||||
|         $this->route->validate([ | ||||
|             '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', | ||||
|             'serverId' => Uuid::uuid(), | ||||
|         ]); | ||||
|         $I->seeResponseCodeIs(401); | ||||
|         $I->seeResponseCodeIs(200); | ||||
|         $I->canSeeResponseEquals('NO'); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -21,7 +21,7 @@ class JoinCest { | ||||
|  | ||||
|     public function joinByLegacyAuthserver(AuthserverSteps $I) { | ||||
|         $I->wantTo('join to server, using legacy authserver access token'); | ||||
|         list($accessToken) = $I->amAuthenticated(); | ||||
|         [$accessToken] = $I->amAuthenticated(); | ||||
|         $this->route->join([ | ||||
|             'accessToken' => $accessToken, | ||||
|             'selectedProfile' => 'df936908-b2e1-544d-96f8-2977ec213022', | ||||
| @@ -32,7 +32,7 @@ class JoinCest { | ||||
|  | ||||
|     public function joinByPassJsonInPost(AuthserverSteps $I) { | ||||
|         $I->wantTo('join to server, passing data in body as encoded json'); | ||||
|         list($accessToken) = $I->amAuthenticated(); | ||||
|         [$accessToken] = $I->amAuthenticated(); | ||||
|         $this->route->join(json_encode([ | ||||
|             'accessToken' => $accessToken, | ||||
|             'selectedProfile' => 'df936908-b2e1-544d-96f8-2977ec213022', | ||||
|   | ||||
| @@ -21,7 +21,7 @@ class JoinLegacyCest { | ||||
|  | ||||
|     public function joinByLegacyAuthserver(AuthserverSteps $I) { | ||||
|         $I->wantTo('join to server by legacy protocol, using legacy authserver access token'); | ||||
|         list($accessToken) = $I->amAuthenticated(); | ||||
|         [$accessToken] = $I->amAuthenticated(); | ||||
|         $this->route->joinLegacy([ | ||||
|             'sessionId' => $accessToken, | ||||
|             'user' => 'Admin', | ||||
| @@ -32,7 +32,7 @@ class JoinLegacyCest { | ||||
|  | ||||
|     public function joinByNewSessionFormat(AuthserverSteps $I) { | ||||
|         $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([ | ||||
|             'sessionId' => 'token:' . $accessToken . ':' . 'df936908-b2e1-544d-96f8-2977ec213022', | ||||
|             'user' => 'Admin', | ||||
|   | ||||
| @@ -4,6 +4,7 @@ namespace codeception\api\unit\models\authentication; | ||||
| use api\models\authentication\ForgotPasswordForm; | ||||
| use Codeception\Specify; | ||||
| use common\models\EmailActivation; | ||||
| use OTPHP\TOTP; | ||||
| use tests\codeception\api\unit\TestCase; | ||||
| use tests\codeception\common\fixtures\AccountFixture; | ||||
| 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() { | ||||
|             $model = new ForgotPasswordForm(['login' => 'unexist']); | ||||
|             $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() { | ||||
|         $this->specify('error.account_not_activated if account is not confirmed', function() { | ||||
|             $model = new ForgotPasswordForm([ | ||||
|   | ||||
| @@ -6,6 +6,7 @@ use api\models\AccountIdentity; | ||||
| use api\models\authentication\LoginForm; | ||||
| use Codeception\Specify; | ||||
| use common\models\Account; | ||||
| use OTPHP\TOTP; | ||||
| use tests\codeception\api\unit\TestCase; | ||||
| use tests\codeception\common\fixtures\AccountFixture; | ||||
|  | ||||
| @@ -38,7 +39,7 @@ class LoginFormTest extends TestCase { | ||||
|                 'account' => null, | ||||
|             ]); | ||||
|             $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 () { | ||||
| @@ -47,7 +48,7 @@ class LoginFormTest extends TestCase { | ||||
|                 'account' => new AccountIdentity(), | ||||
|             ]); | ||||
|             $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']), | ||||
|             ]); | ||||
|             $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 () { | ||||
| @@ -67,7 +68,35 @@ class LoginFormTest extends TestCase { | ||||
|                 'account' => new AccountIdentity(['password' => '12345678']), | ||||
|             ]); | ||||
|             $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]), | ||||
|             ]); | ||||
|             $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 () { | ||||
| @@ -85,7 +114,7 @@ class LoginFormTest extends TestCase { | ||||
|                 'account' => new AccountIdentity(['status' => Account::STATUS_BANNED]), | ||||
|             ]); | ||||
|             $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 () { | ||||
| @@ -93,36 +122,36 @@ class LoginFormTest extends TestCase { | ||||
|                 'account' => new AccountIdentity(['status' => Account::STATUS_ACTIVE]), | ||||
|             ]); | ||||
|             $model->validateActivity('login'); | ||||
|             expect($model->getErrors('login'))->isEmpty(); | ||||
|             $this->assertEmpty($model->getErrors('login')); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     public function testLogin() { | ||||
|         $this->specify('user should be able to login with correct username and password', function () { | ||||
|             $model = $this->createModel([ | ||||
|                 'login' => 'erickskrauch', | ||||
|         $model = $this->createModel([ | ||||
|             'login' => 'erickskrauch', | ||||
|             'password' => '12345678', | ||||
|             'account' => new AccountIdentity([ | ||||
|                 'username' => 'erickskrauch', | ||||
|                 'password' => '12345678', | ||||
|                 'account' => new AccountIdentity([ | ||||
|                     'username' => 'erickskrauch', | ||||
|                     'password' => '12345678', | ||||
|                     'status' => Account::STATUS_ACTIVE, | ||||
|                 ]), | ||||
|             ]); | ||||
|             expect('model should login user', $model->login())->isInstanceOf(LoginResult::class); | ||||
|             expect('error message should not be set', $model->errors)->isEmpty(); | ||||
|         }); | ||||
|                 'status' => Account::STATUS_ACTIVE, | ||||
|             ]), | ||||
|         ]); | ||||
|         $this->assertInstanceOf(LoginResult::class, $model->login(), 'model should login user'); | ||||
|         $this->assertEmpty($model->getErrors(), 'error message should not be set'); | ||||
|     } | ||||
|  | ||||
|     public function testLoginWithRehashing() { | ||||
|         $this->specify('user, that login using account with old pass hash strategy should update it automatically', function () { | ||||
|             $model = new LoginForm([ | ||||
|                 'login' => $this->tester->grabFixture('accounts', 'user-with-old-password-type')['username'], | ||||
|                 'password' => '12345678', | ||||
|             ]); | ||||
|             expect($model->login())->isInstanceOf(LoginResult::class); | ||||
|             expect($model->errors)->isEmpty(); | ||||
|             expect($model->getAccount()->password_hash_strategy)->equals(Account::PASS_HASH_STRATEGY_YII2); | ||||
|         }); | ||||
|         $model = new LoginForm([ | ||||
|             'login' => $this->tester->grabFixture('accounts', 'user-with-old-password-type')['username'], | ||||
|             'password' => '12345678', | ||||
|         ]); | ||||
|         $this->assertInstanceOf(LoginResult::class, $model->login()); | ||||
|         $this->assertEmpty($model->getErrors()); | ||||
|         $this->assertEquals( | ||||
|             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(), | ||||
|         '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, | ||||
|         '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, | ||||
|         '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_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\exceptions\NoContentException; | ||||
| use common\components\Mojang\response\UsernameToUUIDResponse; | ||||
| use common\models\amqp\AccountBanned; | ||||
| use common\models\amqp\UsernameChanged; | ||||
| use common\models\MojangUsername; | ||||
| use console\controllers\AccountQueueController; | ||||
| @@ -143,4 +144,22 @@ class AccountQueueControllerTest extends TestCase { | ||||
|         $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); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user