Merge branch 'develop'

This commit is contained in:
ErickSkrauch
2016-12-14 00:47:12 +03:00
99 changed files with 2000 additions and 969 deletions

View File

@@ -1,9 +1,44 @@
# Основные параметры # Параметры приложения
## Env приложения
YII_DEBUG=true YII_DEBUG=true
YII_ENV=dev YII_ENV=dev
## Параметры, отвечающие за безопасность
JWT_USER_SECRET= JWT_USER_SECRET=
## Внешние сервисы
RECAPTCHA_PUBLIC= RECAPTCHA_PUBLIC=
RECAPTCHA_SECRET= RECAPTCHA_SECRET=
SENTRY_DSN=
## SMTP параметры
SMTP_USER=
SMTP_PASS=
SMTP_PORT=
## Параметры подключения к базе данных
DB_HOST=db
DB_DATABASE=ely_accounts
DB_USER=ely_accounts_user
DB_PASSWORD=ely_accounts_password
## Параметры подключения к redis
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_DATABASE=0
REDIS_PASSWORD=
## Параметры подключения к rabbitmq
RABBITMQ_HOST=rabbitmq
RABBITMQ_PORT=5672
RABBITMQ_USER=ely-accounts-app
RABBITMQ_PASS=ely-accounts-app-password
RABBITMQ_VHOST=/ely.by
## Конфигурация для Dev.
XDEBUG_CONFIG=remote_host=10.254.254.254
PHP_IDE_CONFIG=serverName=docker
# Web # Web
VIRTUAL_HOST=account.ely.by,authserver.ely.by VIRTUAL_HOST=account.ely.by,authserver.ely.by
@@ -11,10 +46,6 @@ AUTHSERVER_HOST=authserver.ely.by
# LETSENCRYPT_HOST=account.ely.by # LETSENCRYPT_HOST=account.ely.by
# LETSENCRYPT_EMAIL=erickskrauch@ely.by # LETSENCRYPT_EMAIL=erickskrauch@ely.by
# SMTP (только для production)
SMTP_USER=
SMTP_PASS=
# MySQL # MySQL
MYSQL_ALLOW_EMPTY_PASSWORD=yes MYSQL_ALLOW_EMPTY_PASSWORD=yes
MYSQL_ROOT_PASSWORD= MYSQL_ROOT_PASSWORD=
@@ -26,7 +57,3 @@ MYSQL_PASSWORD=ely_accounts_password
RABBITMQ_DEFAULT_USER=ely-accounts-app RABBITMQ_DEFAULT_USER=ely-accounts-app
RABBITMQ_DEFAULT_PASS=ely-accounts-app-password RABBITMQ_DEFAULT_PASS=ely-accounts-app-password
RABBITMQ_DEFAULT_VHOST=/ely.by RABBITMQ_DEFAULT_VHOST=/ely.by
# Конфигурация для Dev.
XDEBUG_CONFIG=remote_host=10.254.254.254
PHP_IDE_CONFIG=serverName=docker

View File

@@ -4,21 +4,40 @@ stages:
- release - release
variables: variables:
CONTAINER_IMAGE: registry.ely.by/elyby/accounts DOCKER_DRIVER: aufs
CONTAINER_IMAGE: "registry.ely.by/elyby/accounts"
test:backend: test:backend:
image: jonaskello/docker-and-compose:1.12.1-1.8.0 image: docker:latest
services: services:
- docker:1.12.1-dind - mariadb:10.0
- redis:3.0-alpine
variables:
# mariadb config
MYSQL_RANDOM_ROOT_PASSWORD: "true"
MYSQL_DATABASE: "ely_accounts_test"
MYSQL_USER: "ely_accounts_tester"
MYSQL_PASSWORD: "ely_accounts_tester_password"
stage: test stage: test
before_script: before_script:
- docker login -u gitlab-ci -p $CI_BUILD_TOKEN registry.ely.by - docker login -u gitlab-ci -p $CI_BUILD_TOKEN registry.ely.by
- echo "$SSH_PRIVATE_KEY" > id_rsa - echo "$SSH_PRIVATE_KEY" > id_rsa
- docker-compose -f tests/docker-compose.yml build --pull testphp
after_script:
- docker-compose -f tests/docker-compose.yml down -v
script: script:
- docker-compose -f tests/docker-compose.yml run --rm testphp ./vendor/bin/codecept run -c tests - export TEMP_DEV_IMAGE="${CONTAINER_IMAGE}:ci-${CI_BUILD_ID}"
- docker build --pull -f Dockerfile-dev -t $TEMP_DEV_IMAGE .
- >
docker run --rm
--add-host=mariadb:`getent hosts mariadb | awk '{ print $1 ; exit }'`
--add-host=redis:`getent hosts redis | awk '{ print $1 ; exit }'`
-e YII_DEBUG="true"
-e YII_ENV="test"
-e DB_HOST="mariadb"
-e DB_DATABASE="ely_accounts_test"
-e DB_USER="ely_accounts_tester"
-e DB_PASSWORD="ely_accounts_tester_password"
-e REDIS_HOST="redis"
$TEMP_DEV_IMAGE
php vendor/bin/codecept run -c tests
test:frontend: test:frontend:
image: node:5.12 image: node:5.12
@@ -28,8 +47,8 @@ test:frontend:
- frontend/node_modules - frontend/node_modules
script: script:
- cd frontend - cd frontend
- npm i --silent - npm i --silent > /dev/null
- npm run test - npm run test --silent
build:production: build:production:
image: docker:latest image: docker:latest

View File

@@ -1,4 +1,8 @@
FROM registry.ely.by/elyby/accounts-php:1.0.0 FROM registry.ely.by/elyby/accounts-php:1.2.0
# Вносим конфигурации для крона и воркеров
COPY docker/cron/* /etc/cron.d/
COPY docker/supervisor/* /etc/supervisor/conf.d/
COPY id_rsa /root/.ssh/id_rsa COPY id_rsa /root/.ssh/id_rsa
@@ -7,7 +11,7 @@ RUN chmod 400 ~/.ssh/id_rsa \
&& eval $(ssh-agent -s) \ && eval $(ssh-agent -s) \
&& ssh-add /root/.ssh/id_rsa \ && ssh-add /root/.ssh/id_rsa \
&& touch /root/.ssh/known_hosts \ && touch /root/.ssh/known_hosts \
&& ssh-keyscan gitlab.com >> /root/.ssh/known_hosts && ssh-keyscan gitlab.com gitlab.ely.by >> /root/.ssh/known_hosts
# Копируем composer.json в родительскую директорию, которая не будет синкаться с хостом через # Копируем composer.json в родительскую директорию, которая не будет синкаться с хостом через
# volume на dev окружении. В entrypoint эта папка будет скопирована обратно. # volume на dev окружении. В entrypoint эта папка будет скопирована обратно.
@@ -28,7 +32,7 @@ COPY ./frontend/scripts /var/www/frontend/scripts
COPY ./frontend/webpack-utils /var/www/frontend/webpack-utils COPY ./frontend/webpack-utils /var/www/frontend/webpack-utils
RUN cd ../frontend \ RUN cd ../frontend \
&& npm install \ && npm install --quiet --depth -1 \
&& cd - && cd -
# Удаляем ключи из production контейнера на всякий случай # Удаляем ключи из production контейнера на всякий случай
@@ -42,7 +46,7 @@ RUN mkdir -p api/runtime api/web/assets console/runtime \
# Билдим фронт # Билдим фронт
&& cd frontend \ && cd frontend \
&& ln -s /var/www/frontend/node_modules $PWD/node_modules \ && ln -s /var/www/frontend/node_modules $PWD/node_modules \
&& npm run build \ && npm run build:quite --quiet \
&& rm node_modules \ && rm node_modules \
# Копируем билд наружу, чтобы его не затёрло volume в dev режиме # Копируем билд наружу, чтобы его не затёрло volume в dev режиме
&& cp -r ./dist /var/www/dist \ && cp -r ./dist /var/www/dist \

View File

@@ -1,4 +1,8 @@
FROM registry.ely.by/elyby/accounts-php:1.0.0-dev FROM registry.ely.by/elyby/accounts-php:1.2.0-dev
# Вносим конфигурации для крона и воркеров
COPY docker/cron/* /etc/cron.d/
COPY docker/supervisor/* /etc/supervisor/conf.d/
COPY id_rsa /root/.ssh/id_rsa COPY id_rsa /root/.ssh/id_rsa
@@ -7,7 +11,7 @@ RUN chmod 400 ~/.ssh/id_rsa \
&& eval $(ssh-agent -s) \ && eval $(ssh-agent -s) \
&& ssh-add /root/.ssh/id_rsa \ && ssh-add /root/.ssh/id_rsa \
&& touch /root/.ssh/known_hosts \ && touch /root/.ssh/known_hosts \
&& ssh-keyscan gitlab.com >> /root/.ssh/known_hosts && ssh-keyscan gitlab.com gitlab.ely.by >> /root/.ssh/known_hosts
# Копируем composer.json в родительскую директорию, которая не будет синкаться с хостом через # Копируем composer.json в родительскую директорию, которая не будет синкаться с хостом через
# volume на dev окружении. В entrypoint эта папка будет скопирована обратно. # volume на dev окружении. В entrypoint эта папка будет скопирована обратно.
@@ -28,7 +32,7 @@ COPY ./frontend/scripts /var/www/frontend/scripts
COPY ./frontend/webpack-utils /var/www/frontend/webpack-utils COPY ./frontend/webpack-utils /var/www/frontend/webpack-utils
RUN cd ../frontend \ RUN cd ../frontend \
&& npm install \ && npm install --quiet --depth -1 \
&& cd - && cd -
# Наконец переносим все сорцы внутрь контейнера # Наконец переносим все сорцы внутрь контейнера
@@ -39,7 +43,7 @@ RUN mkdir -p api/runtime api/web/assets console/runtime \
# Билдим фронт # Билдим фронт
&& cd frontend \ && cd frontend \
&& ln -s /var/www/frontend/node_modules $PWD/node_modules \ && ln -s /var/www/frontend/node_modules $PWD/node_modules \
&& npm run build \ && npm run build:quite --quiet \
&& rm node_modules \ && rm node_modules \
# Копируем билд наружу, чтобы его не затёрло volume в dev режиме # Копируем билд наружу, чтобы его не затёрло volume в dev режиме
&& cp -r ./dist /var/www/dist \ && cp -r ./dist /var/www/dist \

View File

@@ -1,6 +1,6 @@
# Accounts Ely.by # Accounts Ely.by
## Развёртывание dev ## Развёртывание dev [backend]
Предварительно нужно установить [git](https://git-scm.com/downloads), Предварительно нужно установить [git](https://git-scm.com/downloads),
[docker](https://docs.docker.com/engine/installation/) и его [docker](https://docs.docker.com/engine/installation/) и его
@@ -15,8 +15,8 @@
За тем сливаем репозиторий: За тем сливаем репозиторий:
```sh ```sh
git clone git@gitlab.com:elyby/account.git account.ely.by git clone git@gitlab.ely.by:elyby/accounts.git account.ely.by
cd account.ely.by.local cd account.ely.by
``` ```
Далее нужно создать `.env`, `docker-compose.yml` и `id_rsa` файлы: Далее нужно создать `.env`, `docker-compose.yml` и `id_rsa` файлы:
@@ -27,12 +27,12 @@ cp docker-compose.dev.yml docker-compose.yml
cp ~/.ssh/id_rsa id_rsa # Использовать ссылку нельзя cp ~/.ssh/id_rsa id_rsa # Использовать ссылку нельзя
``` ```
Касательно файла id_rsa: часть зависимостей находятся в наших приватных репозиториях, получить **Касательно файла id_rsa**: часть зависимостей находятся в наших приватных репозиториях, получить
доступ куда можно только в том случае, если в контейнере окажется ключ, который имеет доступ к этим доступ куда можно только в том случае, если в контейнере окажется ключ, который имеет доступ к этим
репозиториям. репозиториям.
Все вышеперечисленные файла находятся под gitignore, так что с полученными файлами можно произвести Все вышеперечисленные файлы находятся под gitignore, так что с конечными файлами можно произвести
все необходимые манипуляции под конкретный кейс использования. **В файле `.env` обязательно следует все необходимые манипуляции под конкретную задачу разработки. **В файле `.env` обязательно следует
задать `JWT_USER_SECRET`, иначе авторизация на бекенде не заработает.** задать `JWT_USER_SECRET`, иначе авторизация на бекенде не заработает.**
После этого просто выполняем старт всех контейнеров: После этого просто выполняем старт всех контейнеров:
@@ -41,10 +41,50 @@ cp ~/.ssh/id_rsa id_rsa # Использовать ссылку нельзя
docker-compose up -d docker-compose up -d
``` ```
Они автоматически сбилдятся и начнут свою работу. Контейнеры автоматически сбилдятся и начнут свою работу.
## Развёртывание dev [frontend]
Чтобы поднять сборку frontend приложения, необходимо иметь установленный в системе [Node.js](https://nodejs.org)
версии 5.x или 6.x, а так же npm 3-ей версии (`npm i -g npm` для обновления).
За тем переходим в папку `frontend` и устанавливаем зависимости:
```sh
cd frontend
npm i
```
После того, как все зависимости будут установлены, можно поднять dev-сервер. Здесь есть 2 пути: можно, следуя
инструкции выше, поднять backend на своей машине через Docker. Если же разработка не привязывается к специфичной
версии backend, то более быстрым и удобным способ будет использовать наш dev-сервер, расположенный под адресу
https://dev.account.ely.by.
В любом из случаев необходимо в папке `frontend/config` скопировать файл `template.env.js` в `env.js` (находится
под .gitignore) и указать в параметре `apiHost` или свой локальный сервер (тот хост, что был указан в .env
как `VIRTUAL_HOST`), или указав просто `https://dev.account.ely.by`.
После того, как это будет сделано, запускаем dev-сервер (находясь в папке frontend):
```
npm start
```
dev-сервер поднимется на 8080 порту и будет доступен по адресу http://localhost:8080.
### Как влезть в работающий контейнер ### Как влезть в работающий контейнер
Начиная с версии docker-compose 1.9.0, появилась команда `docker-compose exec`, которая позволяет выполнить
на работающем контейнере произвольную команду, основываясь на имени сервиса в compose файле.
```
docker-compose exec app bash
```
------------------------
_// Старый вариант_
Сперва, с помощью команды `docker ps` мы увидим все запущенные контейнеры. Нас интересуют значения Сперва, с помощью команды `docker ps` мы увидим все запущенные контейнеры. Нас интересуют значения
из первой колонки CONTAINER ID или NAMES. Узнать, чему они соответствуют можно прочитав название IMAGE из первой колонки CONTAINER ID или NAMES. Узнать, чему они соответствуют можно прочитав название IMAGE
из 2 колонки. Чтобы выполнить команду внутри работабщего контейнера, нужно выполнить: из 2 колонки. Чтобы выполнить команду внутри работабщего контейнера, нужно выполнить:

View File

@@ -1,7 +1,7 @@
<?php <?php
namespace api\components\ApiUser; namespace api\components\ApiUser;
use common\models\OauthAccessToken; use Yii;
use yii\rbac\CheckAccessInterface; use yii\rbac\CheckAccessInterface;
class AuthChecker implements CheckAccessInterface { class AuthChecker implements CheckAccessInterface {
@@ -10,13 +10,12 @@ class AuthChecker implements CheckAccessInterface {
* @inheritdoc * @inheritdoc
*/ */
public function checkAccess($token, $permissionName, $params = []) : bool { public function checkAccess($token, $permissionName, $params = []) : bool {
/** @var OauthAccessToken|null $accessToken */ $accessToken = Yii::$app->oauth->getAuthServer()->getAccessTokenStorage()->get($token);
$accessToken = OauthAccessToken::findOne($token);
if ($accessToken === null) { if ($accessToken === null) {
return false; return false;
} }
return $accessToken->getScopes()->exists($permissionName); return $accessToken->hasScope($permissionName);
} }
} }

View File

@@ -1,10 +1,11 @@
<?php <?php
namespace api\components\ApiUser; namespace api\components\ApiUser;
use api\components\OAuth2\Entities\AccessTokenEntity;
use common\models\Account; use common\models\Account;
use common\models\OauthAccessToken;
use common\models\OauthClient; use common\models\OauthClient;
use common\models\OauthSession; use common\models\OauthSession;
use Yii;
use yii\base\NotSupportedException; use yii\base\NotSupportedException;
use yii\web\IdentityInterface; use yii\web\IdentityInterface;
use yii\web\UnauthorizedHttpException; use yii\web\UnauthorizedHttpException;
@@ -13,12 +14,12 @@ use yii\web\UnauthorizedHttpException;
* @property Account $account * @property Account $account
* @property OauthClient $client * @property OauthClient $client
* @property OauthSession $session * @property OauthSession $session
* @property OauthAccessToken $accessToken * @property AccessTokenEntity $accessToken
*/ */
class Identity implements IdentityInterface { class Identity implements IdentityInterface {
/** /**
* @var OauthAccessToken * @var AccessTokenEntity
*/ */
private $_accessToken; private $_accessToken;
@@ -26,8 +27,7 @@ class Identity implements IdentityInterface {
* @inheritdoc * @inheritdoc
*/ */
public static function findIdentityByAccessToken($token, $type = null) { public static function findIdentityByAccessToken($token, $type = null) {
/** @var OauthAccessToken|null $model */ $model = Yii::$app->oauth->getAuthServer()->getAccessTokenStorage()->get($token);
$model = OauthAccessToken::findOne($token);
if ($model === null) { if ($model === null) {
throw new UnauthorizedHttpException('Incorrect token'); throw new UnauthorizedHttpException('Incorrect token');
} elseif ($model->isExpired()) { } elseif ($model->isExpired()) {
@@ -37,7 +37,7 @@ class Identity implements IdentityInterface {
return new static($model); return new static($model);
} }
private function __construct(OauthAccessToken $accessToken) { private function __construct(AccessTokenEntity $accessToken) {
$this->_accessToken = $accessToken; $this->_accessToken = $accessToken;
} }
@@ -50,20 +50,20 @@ class Identity implements IdentityInterface {
} }
public function getSession() : OauthSession { public function getSession() : OauthSession {
return $this->_accessToken->session; return OauthSession::findOne($this->_accessToken->getSessionId());
} }
public function getAccessToken() : OauthAccessToken { public function getAccessToken() : AccessTokenEntity {
return $this->_accessToken; return $this->_accessToken;
} }
/** /**
* Этот метод используется для получения пользователя, к которому привязаны права. * Этот метод используется для получения токена, к которому привязаны права.
* У нас права привязываются к токенам, так что возвращаем именно его id. * У нас права привязываются к токенам, так что возвращаем именно его id.
* @inheritdoc * @inheritdoc
*/ */
public function getId() { public function getId() {
return $this->_accessToken->access_token; return $this->_accessToken->getId();
} }
public function getAuthKey() { public function getAuthKey() {

View File

@@ -1,13 +1,13 @@
<?php <?php
namespace common\components\oauth; namespace api\components\OAuth2;
use common\components\oauth\Storage\Redis\AuthCodeStorage; use api\components\OAuth2\Storage\AuthCodeStorage;
use common\components\oauth\Storage\Redis\RefreshTokenStorage; use api\components\OAuth2\Storage\RefreshTokenStorage;
use common\components\oauth\Storage\Yii2\AccessTokenStorage; use api\components\OAuth2\Storage\AccessTokenStorage;
use common\components\oauth\Storage\Yii2\ClientStorage; use api\components\OAuth2\Storage\ClientStorage;
use common\components\oauth\Storage\Yii2\ScopeStorage; use api\components\OAuth2\Storage\ScopeStorage;
use common\components\oauth\Storage\Yii2\SessionStorage; use api\components\OAuth2\Storage\SessionStorage;
use common\components\oauth\Util\KeyAlgorithm\UuidAlgorithm; use api\components\OAuth2\Utils\KeyAlgorithm\UuidAlgorithm;
use League\OAuth2\Server\AuthorizationServer; use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\Grant; use League\OAuth2\Server\Grant;
use League\OAuth2\Server\Util\SecureKey; use League\OAuth2\Server\Util\SecureKey;
@@ -41,22 +41,22 @@ class Component extends \yii\base\Component {
public function getAuthServer() { public function getAuthServer() {
if ($this->_authServer === null) { if ($this->_authServer === null) {
$authServer = new AuthorizationServer(); $authServer = new AuthorizationServer();
$authServer $authServer->setAccessTokenStorage(new AccessTokenStorage());
->setAccessTokenStorage(new AccessTokenStorage()) $authServer->setClientStorage(new ClientStorage());
->setClientStorage(new ClientStorage()) $authServer->setScopeStorage(new ScopeStorage());
->setScopeStorage(new ScopeStorage()) $authServer->setSessionStorage(new SessionStorage());
->setSessionStorage(new SessionStorage()) $authServer->setAuthCodeStorage(new AuthCodeStorage());
->setAuthCodeStorage(new AuthCodeStorage()) $authServer->setRefreshTokenStorage(new RefreshTokenStorage());
->setRefreshTokenStorage(new RefreshTokenStorage()) $authServer->setScopeDelimiter(',');
->setScopeDelimiter(',');
$this->_authServer = $authServer; $this->_authServer = $authServer;
foreach ($this->grantTypes as $grantType) { foreach ($this->grantTypes as $grantType) {
if (!array_key_exists($grantType, $this->grantMap)) { if (!isset($this->grantMap[$grantType])) {
throw new InvalidConfigException('Invalid grant type'); throw new InvalidConfigException('Invalid grant type');
} }
/** @var Grant\GrantTypeInterface $grant */
$grant = new $this->grantMap[$grantType](); $grant = new $this->grantMap[$grantType]();
$this->_authServer->addGrantType($grant); $this->_authServer->addGrantType($grant);
} }

View File

@@ -0,0 +1,44 @@
<?php
namespace api\components\OAuth2\Entities;
use api\components\OAuth2\Storage\SessionStorage;
use ErrorException;
use League\OAuth2\Server\Entity\SessionEntity as OriginalSessionEntity;
class AccessTokenEntity extends \League\OAuth2\Server\Entity\AccessTokenEntity {
protected $sessionId;
public function getSessionId() {
return $this->sessionId;
}
public function setSessionId($sessionId) {
$this->sessionId = $sessionId;
}
/**
* @inheritdoc
* @return static
*/
public function setSession(OriginalSessionEntity $session) {
parent::setSession($session);
$this->sessionId = $session->getId();
return $this;
}
public function getSession() {
if ($this->session instanceof OriginalSessionEntity) {
return $this->session;
}
$sessionStorage = $this->server->getSessionStorage();
if (!$sessionStorage instanceof SessionStorage) {
throw new ErrorException('SessionStorage must be instance of ' . SessionStorage::class);
}
return $sessionStorage->getById($this->sessionId);
}
}

View File

@@ -1,11 +1,9 @@
<?php <?php
namespace common\components\oauth\Entity; namespace api\components\OAuth2\Entities;
use League\OAuth2\Server\Entity\EntityTrait;
use League\OAuth2\Server\Entity\SessionEntity as OriginalSessionEntity; use League\OAuth2\Server\Entity\SessionEntity as OriginalSessionEntity;
class AccessTokenEntity extends \League\OAuth2\Server\Entity\AccessTokenEntity { class AuthCodeEntity extends \League\OAuth2\Server\Entity\AuthCodeEntity {
use EntityTrait;
protected $sessionId; protected $sessionId;
@@ -24,4 +22,8 @@ class AccessTokenEntity extends \League\OAuth2\Server\Entity\AccessTokenEntity {
return $this; return $this;
} }
public function setSessionId(string $sessionId) {
$this->sessionId = $sessionId;
}
} }

View File

@@ -0,0 +1,22 @@
<?php
namespace api\components\OAuth2\Entities;
class ClientEntity extends \League\OAuth2\Server\Entity\ClientEntity {
public function setId(string $id) {
$this->id = $id;
}
public function setName(string $name) {
$this->name = $name;
}
public function setSecret(string $secret) {
$this->secret = $secret;
}
public function setRedirectUri($redirectUri) {
$this->redirectUri = $redirectUri;
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace api\components\OAuth2\Entities;
use api\components\OAuth2\Storage\SessionStorage;
use ErrorException;
use League\OAuth2\Server\Entity\SessionEntity as OriginalSessionEntity;
class RefreshTokenEntity extends \League\OAuth2\Server\Entity\RefreshTokenEntity {
private $sessionId;
public function isExpired() : bool {
return false;
}
public function getSession() : SessionEntity {
if ($this->session instanceof SessionEntity) {
return $this->session;
}
$sessionStorage = $this->server->getSessionStorage();
if (!$sessionStorage instanceof SessionStorage) {
throw new ErrorException('SessionStorage must be instance of ' . SessionStorage::class);
}
return $sessionStorage->getById($this->sessionId);
}
public function getSessionId() : int {
return $this->sessionId;
}
public function setSession(OriginalSessionEntity $session) {
parent::setSession($session);
$this->setSessionId($session->getId());
return $this;
}
public function setSessionId(int $sessionId) {
$this->sessionId = $sessionId;
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace api\components\OAuth2\Entities;
class ScopeEntity extends \League\OAuth2\Server\Entity\ScopeEntity {
public function setId(string $id) {
$this->id = $id;
}
}

View File

@@ -1,7 +1,7 @@
<?php <?php
namespace common\components\oauth\Entity; namespace api\components\OAuth2\Entities;
use League\OAuth2\Server\Entity\ClientEntity; use League\OAuth2\Server\Entity\ClientEntity as OriginalClientEntity;
use League\OAuth2\Server\Entity\EntityTrait; use League\OAuth2\Server\Entity\EntityTrait;
class SessionEntity extends \League\OAuth2\Server\Entity\SessionEntity { class SessionEntity extends \League\OAuth2\Server\Entity\SessionEntity {
@@ -13,15 +13,15 @@ class SessionEntity extends \League\OAuth2\Server\Entity\SessionEntity {
return $this->clientId; return $this->clientId;
} }
/** public function associateClient(OriginalClientEntity $client) {
* @inheritdoc
* @return static
*/
public function associateClient(ClientEntity $client) {
parent::associateClient($client); parent::associateClient($client);
$this->clientId = $client->getId(); $this->clientId = $client->getId();
return $this; return $this;
} }
public function setClientId(string $clientId) {
$this->clientId = $clientId;
}
} }

View File

@@ -1,5 +1,5 @@
<?php <?php
namespace common\components\oauth\Exception; namespace api\components\OAuth2\Exception;
use League\OAuth2\Server\Exception\OAuthException; use League\OAuth2\Server\Exception\OAuthException;

View File

@@ -1,5 +1,5 @@
<?php <?php
namespace common\components\oauth\Exception; namespace api\components\OAuth2\Exception;
class AccessDeniedException extends \League\OAuth2\Server\Exception\AccessDeniedException { class AccessDeniedException extends \League\OAuth2\Server\Exception\AccessDeniedException {

View File

@@ -0,0 +1,20 @@
<?php
namespace api\components\OAuth2\Grants;
use api\components\OAuth2\Entities;
class AuthCodeGrant extends \League\OAuth2\Server\Grant\AuthCodeGrant {
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);
}
}

View File

@@ -0,0 +1,150 @@
<?php
namespace api\components\OAuth2\Grants;
use api\components\OAuth2\Entities;
use ErrorException;
use League\OAuth2\Server\Entity\ClientEntity as OriginalClientEntity;
use League\OAuth2\Server\Entity\RefreshTokenEntity as OriginalRefreshTokenEntity;
use League\OAuth2\Server\Event;
use League\OAuth2\Server\Exception;
use League\OAuth2\Server\Util\SecureKey;
class RefreshTokenGrant extends \League\OAuth2\Server\Grant\RefreshTokenGrant {
public $refreshTokenRotate = false;
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);
}
/**
* Метод таки пришлось переписать по той причине, что нынче мы храним access_token в redis с expire значением,
* так что он может банально несуществовать на тот момент, когда к нему через refresh_token попытаются обратиться.
* Поэтому мы расширили логику RefreshTokenEntity и она теперь знает о сессии, в рамках которой была создана
*
* @inheritdoc
*/
public function completeFlow() {
$clientId = $this->server->getRequest()->request->get('client_id', $this->server->getRequest()->getUser());
if (is_null($clientId)) {
throw new Exception\InvalidRequestException('client_id');
}
$clientSecret = $this->server->getRequest()->request->get(
'client_secret',
$this->server->getRequest()->getPassword()
);
if ($this->shouldRequireClientSecret() && is_null($clientSecret)) {
throw new Exception\InvalidRequestException('client_secret');
}
// Validate client ID and client secret
$client = $this->server->getClientStorage()->get(
$clientId,
$clientSecret,
null,
$this->getIdentifier()
);
if (($client instanceof OriginalClientEntity) === false) {
$this->server->getEventEmitter()->emit(new Event\ClientAuthenticationFailedEvent($this->server->getRequest()));
throw new Exception\InvalidClientException();
}
$oldRefreshTokenParam = $this->server->getRequest()->request->get('refresh_token', null);
if ($oldRefreshTokenParam === null) {
throw new Exception\InvalidRequestException('refresh_token');
}
// Validate refresh token
$oldRefreshToken = $this->server->getRefreshTokenStorage()->get($oldRefreshTokenParam);
if (($oldRefreshToken instanceof OriginalRefreshTokenEntity) === false) {
throw new Exception\InvalidRefreshException();
}
// Ensure the old refresh token hasn't expired
if ($oldRefreshToken->isExpired()) {
throw new Exception\InvalidRefreshException();
}
/** @var Entities\AccessTokenEntity|null $oldAccessToken */
$oldAccessToken = $oldRefreshToken->getAccessToken();
if ($oldAccessToken instanceof Entities\AccessTokenEntity) {
// Get the scopes for the original session
$session = $oldAccessToken->getSession();
} else {
if (!$oldRefreshToken instanceof Entities\RefreshTokenEntity) {
throw new ErrorException('oldRefreshToken must be instance of ' . Entities\RefreshTokenEntity::class);
}
$session = $oldRefreshToken->getSession();
}
$scopes = $this->formatScopes($session->getScopes());
// Get and validate any requested scopes
$requestedScopesString = $this->server->getRequest()->request->get('scope', '');
$requestedScopes = $this->validateScopes($requestedScopesString, $client);
// If no new scopes are requested then give the access token the original session scopes
if (count($requestedScopes) === 0) {
$newScopes = $scopes;
} else {
// The OAuth spec says that a refreshed access token can have the original scopes or fewer so ensure
// the request doesn't include any new scopes
foreach ($requestedScopes as $requestedScope) {
if (!isset($scopes[$requestedScope->getId()])) {
throw new Exception\InvalidScopeException($requestedScope->getId());
}
}
$newScopes = $requestedScopes;
}
// Generate a new access token and assign it the correct sessions
$newAccessToken = $this->createAccessTokenEntity();
$newAccessToken->setId(SecureKey::generate());
$newAccessToken->setExpireTime($this->getAccessTokenTTL() + time());
$newAccessToken->setSession($session);
foreach ($newScopes as $newScope) {
$newAccessToken->associateScope($newScope);
}
// Expire the old token and save the new one
($oldAccessToken instanceof Entities\AccessTokenEntity) && $oldAccessToken->expire();
$newAccessToken->save();
$this->server->getTokenType()->setSession($session);
$this->server->getTokenType()->setParam('access_token', $newAccessToken->getId());
$this->server->getTokenType()->setParam('expires_in', $this->getAccessTokenTTL());
if ($this->shouldRotateRefreshTokens()) {
// Expire the old refresh token
$oldRefreshToken->expire();
// Generate a new refresh token
$newRefreshToken = $this->createRefreshTokenEntity();
$newRefreshToken->setId(SecureKey::generate());
$newRefreshToken->setExpireTime($this->getRefreshTokenTTL() + time());
$newRefreshToken->setAccessToken($newAccessToken);
$newRefreshToken->save();
$this->server->getTokenType()->setParam('refresh_token', $newRefreshToken->getId());
} else {
$oldRefreshToken->setAccessToken($newAccessToken);
$oldRefreshToken->save();
}
return $this->server->getTokenType()->generateResponse();
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace api\components\OAuth2\Storage;
use api\components\OAuth2\Entities\AccessTokenEntity;
use common\components\Redis\Key;
use common\components\Redis\Set;
use League\OAuth2\Server\Entity\AccessTokenEntity as OriginalAccessTokenEntity;
use League\OAuth2\Server\Entity\ScopeEntity;
use League\OAuth2\Server\Storage\AbstractStorage;
use League\OAuth2\Server\Storage\AccessTokenInterface;
use yii\helpers\Json;
class AccessTokenStorage extends AbstractStorage implements AccessTokenInterface {
public $dataTable = 'oauth_access_tokens';
public function get($token) {
$result = Json::decode((new Key($this->dataTable, $token))->getValue());
$token = new AccessTokenEntity($this->server);
$token->setId($result['id']);
$token->setExpireTime($result['expire_time']);
$token->setSessionId($result['session_id']);
return $token;
}
public function getScopes(OriginalAccessTokenEntity $token) {
$scopes = $this->scopes($token->getId());
$entities = [];
foreach($scopes as $scope) {
if ($this->server->getScopeStorage()->get($scope) !== null) {
$entities[] = (new ScopeEntity($this->server))->hydrate(['id' => $scope]);
}
}
return $entities;
}
public function create($token, $expireTime, $sessionId) {
$payload = Json::encode([
'id' => $token,
'expire_time' => $expireTime,
'session_id' => $sessionId,
]);
$this->key($token)->setValue($payload)->expireAt($expireTime);
}
public function associateScope(OriginalAccessTokenEntity $token, ScopeEntity $scope) {
$this->scopes($token->getId())->add($scope->getId())->expireAt($token->getExpireTime());
}
public function delete(OriginalAccessTokenEntity $token) {
$this->key($token->getId())->delete();
$this->scopes($token->getId())->delete();
}
private function key(string $token) : Key {
return new Key($this->dataTable, $token);
}
private function scopes(string $token) : Set {
return new Set($this->dataTable, $token, 'scopes');
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace api\components\OAuth2\Storage;
use api\components\OAuth2\Entities\AuthCodeEntity;
use common\components\Redis\Key;
use common\components\Redis\Set;
use League\OAuth2\Server\Entity\AuthCodeEntity as OriginalAuthCodeEntity;
use League\OAuth2\Server\Entity\ScopeEntity;
use League\OAuth2\Server\Storage\AbstractStorage;
use League\OAuth2\Server\Storage\AuthCodeInterface;
use yii\helpers\Json;
class AuthCodeStorage extends AbstractStorage implements AuthCodeInterface {
public $dataTable = 'oauth_auth_codes';
public function get($code) {
$result = Json::decode((new Key($this->dataTable, $code))->getValue());
if ($result === null) {
return null;
}
$entity = new AuthCodeEntity($this->server);
$entity->setId($result['id']);
$entity->setExpireTime($result['expire_time']);
$entity->setSessionId($result['session_id']);
$entity->setRedirectUri($result['client_redirect_uri']);
return $entity;
}
public function create($token, $expireTime, $sessionId, $redirectUri) {
$payload = Json::encode([
'id' => $token,
'expire_time' => $expireTime,
'session_id' => $sessionId,
'client_redirect_uri' => $redirectUri,
]);
$this->key($token)->setValue($payload)->expireAt($expireTime);
}
public function getScopes(OriginalAuthCodeEntity $token) {
$scopes = $this->scopes($token->getId());
$scopesEntities = [];
foreach ($scopes as $scope) {
if ($this->server->getScopeStorage()->get($scope) !== null) {
$scopesEntities[] = (new ScopeEntity($this->server))->hydrate(['id' => $scope]);
}
}
return $scopesEntities;
}
public function associateScope(OriginalAuthCodeEntity $token, ScopeEntity $scope) {
$this->scopes($token->getId())->add($scope->getId())->expireAt($token->getExpireTime());
}
public function delete(OriginalAuthCodeEntity $token) {
$this->key($token->getId())->delete();
$this->scopes($token->getId())->delete();
}
private function key(string $token) : Key {
return new Key($this->dataTable, $token);
}
private function scopes(string $token) : Set {
return new Set($this->dataTable, $token, 'scopes');
}
}

View File

@@ -1,9 +1,9 @@
<?php <?php
namespace common\components\oauth\Storage\Yii2; namespace api\components\OAuth2\Storage;
use common\components\oauth\Entity\SessionEntity; use api\components\OAuth2\Entities\ClientEntity;
use api\components\OAuth2\Entities\SessionEntity;
use common\models\OauthClient; use common\models\OauthClient;
use League\OAuth2\Server\Entity\ClientEntity;
use League\OAuth2\Server\Entity\SessionEntity as OriginalSessionEntity; use League\OAuth2\Server\Entity\SessionEntity as OriginalSessionEntity;
use League\OAuth2\Server\Storage\AbstractStorage; use League\OAuth2\Server\Storage\AbstractStorage;
use League\OAuth2\Server\Storage\ClientInterface; use League\OAuth2\Server\Storage\ClientInterface;
@@ -18,15 +18,13 @@ class ClientStorage extends AbstractStorage implements ClientInterface {
* @inheritdoc * @inheritdoc
*/ */
public function get($clientId, $clientSecret = null, $redirectUri = null, $grantType = null) { public function get($clientId, $clientSecret = null, $redirectUri = null, $grantType = null) {
$query = OauthClient::find() $query = OauthClient::find()->andWhere(['id' => $clientId]);
->select(['id', 'name', 'secret', 'redirect_uri'])
->where([OauthClient::tableName() . '.id' => $clientId]);
if ($clientSecret !== null) { if ($clientSecret !== null) {
$query->andWhere(['secret' => $clientSecret]); $query->andWhere(['secret' => $clientSecret]);
} }
$model = $query->asArray()->one(); /** @var OauthClient|null $model */
$model = $query->one();
if ($model === null) { if ($model === null) {
return null; return null;
} }
@@ -39,22 +37,17 @@ class ClientStorage extends AbstractStorage implements ClientInterface {
* Короче это нужно учесть * Короче это нужно учесть
*/ */
if ($redirectUri !== null) { if ($redirectUri !== null) {
if ($redirectUri === self::REDIRECT_STATIC_PAGE || $redirectUri === self::REDIRECT_STATIC_PAGE_WITH_CODE) { if (in_array($redirectUri, [self::REDIRECT_STATIC_PAGE, self::REDIRECT_STATIC_PAGE_WITH_CODE], true)) {
// Тут, наверное, нужно проверить тип приложения // Тут, наверное, нужно проверить тип приложения
} else { } else {
if (!StringHelper::startsWith($redirectUri, $model['redirect_uri'], false)) { if (!StringHelper::startsWith($redirectUri, $model->redirect_uri, false)) {
return null; return null;
} }
} }
} }
$entity = new ClientEntity($this->server); $entity = $this->hydrate($model);
$entity->hydrate([ $entity->setRedirectUri($redirectUri);
'id' => $model['id'],
'name' => $model['name'],
'secret' => $model['secret'],
'redirectUri' => $redirectUri,
]);
return $entity; return $entity;
} }
@@ -67,17 +60,23 @@ class ClientStorage extends AbstractStorage implements ClientInterface {
throw new \ErrorException('This module assumes that $session typeof ' . SessionEntity::class); throw new \ErrorException('This module assumes that $session typeof ' . SessionEntity::class);
} }
$model = OauthClient::find() /** @var OauthClient|null $model */
->select(['id', 'name']) $model = OauthClient::findOne($session->getClientId());
->andWhere(['id' => $session->getClientId()])
->asArray()
->one();
if ($model === null) { if ($model === null) {
return null; return null;
} }
return (new ClientEntity($this->server))->hydrate($model); return $this->hydrate($model);
}
private function hydrate(OauthClient $model) : ClientEntity {
$entity = new ClientEntity($this->server);
$entity->setId($model->id);
$entity->setName($model->name);
$entity->setSecret($model->secret);
$entity->setRedirectUri($model->redirect_uri);
return $entity;
} }
} }

View File

@@ -0,0 +1,60 @@
<?php
namespace api\components\OAuth2\Storage;
use api\components\OAuth2\Entities\RefreshTokenEntity;
use common\components\Redis\Key;
use common\components\Redis\Set;
use common\models\OauthSession;
use ErrorException;
use League\OAuth2\Server\Entity\RefreshTokenEntity as OriginalRefreshTokenEntity;
use League\OAuth2\Server\Storage\AbstractStorage;
use League\OAuth2\Server\Storage\RefreshTokenInterface;
use Yii;
use yii\helpers\Json;
class RefreshTokenStorage extends AbstractStorage implements RefreshTokenInterface {
public $dataTable = 'oauth_refresh_tokens';
public function get($token) {
$result = Json::decode((new Key($this->dataTable, $token))->getValue());
$entity = new RefreshTokenEntity($this->server);
$entity->setId($result['id']);
$entity->setAccessTokenId($result['access_token_id']);
$entity->setSessionId($result['session_id']);
return $entity;
}
public function create($token, $expireTime, $accessToken) {
$sessionId = $this->server->getAccessTokenStorage()->get($accessToken)->getSession()->getId();
$payload = Json::encode([
'id' => $token,
'access_token_id' => $accessToken,
'session_id' => $sessionId,
]);
$this->key($token)->setValue($payload);
$this->sessionHash($sessionId)->add($token);
}
public function delete(OriginalRefreshTokenEntity $token) {
if (!$token instanceof RefreshTokenEntity) {
throw new ErrorException('Token must be instance of ' . RefreshTokenEntity::class);
}
$this->key($token->getId())->delete();
$this->sessionHash($token->getSessionId())->remove($token->getId());
}
public function sessionHash(string $sessionId) : Set {
$tableName = Yii::$app->db->getSchema()->getRawTableName(OauthSession::tableName());
return new Set($tableName, $sessionId, 'refresh_tokens');
}
private function key(string $token) : Key {
return new Key($this->dataTable, $token);
}
}

View File

@@ -1,8 +1,8 @@
<?php <?php
namespace common\components\oauth\Storage\Yii2; namespace api\components\OAuth2\Storage;
use api\components\OAuth2\Entities\ScopeEntity;
use common\models\OauthScope; use common\models\OauthScope;
use League\OAuth2\Server\Entity\ScopeEntity;
use League\OAuth2\Server\Storage\AbstractStorage; use League\OAuth2\Server\Storage\AbstractStorage;
use League\OAuth2\Server\Storage\ScopeInterface; use League\OAuth2\Server\Storage\ScopeInterface;
@@ -12,13 +12,12 @@ class ScopeStorage extends AbstractStorage implements ScopeInterface {
* @inheritdoc * @inheritdoc
*/ */
public function get($scope, $grantType = null, $clientId = null) { public function get($scope, $grantType = null, $clientId = null) {
$row = OauthScope::find()->andWhere(['id' => $scope])->asArray()->one(); if (!in_array($scope, OauthScope::getScopes(), true)) {
if ($row === null) {
return null; return null;
} }
$entity = new ScopeEntity($this->server); $entity = new ScopeEntity($this->server);
$entity->hydrate($row); $entity->setId($scope);
return $entity; return $entity;
} }

View File

@@ -1,8 +1,8 @@
<?php <?php
namespace common\components\oauth\Storage\Yii2; namespace api\components\OAuth2\Storage;
use common\components\oauth\Entity\AuthCodeEntity; use api\components\OAuth2\Entities\AuthCodeEntity;
use common\components\oauth\Entity\SessionEntity; use api\components\OAuth2\Entities\SessionEntity;
use common\models\OauthSession; use common\models\OauthSession;
use ErrorException; use ErrorException;
use League\OAuth2\Server\Entity\AccessTokenEntity as OriginalAccessTokenEntity; use League\OAuth2\Server\Entity\AccessTokenEntity as OriginalAccessTokenEntity;
@@ -11,85 +11,41 @@ use League\OAuth2\Server\Entity\ScopeEntity;
use League\OAuth2\Server\Entity\SessionEntity as OriginalSessionEntity; use League\OAuth2\Server\Entity\SessionEntity as OriginalSessionEntity;
use League\OAuth2\Server\Storage\AbstractStorage; use League\OAuth2\Server\Storage\AbstractStorage;
use League\OAuth2\Server\Storage\SessionInterface; use League\OAuth2\Server\Storage\SessionInterface;
use yii\db\ActiveQuery;
use yii\db\Exception; use yii\db\Exception;
class SessionStorage extends AbstractStorage implements SessionInterface { class SessionStorage extends AbstractStorage implements SessionInterface {
private $cache = [];
/**
* @param string $sessionId
* @return OauthSession|null
*/
private function getSessionModel($sessionId) {
if (!isset($this->cache[$sessionId])) {
$this->cache[$sessionId] = OauthSession::findOne($sessionId);
}
return $this->cache[$sessionId];
}
private function hydrateEntity($sessionModel) {
if (!$sessionModel instanceof OauthSession) {
return null;
}
return (new SessionEntity($this->server))->hydrate([
'id' => $sessionModel->id,
'client_id' => $sessionModel->client_id,
])->setOwner($sessionModel->owner_type, $sessionModel->owner_id);
}
/** /**
* @param string $sessionId * @param string $sessionId
* @return SessionEntity|null * @return SessionEntity|null
*/ */
public function getSession($sessionId) { public function getById($sessionId) {
return $this->hydrateEntity($this->getSessionModel($sessionId)); return $this->hydrate($this->getSessionModel($sessionId));
} }
/**
* @inheritdoc
*/
public function getByAccessToken(OriginalAccessTokenEntity $accessToken) { public function getByAccessToken(OriginalAccessTokenEntity $accessToken) {
/** @var OauthSession|null $model */ throw new ErrorException('This method is not implemented and should not be used');
$model = OauthSession::find()->innerJoinWith([
'accessTokens' => function(ActiveQuery $query) use ($accessToken) {
$query->andWhere(['access_token' => $accessToken->getId()]);
},
])->one();
return $this->hydrateEntity($model);
} }
/**
* @inheritdoc
*/
public function getByAuthCode(OriginalAuthCodeEntity $authCode) { public function getByAuthCode(OriginalAuthCodeEntity $authCode) {
if (!$authCode instanceof AuthCodeEntity) { if (!$authCode instanceof AuthCodeEntity) {
throw new ErrorException('This module assumes that $authCode typeof ' . AuthCodeEntity::class); throw new ErrorException('This module assumes that $authCode typeof ' . AuthCodeEntity::class);
} }
return $this->getSession($authCode->getSessionId()); return $this->getById($authCode->getSessionId());
} }
/**
* {@inheritdoc}
*/
public function getScopes(OriginalSessionEntity $session) { public function getScopes(OriginalSessionEntity $session) {
$result = []; $result = [];
foreach ($this->getSessionModel($session->getId())->getScopes() as $scope) { foreach ($this->getSessionModel($session->getId())->getScopes() as $scope) {
// TODO: нужно проверить все выданные скоупы на их существование if ($this->server->getScopeStorage()->get($scope) !== null) {
$result[] = (new ScopeEntity($this->server))->hydrate(['id' => $scope]); $result[] = (new ScopeEntity($this->server))->hydrate(['id' => $scope]);
} }
}
return $result; return $result;
} }
/**
* @inheritdoc
*/
public function create($ownerType, $ownerId, $clientId, $clientRedirectUri = null) { public function create($ownerType, $ownerId, $clientId, $clientRedirectUri = null) {
$sessionId = OauthSession::find() $sessionId = OauthSession::find()
->select('id') ->select('id')
@@ -116,11 +72,26 @@ class SessionStorage extends AbstractStorage implements SessionInterface {
return $sessionId; return $sessionId;
} }
/**
* @inheritdoc
*/
public function associateScope(OriginalSessionEntity $session, ScopeEntity $scope) { public function associateScope(OriginalSessionEntity $session, ScopeEntity $scope) {
$this->getSessionModel($session->getId())->getScopes()->add($scope->getId()); $this->getSessionModel($session->getId())->getScopes()->add($scope->getId());
} }
private function getSessionModel(string $sessionId) : OauthSession {
$session = OauthSession::findOne($sessionId);
if ($session === null) {
throw new ErrorException('Cannot find oauth session');
}
return $session;
}
private function hydrate(OauthSession $sessionModel) {
$entity = new SessionEntity($this->server);
$entity->setId($sessionModel->id);
$entity->setClientId($sessionModel->client_id);
$entity->setOwner($sessionModel->owner_type, $sessionModel->owner_id);
return $entity;
}
} }

View File

@@ -0,0 +1,16 @@
<?php
namespace api\components\OAuth2\Utils\KeyAlgorithm;
use League\OAuth2\Server\Util\KeyAlgorithm\KeyAlgorithmInterface;
use Ramsey\Uuid\Uuid;
class UuidAlgorithm implements KeyAlgorithmInterface {
/**
* @inheritdoc
*/
public function generate($len = 40) : string {
return Uuid::uuid4()->toString();
}
}

View File

@@ -23,7 +23,7 @@ use yii\web\User as YiiUserComponent;
* @property AccountSession|null $activeSession * @property AccountSession|null $activeSession
* @property AccountIdentity|null $identity * @property AccountIdentity|null $identity
* *
* @method AccountIdentity|null getIdentity($autoRenew = true) * @method AccountIdentity|null loginByAccessToken($token, $type = null)
*/ */
class Component extends YiiUserComponent { class Component extends YiiUserComponent {
@@ -39,6 +39,8 @@ class Component extends YiiUserComponent {
public $sessionTimeout = 'P7D'; public $sessionTimeout = 'P7D';
private $_identity;
public function init() { public function init() {
parent::init(); parent::init();
if (!$this->secret) { if (!$this->secret) {
@@ -46,6 +48,24 @@ class Component extends YiiUserComponent {
} }
} }
/**
* @param bool $autoRenew
* @return null|AccountIdentity
*/
public function getIdentity($autoRenew = true) {
$result = parent::getIdentity($autoRenew);
if ($result === null && $this->_identity !== false) {
$bearer = $this->getBearerToken();
if ($bearer !== null) {
$result = $this->loginByAccessToken($bearer);
}
$this->_identity = $result ?: false;
}
return $result;
}
/** /**
* @param IdentityInterface $identity * @param IdentityInterface $identity
* @param bool $rememberMe * @param bool $rememberMe
@@ -90,7 +110,7 @@ class Component extends YiiUserComponent {
return $result; return $result;
} }
public function renew(AccountSession $session) { public function renew(AccountSession $session): RenewResult {
$account = $session->account; $account = $session->account;
$transaction = Yii::$app->db->beginTransaction(); $transaction = Yii::$app->db->beginTransaction();
try { try {
@@ -149,14 +169,9 @@ class Component extends YiiUserComponent {
return null; return null;
} }
$authHeader = Yii::$app->request->getHeaders()->get('Authorization'); $bearer = $this->getBearerToken();
if ($authHeader === null || !preg_match('/^Bearer\s+(.*?)$/', $authHeader, $matches)) {
return null;
}
$token = $matches[1];
try { try {
$token = $this->parseToken($token); $token = $this->parseToken($bearer);
} catch (VerificationException $e) { } catch (VerificationException $e) {
return null; return null;
} }
@@ -203,4 +218,16 @@ class Component extends YiiUserComponent {
]; ];
} }
/**
* @return ?string
*/
private function getBearerToken() {
$authHeader = Yii::$app->request->getHeaders()->get('Authorization');
if ($authHeader === null || !preg_match('/^Bearer\s+(.*?)$/', $authHeader, $matches)) {
return null;
}
return $matches[1];
}
} }

View File

@@ -1,7 +1,7 @@
<?php <?php
$params = array_merge( $params = array_merge(
require(__DIR__ . '/../../common/config/params.php'), require __DIR__ . '/../../common/config/params.php',
require(__DIR__ . '/params.php') require __DIR__ . '/params.php'
); );
return [ return [
@@ -21,6 +21,17 @@ return [
'log' => [ 'log' => [
'traceLevel' => YII_DEBUG ? 3 : 0, 'traceLevel' => YII_DEBUG ? 3 : 0,
'targets' => [ 'targets' => [
[
'class' => mito\sentry\Target::class,
'levels' => ['error', 'warning'],
'except' => [
'legacy-authserver',
'session',
'yii\web\HttpException:*',
'api\modules\session\exceptions\SessionServerException:*',
'api\modules\authserver\exceptions\AuthserverException:*',
],
],
[ [
'class' => yii\log\FileTarget::class, 'class' => yii\log\FileTarget::class,
'levels' => ['error', 'warning'], 'levels' => ['error', 'warning'],
@@ -63,8 +74,12 @@ return [
'format' => yii\web\Response::FORMAT_JSON, 'format' => yii\web\Response::FORMAT_JSON,
], ],
'oauth' => [ 'oauth' => [
'class' => common\components\oauth\Component::class, 'class' => api\components\OAuth2\Component::class,
'grantTypes' => ['authorization_code'], 'grantTypes' => ['authorization_code'],
'grantMap' => [
'authorization_code' => api\components\OAuth2\Grants\AuthCodeGrant::class,
'refresh_token' => api\components\OAuth2\Grants\RefreshTokenGrant::class,
],
], ],
'errorHandler' => [ 'errorHandler' => [
'class' => api\components\ErrorHandler::class, 'class' => api\components\ErrorHandler::class,

View File

@@ -79,7 +79,7 @@ class AccountsController extends Controller {
if (!$model->changePassword()) { if (!$model->changePassword()) {
return [ return [
'success' => false, 'success' => false,
'errors' => $this->normalizeModelErrors($model->getErrors()), 'errors' => $model->getFirstErrors(),
]; ];
} }
@@ -94,7 +94,7 @@ class AccountsController extends Controller {
if (!$model->change()) { if (!$model->change()) {
return [ return [
'success' => false, 'success' => false,
'errors' => $this->normalizeModelErrors($model->getErrors()), 'errors' => $model->getFirstErrors(),
]; ];
} }
@@ -110,7 +110,7 @@ class AccountsController extends Controller {
if (!$model->sendCurrentEmailConfirmation()) { if (!$model->sendCurrentEmailConfirmation()) {
$data = [ $data = [
'success' => false, 'success' => false,
'errors' => $this->normalizeModelErrors($model->getErrors()), 'errors' => $model->getFirstErrors(),
]; ];
if (ArrayHelper::getValue($data['errors'], 'email') === E::RECENTLY_SENT_MESSAGE) { if (ArrayHelper::getValue($data['errors'], 'email') === E::RECENTLY_SENT_MESSAGE) {
@@ -136,7 +136,7 @@ class AccountsController extends Controller {
if (!$model->sendNewEmailConfirmation()) { if (!$model->sendNewEmailConfirmation()) {
return [ return [
'success' => false, 'success' => false,
'errors' => $this->normalizeModelErrors($model->getErrors()), 'errors' => $model->getFirstErrors(),
]; ];
} }
@@ -152,7 +152,7 @@ class AccountsController extends Controller {
if (!$model->changeEmail()) { if (!$model->changeEmail()) {
return [ return [
'success' => false, 'success' => false,
'errors' => $this->normalizeModelErrors($model->getErrors()), 'errors' => $model->getFirstErrors(),
]; ];
} }
@@ -171,7 +171,7 @@ class AccountsController extends Controller {
if (!$model->applyLanguage()) { if (!$model->applyLanguage()) {
return [ return [
'success' => false, 'success' => false,
'errors' => $this->normalizeModelErrors($model->getErrors()), 'errors' => $model->getFirstErrors(),
]; ];
} }
@@ -187,7 +187,7 @@ class AccountsController extends Controller {
if (!$model->agreeWithLatestRules()) { if (!$model->agreeWithLatestRules()) {
return [ return [
'success' => false, 'success' => false,
'errors' => $this->normalizeModelErrors($model->getErrors()), 'errors' => $model->getFirstErrors(),
]; ];
} }

View File

@@ -17,13 +17,14 @@ class AuthenticationController extends Controller {
public function behaviors() { public function behaviors() {
return ArrayHelper::merge(parent::behaviors(), [ return ArrayHelper::merge(parent::behaviors(), [
'authenticator' => [ 'authenticator' => [
'except' => ['login', 'forgot-password', 'recover-password', 'refresh-token'], 'only' => ['logout'],
], ],
'access' => [ 'access' => [
'class' => AccessControl::class, 'class' => AccessControl::class,
'except' => ['refresh-token'],
'rules' => [ 'rules' => [
[ [
'actions' => ['login', 'forgot-password', 'recover-password', 'refresh-token'], 'actions' => ['login', 'forgot-password', 'recover-password'],
'allow' => true, 'allow' => true,
'roles' => ['?'], 'roles' => ['?'],
], ],
@@ -53,7 +54,7 @@ class AuthenticationController extends Controller {
if (($result = $model->login()) === false) { if (($result = $model->login()) === false) {
$data = [ $data = [
'success' => false, 'success' => false,
'errors' => $this->normalizeModelErrors($model->getErrors()), 'errors' => $model->getFirstErrors(),
]; ];
if (ArrayHelper::getValue($data['errors'], 'login') === E::ACCOUNT_NOT_ACTIVATED) { if (ArrayHelper::getValue($data['errors'], 'login') === E::ACCOUNT_NOT_ACTIVATED) {
@@ -83,7 +84,7 @@ class AuthenticationController extends Controller {
if ($model->forgotPassword() === false) { if ($model->forgotPassword() === false) {
$data = [ $data = [
'success' => false, 'success' => false,
'errors' => $this->normalizeModelErrors($model->getErrors()), 'errors' => $model->getFirstErrors(),
]; ];
if (ArrayHelper::getValue($data['errors'], 'login') === E::RECENTLY_SENT_MESSAGE) { if (ArrayHelper::getValue($data['errors'], 'login') === E::RECENTLY_SENT_MESSAGE) {
@@ -119,7 +120,7 @@ class AuthenticationController extends Controller {
if (($result = $model->recoverPassword()) === false) { if (($result = $model->recoverPassword()) === false) {
return [ return [
'success' => false, 'success' => false,
'errors' => $this->normalizeModelErrors($model->getErrors()), 'errors' => $model->getFirstErrors(),
]; ];
} }
@@ -134,7 +135,7 @@ class AuthenticationController extends Controller {
if (($result = $model->renew()) === false) { if (($result = $model->renew()) === false) {
return [ return [
'success' => false, 'success' => false,
'errors' => $this->normalizeModelErrors($model->getErrors()), 'errors' => $model->getFirstErrors(),
]; ];
} }

View File

@@ -1,7 +1,6 @@
<?php <?php
namespace api\controllers; namespace api\controllers;
use api\traits\ApiNormalize;
use Yii; use Yii;
use yii\filters\auth\HttpBearerAuth; use yii\filters\auth\HttpBearerAuth;
@@ -12,7 +11,6 @@ use yii\filters\auth\HttpBearerAuth;
* @mixin \yii\filters\auth\CompositeAuth * @mixin \yii\filters\auth\CompositeAuth
*/ */
class Controller extends \yii\rest\Controller { class Controller extends \yii\rest\Controller {
use ApiNormalize;
public function behaviors() { public function behaviors() {
$parentBehaviors = parent::behaviors(); $parentBehaviors = parent::behaviors();
@@ -22,10 +20,11 @@ class Controller extends \yii\rest\Controller {
'user' => Yii::$app->getUser(), 'user' => Yii::$app->getUser(),
]; ];
// xml нам не понадобится // xml и rate limiter нам не понадобятся
unset($parentBehaviors['contentNegotiator']['formats']['application/xml']); unset(
// rate limiter здесь не применяется $parentBehaviors['contentNegotiator']['formats']['application/xml'],
unset($parentBehaviors['rateLimiter']); $parentBehaviors['rateLimiter']
);
return $parentBehaviors; return $parentBehaviors;
} }

View File

@@ -27,7 +27,7 @@ class FeedbackController extends Controller {
if (!$model->sendMessage()) { if (!$model->sendMessage()) {
return [ return [
'success' => false, 'success' => false,
'errors' => $this->normalizeModelErrors($model->getErrors()), 'errors' => $model->getFirstErrors(),
]; ];
} }

View File

@@ -2,13 +2,12 @@
namespace api\controllers; namespace api\controllers;
use api\filters\ActiveUserRule; use api\filters\ActiveUserRule;
use common\components\oauth\Exception\AcceptRequiredException; use api\components\OAuth2\Exception\AcceptRequiredException;
use common\components\oauth\Exception\AccessDeniedException; use api\components\OAuth2\Exception\AccessDeniedException;
use common\models\Account; use common\models\Account;
use common\models\OauthClient; use common\models\OauthClient;
use common\models\OauthScope; use common\models\OauthScope;
use League\OAuth2\Server\Exception\OAuthException; use League\OAuth2\Server\Exception\OAuthException;
use League\OAuth2\Server\Grant\RefreshTokenGrant;
use Yii; use Yii;
use yii\filters\AccessControl; use yii\filters\AccessControl;
use yii\helpers\ArrayHelper; use yii\helpers\ArrayHelper;
@@ -18,16 +17,12 @@ class OauthController extends Controller {
public function behaviors() { public function behaviors() {
return ArrayHelper::merge(parent::behaviors(), [ return ArrayHelper::merge(parent::behaviors(), [
'authenticator' => [ 'authenticator' => [
'except' => ['validate', 'token'], 'only' => ['complete'],
], ],
'access' => [ 'access' => [
'class' => AccessControl::class, 'class' => AccessControl::class,
'only' => ['complete'],
'rules' => [ 'rules' => [
[
'actions' => ['validate', 'token'],
'allow' => true,
'roles' => ['?'],
],
[ [
'class' => ActiveUserRule::class, 'class' => ActiveUserRule::class,
'actions' => ['complete'], 'actions' => ['complete'],
@@ -186,7 +181,7 @@ class OauthController extends Controller {
} }
$scopes = $codeModel->getScopes(); $scopes = $codeModel->getScopes();
if (array_search(OauthScope::OFFLINE_ACCESS, array_keys($scopes)) === false) { if (array_search(OauthScope::OFFLINE_ACCESS, array_keys($scopes), true) === false) {
return; return;
} }
} elseif ($grantType === 'refresh_token') { } elseif ($grantType === 'refresh_token') {
@@ -195,7 +190,10 @@ class OauthController extends Controller {
return; return;
} }
$this->getServer()->addGrantType(new RefreshTokenGrant()); $grantClass = Yii::$app->oauth->grantMap['refresh_token'];
$grant = new $grantClass;
$this->getServer()->addGrantType($grant);
} }
/** /**

View File

@@ -1,6 +1,7 @@
<?php <?php
namespace api\controllers; namespace api\controllers;
use api\filters\NginxCache;
use Yii; use Yii;
use yii\helpers\ArrayHelper; use yii\helpers\ArrayHelper;
@@ -11,6 +12,12 @@ class OptionsController extends Controller {
'authenticator' => [ 'authenticator' => [
'except' => ['index'], 'except' => ['index'],
], ],
'nginxCache' => [
'class' => NginxCache::class,
'rules' => [
'index' => 3600, // 1h
],
],
]); ]);
} }

View File

@@ -43,7 +43,7 @@ class SignupController extends Controller {
if (!$model->signup()) { if (!$model->signup()) {
return [ return [
'success' => false, 'success' => false,
'errors' => $this->normalizeModelErrors($model->getErrors()), 'errors' => $model->getFirstErrors(),
]; ];
} }
@@ -58,7 +58,7 @@ class SignupController extends Controller {
if (!$model->sendRepeatMessage()) { if (!$model->sendRepeatMessage()) {
$response = [ $response = [
'success' => false, 'success' => false,
'errors' => $this->normalizeModelErrors($model->getErrors()), 'errors' => $model->getFirstErrors(),
]; ];
if (ArrayHelper::getValue($response['errors'], 'email') === E::RECENTLY_SENT_MESSAGE) { if (ArrayHelper::getValue($response['errors'], 'email') === E::RECENTLY_SENT_MESSAGE) {
@@ -83,7 +83,7 @@ class SignupController extends Controller {
if (!($result = $model->confirm())) { if (!($result = $model->confirm())) {
return [ return [
'success' => false, 'success' => false,
'errors' => $this->normalizeModelErrors($model->getErrors()), 'errors' => $model->getFirstErrors(),
]; ];
} }

View File

@@ -0,0 +1,35 @@
<?php
namespace api\filters;
use Yii;
use yii\base\ActionFilter;
class NginxCache extends ActionFilter {
/**
* @var array|callable массив или callback, содержащий пары роут -> сколько кэшировать.
*
* Период можно задавать 2-умя путями:
* - если значение начинается с префикса @, оно задаёт абсолютное время в unix timestamp,
* до которого ответ может быть закэширован.
* - в ином случае значение интерпретируется как количество секунд, на которое необходимо
* закэшировать ответ
*/
public $rules;
public function afterAction($action, $result) {
$rule = $this->rules[$action->id] ?? null;
if ($rule !== null) {
if (is_callable($rule)) {
$cacheTime = $rule($action);
} else {
$cacheTime = $rule;
}
Yii::$app->response->headers->set('X-Accel-Expires', $cacheTime);
}
return parent::afterAction($action, $result);
}
}

View File

@@ -28,15 +28,19 @@ class ChangeUsernameForm extends ApiForm {
]; ];
} }
public function change() { public function change() : bool {
if (!$this->validate()) { if (!$this->validate()) {
return false; return false;
} }
$transaction = Yii::$app->db->beginTransaction();
$account = $this->getAccount(); $account = $this->getAccount();
$oldNickname = $account->username; if ($this->username === $account->username) {
return true;
}
$transaction = Yii::$app->db->beginTransaction();
try { try {
$oldNickname = $account->username;
$account->username = $this->username; $account->username = $this->username;
if (!$account->save()) { if (!$account->save()) {
throw new ErrorException('Cannot save account model with new username'); throw new ErrorException('Cannot save account model with new username');

View File

@@ -25,6 +25,7 @@ class RateLimiter extends \yii\filters\RateLimiter {
/** /**
* @inheritdoc * @inheritdoc
* @throws TooManyRequestsHttpException
*/ */
public function beforeAction($action) { public function beforeAction($action) {
$this->checkRateLimit( $this->checkRateLimit(
@@ -39,6 +40,7 @@ class RateLimiter extends \yii\filters\RateLimiter {
/** /**
* @inheritdoc * @inheritdoc
* @throws TooManyRequestsHttpException
*/ */
public function checkRateLimit($user, $request, $response, $action) { public function checkRateLimit($user, $request, $response, $action) {
if (parse_url($request->getHostInfo(), PHP_URL_HOST) === $this->authserverDomain) { if (parse_url($request->getHostInfo(), PHP_URL_HOST) === $this->authserverDomain) {
@@ -54,7 +56,7 @@ class RateLimiter extends \yii\filters\RateLimiter {
$key = $this->buildKey($ip); $key = $this->buildKey($ip);
$redis = $this->getRedis(); $redis = $this->getRedis();
$countRequests = intval($redis->executeCommand('INCR', [$key])); $countRequests = (int)$redis->incr($key);
if ($countRequests === 1) { if ($countRequests === 1) {
$redis->executeCommand('EXPIRE', [$key, $this->limitTime]); $redis->executeCommand('EXPIRE', [$key, $this->limitTime]);
} }
@@ -65,7 +67,7 @@ class RateLimiter extends \yii\filters\RateLimiter {
} }
/** /**
* @return \yii\redis\Connection * @return \common\components\Redis\Connection
*/ */
public function getRedis() { public function getRedis() {
return Yii::$app->redis; return Yii::$app->redis;

View File

@@ -128,7 +128,7 @@ class JoinForm extends Model {
$account = $accessModel->account; $account = $accessModel->account;
} }
/** @var MinecraftAccessKey|\common\models\OauthAccessToken $accessModel */ /** @var MinecraftAccessKey|\api\components\OAuth2\Entities\AccessTokenEntity $accessModel */
if ($accessModel->isExpired()) { if ($accessModel->isExpired()) {
Session::error("User with access_token = '{$accessToken}' failed join by expired access_token."); Session::error("User with access_token = '{$accessToken}' failed join by expired access_token.");
throw new ForbiddenOperationException('Expired access_token.'); throw new ForbiddenOperationException('Expired access_token.');

View File

@@ -1,26 +0,0 @@
<?php
namespace api\traits;
trait ApiNormalize {
/**
* Метод убирает все ошибки для поля, кроме первой и возвращает значения в формате
* [
* 'field1' => 'first_error_of_field1',
* 'field2' => 'first_error_of_field2',
* ]
*
* @param array $errors
* @return array
*/
public function normalizeModelErrors(array $errors) {
$normalized = [];
foreach($errors as $attribute => $attrErrors) {
$normalized[$attribute] = $attrErrors[0];
}
return $normalized;
}
}

View File

@@ -17,10 +17,11 @@ class Yii extends \yii\BaseYii {
* Used for properties that are identical for both WebApplication and ConsoleApplication * Used for properties that are identical for both WebApplication and ConsoleApplication
* *
* @property \yii\swiftmailer\Mailer $mailer * @property \yii\swiftmailer\Mailer $mailer
* @property \yii\redis\Connection $redis * @property \common\components\Redis\Connection $redis
* @property \common\components\RabbitMQ\Component $amqp * @property \common\components\RabbitMQ\Component $amqp
* @property \GuzzleHttp\Client $guzzle * @property \GuzzleHttp\Client $guzzle
* @property \common\components\EmailRenderer $emailRenderer * @property \common\components\EmailRenderer $emailRenderer
* @property \mito\sentry\Component $sentry
*/ */
abstract class BaseApplication extends yii\base\Application { abstract class BaseApplication extends yii\base\Application {
} }
@@ -32,7 +33,7 @@ abstract class BaseApplication extends yii\base\Application {
* @property \api\components\User\Component $user User component. * @property \api\components\User\Component $user User component.
* @property \api\components\ApiUser\Component $apiUser Api User component. * @property \api\components\ApiUser\Component $apiUser Api User component.
* @property \api\components\ReCaptcha\Component $reCaptcha * @property \api\components\ReCaptcha\Component $reCaptcha
* @property \common\components\oauth\Component $oauth * @property \api\components\OAuth2\Component $oauth
* *
* @method \api\components\User\Component getUser() * @method \api\components\User\Component getUser()
*/ */

View File

@@ -111,8 +111,8 @@ class Component extends \yii\base\Component {
public function sendToExchange($exchangeName, $routingKey, $message, $exchangeArgs = [], $publishArgs = []) { public function sendToExchange($exchangeName, $routingKey, $message, $exchangeArgs = [], $publishArgs = []) {
$message = $this->prepareMessage($message); $message = $this->prepareMessage($message);
$channel = $this->getChannel(); $channel = $this->getChannel();
call_user_func_array([$channel, 'exchange_declare'], $this->prepareExchangeArgs($exchangeName, $exchangeArgs)); $channel->exchange_declare(...$this->prepareExchangeArgs($exchangeName, $exchangeArgs));
call_user_func_array([$channel, 'basic_publish'], $this->preparePublishArgs($message, $exchangeName, $routingKey, $publishArgs)); $channel->basic_publish(...$this->preparePublishArgs($message, $exchangeName, $routingKey, $publishArgs));
} }
/** /**

View File

@@ -0,0 +1,13 @@
<?php
namespace common\components\Redis;
use yii\di\Instance;
class Cache extends \yii\redis\Cache {
public function init() {
\yii\caching\Cache::init();
$this->redis = Instance::ensure($this->redis, ConnectionInterface::class);
}
}

View File

@@ -0,0 +1,415 @@
<?php
namespace common\components\Redis;
use Predis\Client;
use Predis\ClientInterface;
use yii\base\Component;
/**
* Interface defining a client able to execute commands against Redis.
*
* All the commands exposed by the client generally have the same signature as
* described by the Redis documentation, but some of them offer an additional
* and more friendly interface to ease programming which is described in the
* following list of methods:
*
* @method int del(array $keys)
* @method string dump($key)
* @method int exists($key)
* @method int expire($key, $seconds)
* @method int expireat($key, $timestamp)
* @method array keys($pattern)
* @method int move($key, $db)
* @method mixed object($subcommand, $key)
* @method int persist($key)
* @method int pexpire($key, $milliseconds)
* @method int pexpireat($key, $timestamp)
* @method int pttl($key)
* @method string randomkey()
* @method mixed rename($key, $target)
* @method int renamenx($key, $target)
* @method array scan($cursor, array $options = null)
* @method array sort($key, array $options = null)
* @method int ttl($key)
* @method mixed type($key)
* @method int append($key, $value)
* @method int bitcount($key, $start = null, $end = null)
* @method int bitop($operation, $destkey, $key)
* @method int decr($key)
* @method int decrby($key, $decrement)
* @method string get($key)
* @method int getbit($key, $offset)
* @method string getrange($key, $start, $end)
* @method string getset($key, $value)
* @method int incr($key)
* @method int incrby($key, $increment)
* @method string incrbyfloat($key, $increment)
* @method array mget(array $keys)
* @method mixed mset(array $dictionary)
* @method int msetnx(array $dictionary)
* @method mixed psetex($key, $milliseconds, $value)
* @method mixed set($key, $value, $expireResolution = null, $expireTTL = null, $flag = null)
* @method int setbit($key, $offset, $value)
* @method int setex($key, $seconds, $value)
* @method int setnx($key, $value)
* @method int setrange($key, $offset, $value)
* @method int strlen($key)
* @method int hdel($key, array $fields)
* @method int hexists($key, $field)
* @method string hget($key, $field)
* @method array hgetall($key)
* @method int hincrby($key, $field, $increment)
* @method string hincrbyfloat($key, $field, $increment)
* @method array hkeys($key)
* @method int hlen($key)
* @method array hmget($key, array $fields)
* @method mixed hmset($key, array $dictionary)
* @method array hscan($key, $cursor, array $options = null)
* @method int hset($key, $field, $value)
* @method int hsetnx($key, $field, $value)
* @method array hvals($key)
* @method array blpop(array $keys, $timeout)
* @method array brpop(array $keys, $timeout)
* @method array brpoplpush($source, $destination, $timeout)
* @method string lindex($key, $index)
* @method int linsert($key, $whence, $pivot, $value)
* @method int llen($key)
* @method string lpop($key)
* @method int lpush($key, array $values)
* @method int lpushx($key, $value)
* @method array lrange($key, $start, $stop)
* @method int lrem($key, $count, $value)
* @method mixed lset($key, $index, $value)
* @method mixed ltrim($key, $start, $stop)
* @method string rpop($key)
* @method string rpoplpush($source, $destination)
* @method int rpush($key, array $values)
* @method int rpushx($key, $value)
* @method int sadd($key, array $members)
* @method int scard($key)
* @method array sdiff(array $keys)
* @method int sdiffstore($destination, array $keys)
* @method array sinter(array $keys)
* @method int sinterstore($destination, array $keys)
* @method int sismember($key, $member)
* @method array smembers($key)
* @method int smove($source, $destination, $member)
* @method string spop($key)
* @method string srandmember($key, $count = null)
* @method int srem($key, $member)
* @method array sscan($key, $cursor, array $options = null)
* @method array sunion(array $keys)
* @method int sunionstore($destination, array $keys)
* @method int zadd($key, array $membersAndScoresDictionary)
* @method int zcard($key)
* @method string zcount($key, $min, $max)
* @method string zincrby($key, $increment, $member)
* @method int zinterstore($destination, array $keys, array $options = null)
* @method array zrange($key, $start, $stop, array $options = null)
* @method array zrangebyscore($key, $min, $max, array $options = null)
* @method int zrank($key, $member)
* @method int zrem($key, $member)
* @method int zremrangebyrank($key, $start, $stop)
* @method int zremrangebyscore($key, $min, $max)
* @method array zrevrange($key, $start, $stop, array $options = null)
* @method array zrevrangebyscore($key, $min, $max, array $options = null)
* @method int zrevrank($key, $member)
* @method int zunionstore($destination, array $keys, array $options = null)
* @method string zscore($key, $member)
* @method array zscan($key, $cursor, array $options = null)
* @method array zrangebylex($key, $start, $stop, array $options = null)
* @method int zremrangebylex($key, $min, $max)
* @method int zlexcount($key, $min, $max)
* @method int pfadd($key, array $elements)
* @method mixed pfmerge($destinationKey, array $sourceKeys)
* @method int pfcount(array $keys)
* @method mixed pubsub($subcommand, $argument)
* @method int publish($channel, $message)
* @method mixed discard()
* @method array exec()
* @method mixed multi()
* @method mixed unwatch()
* @method mixed watch($key)
* @method mixed eval($script, $numkeys, $keyOrArg1 = null, $keyOrArgN = null)
* @method mixed evalsha($script, $numkeys, $keyOrArg1 = null, $keyOrArgN = null)
* @method mixed script($subcommand, $argument = null)
* @method mixed auth($password)
* @method string echo($message)
* @method mixed ping($message = null)
* @method mixed select($database)
* @method mixed bgrewriteaof()
* @method mixed bgsave()
* @method mixed client($subcommand, $argument = null)
* @method mixed config($subcommand, $argument = null)
* @method int dbsize()
* @method mixed flushall()
* @method mixed flushdb()
* @method array info($section = null)
* @method int lastsave()
* @method mixed save()
* @method mixed slaveof($host, $port)
* @method mixed slowlog($subcommand, $argument = null)
* @method array time()
* @method array command()
*/
class Connection extends Component implements ConnectionInterface {
/**
* @var array List of available redis commands http://redis.io/commands
*/
const REDIS_COMMANDS = [
'BLPOP', // key [key ...] timeout Remove and get the first element in a list, or block until one is available
'BRPOP', // key [key ...] timeout Remove and get the last element in a list, or block until one is available
'BRPOPLPUSH', // source destination timeout Pop a value from a list, push it to another list and return it; or block until one is available
'CLIENT KILL', // ip:port Kill the connection of a client
'CLIENT LIST', // Get the list of client connections
'CLIENT GETNAME', // Get the current connection name
'CLIENT SETNAME', // connection-name Set the current connection name
'CONFIG GET', // parameter Get the value of a configuration parameter
'CONFIG SET', // parameter value Set a configuration parameter to the given value
'CONFIG RESETSTAT', // Reset the stats returned by INFO
'DBSIZE', // Return the number of keys in the selected database
'DEBUG OBJECT', // key Get debugging information about a key
'DEBUG SEGFAULT', // Make the server crash
'DECR', // key Decrement the integer value of a key by one
'DECRBY', // key decrement Decrement the integer value of a key by the given number
'DEL', // key [key ...] Delete a key
'DISCARD', // Discard all commands issued after MULTI
'DUMP', // key Return a serialized version of the value stored at the specified key.
'ECHO', // message Echo the given string
'EVAL', // script numkeys key [key ...] arg [arg ...] Execute a Lua script server side
'EVALSHA', // sha1 numkeys key [key ...] arg [arg ...] Execute a Lua script server side
'EXEC', // Execute all commands issued after MULTI
'EXISTS', // key Determine if a key exists
'EXPIRE', // key seconds Set a key's time to live in seconds
'EXPIREAT', // key timestamp Set the expiration for a key as a UNIX timestamp
'FLUSHALL', // Remove all keys from all databases
'FLUSHDB', // Remove all keys from the current database
'GET', // key Get the value of a key
'GETBIT', // key offset Returns the bit value at offset in the string value stored at key
'GETRANGE', // key start end Get a substring of the string stored at a key
'GETSET', // key value Set the string value of a key and return its old value
'HDEL', // key field [field ...] Delete one or more hash fields
'HEXISTS', // key field Determine if a hash field exists
'HGET', // key field Get the value of a hash field
'HGETALL', // key Get all the fields and values in a hash
'HINCRBY', // key field increment Increment the integer value of a hash field by the given number
'HINCRBYFLOAT', // key field increment Increment the float value of a hash field by the given amount
'HKEYS', // key Get all the fields in a hash
'HLEN', // key Get the number of fields in a hash
'HMGET', // key field [field ...] Get the values of all the given hash fields
'HMSET', // key field value [field value ...] Set multiple hash fields to multiple values
'HSET', // key field value Set the string value of a hash field
'HSETNX', // key field value Set the value of a hash field, only if the field does not exist
'HVALS', // key Get all the values in a hash
'INCR', // key Increment the integer value of a key by one
'INCRBY', // key increment Increment the integer value of a key by the given amount
'INCRBYFLOAT', // key increment Increment the float value of a key by the given amount
'INFO', // [section] Get information and statistics about the server
'KEYS', // pattern Find all keys matching the given pattern
'LASTSAVE', // Get the UNIX time stamp of the last successful save to disk
'LINDEX', // key index Get an element from a list by its index
'LINSERT', // key BEFORE|AFTER pivot value Insert an element before or after another element in a list
'LLEN', // key Get the length of a list
'LPOP', // key Remove and get the first element in a list
'LPUSH', // key value [value ...] Prepend one or multiple values to a list
'LPUSHX', // key value Prepend a value to a list, only if the list exists
'LRANGE', // key start stop Get a range of elements from a list
'LREM', // key count value Remove elements from a list
'LSET', // key index value Set the value of an element in a list by its index
'LTRIM', // key start stop Trim a list to the specified range
'MGET', // key [key ...] Get the values of all the given keys
'MIGRATE', // host port key destination-db timeout Atomically transfer a key from a Redis instance to another one.
'MONITOR', // Listen for all requests received by the server in real time
'MOVE', // key db Move a key to another database
'MSET', // key value [key value ...] Set multiple keys to multiple values
'MSETNX', // key value [key value ...] Set multiple keys to multiple values, only if none of the keys exist
'MULTI', // Mark the start of a transaction block
'OBJECT', // subcommand [arguments [arguments ...]] Inspect the internals of Redis objects
'PERSIST', // key Remove the expiration from a key
'PEXPIRE', // key milliseconds Set a key's time to live in milliseconds
'PEXPIREAT', // key milliseconds-timestamp Set the expiration for a key as a UNIX timestamp specified in milliseconds
'PING', // Ping the server
'PSETEX', // key milliseconds value Set the value and expiration in milliseconds of a key
'PSUBSCRIBE', // pattern [pattern ...] Listen for messages published to channels matching the given patterns
'PTTL', // key Get the time to live for a key in milliseconds
'PUBLISH', // channel message Post a message to a channel
'PUNSUBSCRIBE', // [pattern [pattern ...]] Stop listening for messages posted to channels matching the given patterns
'QUIT', // Close the connection
'RANDOMKEY', // Return a random key from the keyspace
'RENAME', // key newkey Rename a key
'RENAMENX', // key newkey Rename a key, only if the new key does not exist
'RESTORE', // key ttl serialized-value Create a key using the provided serialized value, previously obtained using DUMP.
'RPOP', // key Remove and get the last element in a list
'RPOPLPUSH', // source destination Remove the last element in a list, append it to another list and return it
'RPUSH', // key value [value ...] Append one or multiple values to a list
'RPUSHX', // key value Append a value to a list, only if the list exists
'SADD', // key member [member ...] Add one or more members to a set
'SAVE', // Synchronously save the dataset to disk
'SCARD', // key Get the number of members in a set
'SCRIPT EXISTS', // script [script ...] Check existence of scripts in the script cache.
'SCRIPT FLUSH', // Remove all the scripts from the script cache.
'SCRIPT KILL', // Kill the script currently in execution.
'SCRIPT LOAD', // script Load the specified Lua script into the script cache.
'SDIFF', // key [key ...] Subtract multiple sets
'SDIFFSTORE', // destination key [key ...] Subtract multiple sets and store the resulting set in a key
'SELECT', // index Change the selected database for the current connection
'SET', // key value Set the string value of a key
'SETBIT', // key offset value Sets or clears the bit at offset in the string value stored at key
'SETEX', // key seconds value Set the value and expiration of a key
'SETNX', // key value Set the value of a key, only if the key does not exist
'SETRANGE', // key offset value Overwrite part of a string at key starting at the specified offset
'SHUTDOWN', // [NOSAVE] [SAVE] Synchronously save the dataset to disk and then shut down the server
'SINTER', // key [key ...] Intersect multiple sets
'SINTERSTORE', // destination key [key ...] Intersect multiple sets and store the resulting set in a key
'SISMEMBER', // key member Determine if a given value is a member of a set
'SLAVEOF', // host port Make the server a slave of another instance, or promote it as master
'SLOWLOG', // subcommand [argument] Manages the Redis slow queries log
'SMEMBERS', // key Get all the members in a set
'SMOVE', // source destination member Move a member from one set to another
'SORT', // key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination] Sort the elements in a list, set or sorted set
'SPOP', // key Remove and return a random member from a set
'SRANDMEMBER', // key [count] Get one or multiple random members from a set
'SREM', // key member [member ...] Remove one or more members from a set
'STRLEN', // key Get the length of the value stored in a key
'SUBSCRIBE', // channel [channel ...] Listen for messages published to the given channels
'SUNION', // key [key ...] Add multiple sets
'SUNIONSTORE', // destination key [key ...] Add multiple sets and store the resulting set in a key
'SYNC', // Internal command used for replication
'TIME', // Return the current server time
'TTL', // key Get the time to live for a key
'TYPE', // key Determine the type stored at key
'UNSUBSCRIBE', // [channel [channel ...]] Stop listening for messages posted to the given channels
'UNWATCH', // Forget about all watched keys
'WATCH', // key [key ...] Watch the given keys to determine execution of the MULTI/EXEC block
'ZADD', // key score member [score member ...] Add one or more members to a sorted set, or update its score if it already exists
'ZCARD', // key Get the number of members in a sorted set
'ZCOUNT', // key min max Count the members in a sorted set with scores within the given values
'ZINCRBY', // key increment member Increment the score of a member in a sorted set
'ZINTERSTORE', // destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] Intersect multiple sorted sets and store the resulting sorted set in a new key
'ZRANGE', // key start stop [WITHSCORES] Return a range of members in a sorted set, by index
'ZRANGEBYSCORE', // key min max [WITHSCORES] [LIMIT offset count] Return a range of members in a sorted set, by score
'ZRANK', // key member Determine the index of a member in a sorted set
'ZREM', // key member [member ...] Remove one or more members from a sorted set
'ZREMRANGEBYRANK', // key start stop Remove all members in a sorted set within the given indexes
'ZREMRANGEBYSCORE', // key min max Remove all members in a sorted set within the given scores
'ZREVRANGE', // key start stop [WITHSCORES] Return a range of members in a sorted set, by index, with scores ordered from high to low
'ZREVRANGEBYSCORE', // key max min [WITHSCORES] [LIMIT offset count] Return a range of members in a sorted set, by score, with scores ordered from high to low
'ZREVRANK', // key member Determine the index of a member in a sorted set, with scores ordered from high to low
'ZSCORE', // key member Get the score associated with the given member in a sorted set
'ZUNIONSTORE', // destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] Add multiple sorted sets and store the resulting sorted set in a new key
'GEOADD', // key longitude latitude member [longitude latitude member ...] Add point
'GEODIST', // key member1 member2 [unit] Return the distance between two members
'GEOHASH', // key member [member ...] Return valid Geohash strings
'GEOPOS', // key member [member ...] Return the positions (longitude,latitude)
'GEORADIUS', // key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] Return the members
'GEORADIUSBYMEMBER', // key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count]
];
/**
* @var string the hostname or ip address to use for connecting to the redis server. Defaults to 'localhost'.
* If [[unixSocket]] is specified, hostname and port will be ignored.
*/
public $hostname = 'localhost';
/**
* @var integer the port to use for connecting to the redis server. Default port is 6379.
* If [[unixSocket]] is specified, hostname and port will be ignored.
*/
public $port = 6379;
/**
* @var string the unix socket path (e.g. `/var/run/redis/redis.sock`) to use for connecting to the redis server.
* This can be used instead of [[hostname]] and [[port]] to connect to the server using a unix socket.
* If a unix socket path is specified, [[hostname]] and [[port]] will be ignored.
*/
public $unixSocket;
/**
* @var string the password for establishing DB connection. Defaults to null meaning no AUTH command is send.
* See http://redis.io/commands/auth
*/
public $password;
/**
* @var integer the redis database to use. This is an integer value starting from 0. Defaults to 0.
*/
public $database = 0;
/**
* @var float timeout to use for connection to redis. If not set the timeout set in php.ini will be used: ini_get("default_socket_timeout")
*/
public $connectionTimeout;
/**
* @var float timeout to use for redis socket when reading and writing data. If not set the php default value will be used.
*/
public $dataTimeout;
/**
* @var integer Bitmask field which may be set to any combination of connection flags passed to [stream_socket_client()](http://php.net/manual/en/function.stream-socket-client.php).
* Currently the select of connection flags is limited to `STREAM_CLIENT_CONNECT` (default), `STREAM_CLIENT_ASYNC_CONNECT` and `STREAM_CLIENT_PERSISTENT`.
* @see http://php.net/manual/en/function.stream-socket-client.php
*/
public $socketClientFlags = STREAM_CLIENT_CONNECT;
/**
* @var array|null
*/
public $parameters;
/**
* @var array
*/
public $options = [];
/**
* @var ClientInterface
*/
private $_client;
public function getConnection() : ClientInterface {
if ($this->_client === null) {
$this->_client = new Client($this->prepareParams(), $this->options);
}
return $this->_client;
}
public function __call($name, $params) {
$redisCommand = mb_strtoupper($name);
if (in_array($redisCommand, self::REDIS_COMMANDS)) {
return $this->executeCommand($name, $params);
}
return parent::__call($name, $params);
}
public function executeCommand(string $name, array $params = []) {
return $this->getConnection()->$name(...$params);
}
private function prepareParams() {
if ($this->parameters !== null) {
return $this->parameters;
}
if ($this->unixSocket) {
$parameters = [
'scheme' => 'unix',
'path' => $this->unixSocket,
];
} else {
$parameters = [
'scheme' => 'tcp',
'host' => $this->hostname,
'port' => $this->port,
];
}
return array_merge($parameters, [
'database' => $this->database,
]);
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace common\components\Redis;
interface ConnectionInterface {
/**
* @return ConnectionInterface
*/
public function getConnection();
/**
* @param string $name Command, that should be executed
* @param array $params Arguments for this command
*
* @return mixed
*/
public function executeCommand(string $name, array $params = []);
}

View File

@@ -1,5 +1,5 @@
<?php <?php
namespace common\components\redis; namespace common\components\Redis;
use InvalidArgumentException; use InvalidArgumentException;
use Yii; use Yii;
@@ -9,35 +9,52 @@ class Key {
protected $key; protected $key;
/** /**
* @return \yii\redis\Connection * @return Connection
*/ */
public function getRedis() { public function getRedis() {
return Yii::$app->redis; return Yii::$app->redis;
} }
public function getKey() { public function getKey() : string {
return $this->key; return $this->key;
} }
public function getValue() { public function getValue() {
return json_decode($this->getRedis()->get($this->key), true); return $this->getRedis()->get($this->key);
} }
public function setValue($value) { public function setValue($value) {
$this->getRedis()->set($this->key, json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); $this->getRedis()->set($this->key, $value);
return $this; return $this;
} }
public function delete() { public function delete() {
$this->getRedis()->executeCommand('DEL', [$this->key]); $this->getRedis()->del($this->key);
return $this; return $this;
} }
public function expire($ttl) { public function exists() : bool {
$this->getRedis()->executeCommand('EXPIRE', [$this->key, $ttl]); return (bool)$this->getRedis()->exists($this->key);
}
public function expire(int $ttl) {
$this->getRedis()->expire($this->key, $ttl);
return $this; return $this;
} }
public function expireAt(int $unixTimestamp) {
$this->getRedis()->expireat($this->key, $unixTimestamp);
return $this;
}
public function __construct(...$key) {
if (empty($key)) {
throw new InvalidArgumentException('You must specify at least one key.');
}
$this->key = $this->buildKey($key);
}
private function buildKey(array $parts) { private function buildKey(array $parts) {
$keyParts = []; $keyParts = [];
foreach($parts as $part) { foreach($parts as $part) {
@@ -47,12 +64,4 @@ class Key {
return implode(':', $keyParts); return implode(':', $keyParts);
} }
public function __construct(...$key) {
if (empty($key)) {
throw new InvalidArgumentException('You must specify at least one key.');
}
$this->key = $this->buildKey($key);
}
} }

View File

@@ -0,0 +1,46 @@
<?php
namespace common\components\Redis;
use ArrayIterator;
use IteratorAggregate;
class Set extends Key implements IteratorAggregate {
public function add($value) {
$this->getRedis()->sadd($this->key, $value);
return $this;
}
public function remove($value) {
$this->getRedis()->srem($this->key, $value);
return $this;
}
public function members() {
return $this->getRedis()->smembers($this->key);
}
public function getValue() {
return $this->members();
}
public function exists(string $value = null) : bool {
if ($value === null) {
return parent::exists();
} else {
return (bool)$this->getRedis()->sismember($this->key, $value);
}
}
public function diff(array $sets) {
return $this->getRedis()->sdiff([$this->key, implode(' ', $sets)]);
}
/**
* @inheritdoc
*/
public function getIterator() {
return new ArrayIterator($this->members());
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace common\components\Sentry;
use Yii;
class Component extends \mito\sentry\Component {
public $jsNotifier = false;
public function init() {
if (is_array($this->client) && !isset($this->client['release'])) {
$this->client['release'] = Yii::$app->version;
}
parent::init();
}
}

View File

@@ -1,27 +0,0 @@
<?php
namespace common\components\oauth\Entity;
use League\OAuth2\Server\Entity\EntityTrait;
use League\OAuth2\Server\Entity\SessionEntity;
class AuthCodeEntity extends \League\OAuth2\Server\Entity\AuthCodeEntity {
use EntityTrait;
protected $sessionId;
public function getSessionId() {
return $this->sessionId;
}
/**
* @inheritdoc
* @return static
*/
public function setSession(SessionEntity $session) {
parent::setSession($session);
$this->sessionId = $session->getId();
return $this;
}
}

View File

@@ -1,84 +0,0 @@
<?php
namespace common\components\oauth\Storage\Redis;
use common\components\oauth\Entity\AuthCodeEntity;
use common\components\redis\Key;
use common\components\redis\Set;
use League\OAuth2\Server\Entity\AuthCodeEntity as OriginalAuthCodeEntity;
use League\OAuth2\Server\Entity\ScopeEntity;
use League\OAuth2\Server\Storage\AbstractStorage;
use League\OAuth2\Server\Storage\AuthCodeInterface;
class AuthCodeStorage extends AbstractStorage implements AuthCodeInterface {
public $dataTable = 'oauth_auth_codes';
public $ttl = 3600; // 1h
/**
* @inheritdoc
*/
public function get($code) {
$result = (new Key($this->dataTable, $code))->getValue();
if (!$result) {
return null;
}
if ($result['expire_time'] < time()) {
return null;
}
return (new AuthCodeEntity($this->server))->hydrate([
'id' => $result['id'],
'redirectUri' => $result['client_redirect_uri'],
'expireTime' => $result['expire_time'],
'sessionId' => $result['session_id'],
]);
}
/**
* @inheritdoc
*/
public function create($token, $expireTime, $sessionId, $redirectUri) {
$payload = [
'id' => $token,
'expire_time' => $expireTime,
'session_id' => $sessionId,
'client_redirect_uri' => $redirectUri,
];
(new Key($this->dataTable, $token))->setValue($payload)->expire($this->ttl);
}
/**
* @inheritdoc
*/
public function getScopes(OriginalAuthCodeEntity $token) {
$result = new Set($this->dataTable, $token->getId(), 'scopes');
$response = [];
foreach ($result as $scope) {
// TODO: нужно проверить все выданные скоупы на их существование
$response[] = (new ScopeEntity($this->server))->hydrate(['id' => $scope]);
}
return $response;
}
/**
* @inheritdoc
*/
public function associateScope(OriginalAuthCodeEntity $token, ScopeEntity $scope) {
(new Set($this->dataTable, $token->getId(), 'scopes'))->add($scope->getId())->expire($this->ttl);
}
/**
* @inheritdoc
*/
public function delete(OriginalAuthCodeEntity $token) {
// Удаляем ключ
(new Set($this->dataTable, $token->getId()))->delete();
// Удаляем список скоупов для ключа
(new Set($this->dataTable, $token->getId(), 'scopes'))->delete();
}
}

View File

@@ -1,48 +0,0 @@
<?php
namespace common\components\oauth\Storage\Redis;
use common\components\redis\Key;
use League\OAuth2\Server\Entity\RefreshTokenEntity;
use League\OAuth2\Server\Storage\AbstractStorage;
use League\OAuth2\Server\Storage\RefreshTokenInterface;
class RefreshTokenStorage extends AbstractStorage implements RefreshTokenInterface {
public $dataTable = 'oauth_refresh_tokens';
/**
* @inheritdoc
*/
public function get($token) {
$result = (new Key($this->dataTable, $token))->getValue();
if (!$result) {
return null;
}
return (new RefreshTokenEntity($this->server))
->setId($result['id'])
->setExpireTime($result['expire_time'])
->setAccessTokenId($result['access_token_id']);
}
/**
* @inheritdoc
*/
public function create($token, $expireTime, $accessToken) {
$payload = [
'id' => $token,
'expire_time' => $expireTime,
'access_token_id' => $accessToken,
];
(new Key($this->dataTable, $token))->setValue($payload);
}
/**
* @inheritdoc
*/
public function delete(RefreshTokenEntity $token) {
(new Key($this->dataTable, $token->getId()))->delete();
}
}

View File

@@ -1,84 +0,0 @@
<?php
namespace common\components\oauth\Storage\Yii2;
use common\components\oauth\Entity\AccessTokenEntity;
use common\models\OauthAccessToken;
use League\OAuth2\Server\Entity\AccessTokenEntity as OriginalAccessTokenEntity;
use League\OAuth2\Server\Entity\ScopeEntity;
use League\OAuth2\Server\Storage\AbstractStorage;
use League\OAuth2\Server\Storage\AccessTokenInterface;
use yii\db\Exception;
class AccessTokenStorage extends AbstractStorage implements AccessTokenInterface {
private $cache = [];
/**
* @param string $token
* @return OauthAccessToken|null
*/
private function getTokenModel($token) {
if (!isset($this->cache[$token])) {
$this->cache[$token] = OauthAccessToken::findOne($token);
}
return $this->cache[$token];
}
/**
* @inheritdoc
*/
public function get($token) {
$model = $this->getTokenModel($token);
if ($model === null) {
return null;
}
return (new AccessTokenEntity($this->server))->hydrate([
'id' => $model->access_token,
'expireTime' => $model->expire_time,
'sessionId' => $model->session_id,
]);
}
/**
* @inheritdoc
*/
public function getScopes(OriginalAccessTokenEntity $token) {
$entities = [];
foreach($this->getTokenModel($token->getId())->getScopes() as $scope) {
$entities[] = (new ScopeEntity($this->server))->hydrate(['id' => $scope]);
}
return $entities;
}
/**
* @inheritdoc
*/
public function create($token, $expireTime, $sessionId) {
$model = new OauthAccessToken();
$model->access_token = $token;
$model->expire_time = $expireTime;
$model->session_id = $sessionId;
if (!$model->save()) {
throw new Exception('Cannot save ' . OauthAccessToken::class . ' model.');
}
}
/**
* @inheritdoc
*/
public function associateScope(OriginalAccessTokenEntity $token, ScopeEntity $scope) {
$this->getTokenModel($token->getId())->getScopes()->add($scope->getId());
}
/**
* @inheritdoc
*/
public function delete(OriginalAccessTokenEntity $token) {
$this->getTokenModel($token->getId())->delete();
}
}

View File

@@ -1,16 +0,0 @@
<?php
namespace common\components\oauth\Util\KeyAlgorithm;
use League\OAuth2\Server\Util\KeyAlgorithm\DefaultAlgorithm;
use Ramsey\Uuid\Uuid;
class UuidAlgorithm extends DefaultAlgorithm {
/**
* @inheritdoc
*/
public function generate($len = 40) : string {
return Uuid::uuid4()->toString();
}
}

View File

@@ -1,49 +0,0 @@
<?php
namespace common\components\redis;
use IteratorAggregate;
use Yii;
class Set extends Key implements IteratorAggregate {
/**
* @return \yii\redis\Connection
*/
public static function getDb() {
return Yii::$app->redis;
}
public function add($value) {
$this->getDb()->executeCommand('SADD', [$this->key, $value]);
return $this;
}
public function remove($value) {
$this->getDb()->executeCommand('SREM', [$this->key, $value]);
return $this;
}
public function members() {
return $this->getDb()->executeCommand('SMEMBERS', [$this->key]);
}
public function getValue() {
return $this->members();
}
public function exists($value) {
return !!$this->getDb()->executeCommand('SISMEMBER', [$this->key, $value]);
}
public function diff(array $sets) {
return $this->getDb()->executeCommand('SDIFF', [$this->key, implode(' ', $sets)]);
}
/**
* @inheritdoc
*/
public function getIterator() {
return new \ArrayIterator($this->members());
}
}

View File

@@ -6,22 +6,5 @@ return [
'schemaCacheDuration' => 3600, 'schemaCacheDuration' => 3600,
'schemaCache' => 'cache', 'schemaCache' => 'cache',
], ],
'mailer' => [
'useFileTransport' => false,
'transport' => [
'class' => Swift_SmtpTransport::class,
'host' => 'ely.by',
'username' => getenv('SMTP_USER'),
'password' => getenv('SMTP_PASS'),
'port' => 587,
'encryption' => 'tls',
'streamOptions' => [
'ssl' => [
'allow_self_signed' => true,
'verify_peer' => false,
],
],
],
],
], ],
]; ];

View File

@@ -1,16 +1,17 @@
<?php <?php
return [ return [
'version' => '1.1.3',
'vendorPath' => dirname(dirname(__DIR__)) . '/vendor', 'vendorPath' => dirname(dirname(__DIR__)) . '/vendor',
'components' => [ 'components' => [
'cache' => [ 'cache' => [
'class' => yii\redis\Cache::class, 'class' => common\components\Redis\Cache::class,
'redis' => 'redis', 'redis' => 'redis',
], ],
'db' => [ 'db' => [
'class' => yii\db\Connection::class, 'class' => yii\db\Connection::class,
'dsn' => 'mysql:host=db;dbname=' . getenv('MYSQL_DATABASE'), 'dsn' => 'mysql:host=' . (getenv('DB_HOST') ?: 'db') . ';dbname=' . getenv('DB_DATABASE'),
'username' => getenv('MYSQL_USER'), 'username' => getenv('DB_USER'),
'password' => getenv('MYSQL_PASSWORD'), 'password' => getenv('DB_PASSWORD'),
'charset' => 'utf8', 'charset' => 'utf8',
'schemaMap' => [ 'schemaMap' => [
'mysql' => common\db\mysql\Schema::class, 'mysql' => common\db\mysql\Schema::class,
@@ -19,24 +20,47 @@ return [
'mailer' => [ 'mailer' => [
'class' => yii\swiftmailer\Mailer::class, 'class' => yii\swiftmailer\Mailer::class,
'viewPath' => '@common/mail', 'viewPath' => '@common/mail',
'transport' => [
'class' => Swift_SmtpTransport::class,
'host' => 'ely.by',
'username' => getenv('SMTP_USER'),
'password' => getenv('SMTP_PASS'),
'port' => getenv('SMTP_PORT') ?: 587,
'encryption' => 'tls',
'streamOptions' => [
'ssl' => [
'allow_self_signed' => true,
'verify_peer' => false,
],
],
],
],
'sentry' => [
'class' => common\components\Sentry\Component::class,
'enabled' => !empty(getenv('SENTRY_DSN')),
'dsn' => getenv('SENTRY_DSN'),
'environment' => YII_ENV_DEV ? 'development' : 'production',
'client' => [
'curl_method' => 'async',
],
], ],
'security' => [ 'security' => [
'passwordHashStrategy' => 'password_hash', 'passwordHashStrategy' => 'password_hash',
], ],
'redis' => [ 'redis' => [
'class' => yii\redis\Connection::class, 'class' => common\components\Redis\Connection::class,
'hostname' => 'redis', 'hostname' => getenv('REDIS_HOST') ?: 'redis',
'password' => null, 'password' => getenv('REDIS_PASS') ?: null,
'port' => 6379, 'port' => getenv('REDIS_PORT') ?: 6379,
'database' => 0, 'database' => getenv('REDIS_DATABASE') ?: 0,
], ],
'amqp' => [ 'amqp' => [
'class' => common\components\RabbitMQ\Component::class, 'class' => common\components\RabbitMQ\Component::class,
'host' => 'rabbitmq', 'host' => getenv('RABBITMQ_HOST') ?: 'rabbitmq',
'port' => 5672, 'port' => getenv('RABBITMQ_PORT') ?: 5672,
'user' => getenv('RABBITMQ_DEFAULT_USER'), 'user' => getenv('RABBITMQ_USER'),
'password' => getenv('RABBITMQ_DEFAULT_PASS'), 'password' => getenv('RABBITMQ_PASS'),
'vhost' => getenv('RABBITMQ_DEFAULT_VHOST'), 'vhost' => getenv('RABBITMQ_VHOST'),
], ],
'guzzle' => [ 'guzzle' => [
'class' => GuzzleHttp\Client::class, 'class' => GuzzleHttp\Client::class,

View File

@@ -1,7 +1,7 @@
<?php <?php
namespace common\models; namespace common\models;
use common\components\redis\Set; use common\components\Redis\Set;
use yii\db\ActiveRecord; use yii\db\ActiveRecord;
/** /**
@@ -15,6 +15,7 @@ use yii\db\ActiveRecord;
* *
* Отношения: * Отношения:
* @property OauthSession $session * @property OauthSession $session
* @deprecated
*/ */
class OauthAccessToken extends ActiveRecord { class OauthAccessToken extends ActiveRecord {

View File

@@ -1,21 +1,20 @@
<?php <?php
namespace common\models; namespace common\models;
use yii\db\ActiveRecord; class OauthScope {
/**
* Поля:
* @property string $id
*/
class OauthScope extends ActiveRecord {
const OFFLINE_ACCESS = 'offline_access'; const OFFLINE_ACCESS = 'offline_access';
const MINECRAFT_SERVER_SESSION = 'minecraft_server_session'; const MINECRAFT_SERVER_SESSION = 'minecraft_server_session';
const ACCOUNT_INFO = 'account_info'; const ACCOUNT_INFO = 'account_info';
const ACCOUNT_EMAIL = 'account_email'; const ACCOUNT_EMAIL = 'account_email';
public static function tableName() { public static function getScopes() : array {
return '{{%oauth_scopes}}'; return [
self::OFFLINE_ACCESS,
self::MINECRAFT_SERVER_SESSION,
self::ACCOUNT_INFO,
self::ACCOUNT_EMAIL,
];
} }
} }

View File

@@ -1,7 +1,9 @@
<?php <?php
namespace common\models; namespace common\models;
use common\components\redis\Set; use common\components\Redis\Set;
use Yii;
use yii\base\ErrorException;
use yii\db\ActiveRecord; use yii\db\ActiveRecord;
/** /**
@@ -13,7 +15,6 @@ use yii\db\ActiveRecord;
* @property string $client_redirect_uri * @property string $client_redirect_uri
* *
* Отношения * Отношения
* @property OauthAccessToken[] $accessTokens
* @property OauthClient $client * @property OauthClient $client
* @property Account $account * @property Account $account
* @property Set $scopes * @property Set $scopes
@@ -25,7 +26,7 @@ class OauthSession extends ActiveRecord {
} }
public function getAccessTokens() { public function getAccessTokens() {
return $this->hasMany(OauthAccessToken::class, ['session_id' => 'id']); throw new ErrorException('This method is possible, but not implemented');
} }
public function getClient() { public function getClient() {
@@ -46,6 +47,14 @@ class OauthSession extends ActiveRecord {
} }
$this->getScopes()->delete(); $this->getScopes()->delete();
/** @var \api\components\OAuth2\Storage\RefreshTokenStorage $refreshTokensStorage */
$refreshTokensStorage = Yii::$app->oauth->getAuthServer()->getRefreshTokenStorage();
$refreshTokensSet = $refreshTokensStorage->sessionHash($this->id);
foreach ($refreshTokensSet->members() as $refreshTokenId) {
$refreshTokensStorage->delete($refreshTokensStorage->get($refreshTokenId));
}
$refreshTokensSet->delete();
return true; return true;
} }

View File

@@ -15,17 +15,19 @@
"minimum-stability": "stable", "minimum-stability": "stable",
"require": { "require": {
"php": "^7.0.6", "php": "^7.0.6",
"yiisoft/yii2": "2.0.9", "yiisoft/yii2": "2.0.10",
"yiisoft/yii2-swiftmailer": "*", "yiisoft/yii2-swiftmailer": "*",
"ramsey/uuid": "^3.5.0", "ramsey/uuid": "^3.5.0",
"league/oauth2-server": "~4.1.5", "league/oauth2-server": "dev-improvements#b9277ccd664dcb80a766b73674d21de686cb9dda",
"yiisoft/yii2-redis": "~2.0.0", "yiisoft/yii2-redis": "~2.0.0",
"guzzlehttp/guzzle": "^6.0.0", "guzzlehttp/guzzle": "^6.0.0",
"php-amqplib/php-amqplib": "^2.6.2", "php-amqplib/php-amqplib": "^2.6.2",
"ely/yii2-tempmail-validator": "~1.0.0", "ely/yii2-tempmail-validator": "~1.0.0",
"emarref/jwt": "~1.0.3", "emarref/jwt": "~1.0.3",
"ely/amqp-controller": "dev-master#d7f8cdbc66c45e477c9c7d5d509bc0c1b11fd3ec", "ely/amqp-controller": "dev-master#d7f8cdbc66c45e477c9c7d5d509bc0c1b11fd3ec",
"ely/email-renderer": "dev-master#38a148cd5081147acc31125ddc49966b149f65cf" "ely/email-renderer": "dev-master#38a148cd5081147acc31125ddc49966b149f65cf",
"predis/predis": "^1.0",
"mito/yii2-sentry": "dev-fix_init#27f00805cb906f73b2c6f8181c1c655decb9be70"
}, },
"require-dev": { "require-dev": {
"yiisoft/yii2-codeception": "*", "yiisoft/yii2-codeception": "*",
@@ -35,8 +37,7 @@
"codeception/codeception": "~2.2.4", "codeception/codeception": "~2.2.4",
"codeception/specify": "*", "codeception/specify": "*",
"codeception/verify": "*", "codeception/verify": "*",
"phploc/phploc": "^3.0.1", "phploc/phploc": "^3.0.1"
"predis/predis": "^1.0"
}, },
"config": { "config": {
"process-timeout": 1800 "process-timeout": 1800
@@ -53,6 +54,14 @@
{ {
"type": "git", "type": "git",
"url": "git@gitlab.com:elyby/email-renderer.git" "url": "git@gitlab.com:elyby/email-renderer.git"
},
{
"type": "git",
"url": "git@gitlab.ely.by:elyby/oauth2-server.git"
},
{
"type": "git",
"url": "git@github.com:erickskrauch/yii2-sentry.git"
} }
], ],
"scripts": { "scripts": {

View File

@@ -13,6 +13,10 @@ return [
'components' => [ 'components' => [
'log' => [ 'log' => [
'targets' => [ 'targets' => [
[
'class' => mito\sentry\Target::class,
'levels' => ['error', 'warning'],
],
[ [
'class' => yii\log\FileTarget::class, 'class' => yii\log\FileTarget::class,
'levels' => ['error', 'warning'], 'levels' => ['error', 'warning'],

View File

@@ -0,0 +1,22 @@
<?php
namespace console\controllers;
use common\models\OauthAccessToken;
use yii\console\Controller;
class CleanupController extends Controller {
public function actionAccessTokens() {
$accessTokens = OauthAccessToken::find()
->andWhere(['<', 'expire_time', time()])
->each(1000);
foreach($accessTokens as $token) {
/** @var OauthAccessToken $token */
$token->delete();
}
return self::EXIT_CODE_NORMAL;
}
}

View File

@@ -0,0 +1,25 @@
<?php
use console\db\Migration;
class m161127_145211_remove_oauth_scopes extends Migration {
public function safeUp() {
$this->dropTable('{{%oauth_scopes}}');
}
public function safeDown() {
$this->createTable('{{%oauth_scopes}}', [
'id' => $this->string(64),
$this->primary('id'),
]);
$this->batchInsert('{{%oauth_scopes}}', ['id'], [
['offline_access'],
['minecraft_server_session'],
['account_info'],
['account_email'],
]);
}
}

View File

@@ -13,7 +13,7 @@ services:
env_file: .env env_file: .env
web: web:
build: ./docker/nginx image: registry.ely.by/elyby/accounts-nginx:latest
volumes_from: volumes_from:
- app - app
links: links:

View File

@@ -9,7 +9,7 @@ services:
env_file: .env env_file: .env
web: web:
build: ./docker/nginx image: registry.ely.by/elyby/accounts-nginx:1.0.2
volumes_from: volumes_from:
- app - app
links: links:

0
docker/cron/.gitkeep Normal file
View File

View File

@@ -1,11 +0,0 @@
FROM nginx:1.11-alpine
COPY nginx.conf /etc/nginx/nginx.conf
COPY account.ely.by.conf.template /etc/nginx/conf.d/account.ely.by.conf.template
COPY run.sh /run.sh
RUN rm /etc/nginx/conf.d/default.conf \
&& chmod a+x /run.sh
ENTRYPOINT ["/run.sh"]
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -1,78 +0,0 @@
server {
listen 80;
root $root_path;
charset utf-8;
index index.html;
etag on;
# Это можно раскоментить для целей отладки
# rewrite_log on;
# error_log /var/log/nginx/error.log debug;
set $root_path '/var/www/html';
set $frontend_path '${root_path}/frontend/dist';
set $request_url $request_uri;
set $host_with_uri '${host}${request_uri}';
if ($host_with_uri ~ '^${AUTHSERVER_HOST}/auth') {
set $request_url '/api/authserver${request_uri}';
rewrite ^/auth /api/authserver$uri last;
}
if ($host_with_uri ~ '^${AUTHSERVER_HOST}/session') {
set $request_url '/api/minecraft${request_uri}';
rewrite ^/session /api/minecraft$uri last;
}
if ($host_with_uri ~ '^${AUTHSERVER_HOST}/api/(user|profiles)') {
set $request_url '/api/mojang${request_uri}';
rewrite ^/api/(user|profiles) /api/mojang$uri last;
}
location / {
alias $frontend_path;
try_files $uri /index.html =404;
}
location /api {
try_files $uri $uri /api/web/index.php$is_args$args;
}
location ~* \.php$ {
fastcgi_pass php:9000;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param SERVER_NAME $host;
fastcgi_param REQUEST_URI $request_url;
fastcgi_param REMOTE_ADDR $http_x_real_ip;
try_files $uri =404;
}
# html файлы идут отдельно, для них будет применяться E-Tag кэширование
location ~* \.html$ {
root $frontend_path;
access_log off;
}
# Раздача статики для frontend с указанием max-кэша. Сброс будет по #hash после ребилда webpackом
location ~* ^.+\.(jpg|jpeg|gif|png|svg|js|json|css|zip|rar|eot|ttf|woff|woff2|ico)$ {
root $frontend_path;
expires max;
etag off;
access_log off;
}
# Запросы к статике для email, их нужно запустить внутрь vendor
location ^~ /images/emails/assets {
rewrite ^/images/emails/assets/(.+)$ /vendor/ely/emails-renderer/dist/assets/$1 last;
}
location ^~ /vendor/ely/emails-renderer/dist/assets {
alias '${root_path}/vendor/ely/email-renderer/dist/assets';
try_files $uri =404;
}
}

View File

@@ -1,25 +0,0 @@
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 10;
include /etc/nginx/conf.d/*.conf;
}

View File

@@ -1,5 +0,0 @@
#!/usr/bin/env sh
envsubst '$AUTHSERVER_HOST' < /etc/nginx/conf.d/account.ely.by.conf.template > /etc/nginx/conf.d/default.conf
exec "$@"

View File

@@ -1,8 +0,0 @@
#!/bin/bash
if [ -n "$API_TOKEN" ]
then
php /usr/local/bin/composer.phar config -g github-oauth.github.com $API_TOKEN
fi
exec php /usr/local/bin/composer.phar "$@"

View File

@@ -1,38 +0,0 @@
#!/bin/bash
cd /var/www/html
if [ "$1" = "bash" ] || [ "$1" = "composer" ]
then
exec "$@"
exit 0
fi
# Переносим vendor, если его нету или он изменился (или затёрся силами volume)
if ! cmp -s ./../vendor/autoload.php ./vendor/autoload.php
then
echo "vendor have diffs..."
echo "removing exists vendor"
rm -rf ./vendor
echo "copying new one"
cp -r ./../vendor ./vendor
fi
# Переносим dist, если его нету или он изменился (или затёрся силами volume)
if ! cmp -s ./../dist/index.html ./frontend/dist/index.html
then
echo "frontend dist have diffs..."
echo "removing exists dist"
rm -rf ./frontend/dist
echo "copying new one"
cp -r ./../dist ./frontend/dist
fi
if [ "$YII_ENV" != "test" ]
then
wait-for-it db:3306 -s -- "php /var/www/html/yii migrate/up --interactive=0"
else
wait-for-it testdb:3306 -s -- "php /var/www/html/tests/codeception/bin/yii migrate/up --interactive=0"
fi
exec "$@"

View File

@@ -1,2 +0,0 @@
error_reporting = E_ALL;
display_errors = On;

View File

@@ -1,36 +0,0 @@
[supervisord]
logfile=/tmp/supervisord.log ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=50MB ; (max main logfile bytes b4 rotation;default 50MB)
logfile_backups=10 ; (num of main logfile rotation backups;default 10)
loglevel=info ; (log level;default info; others: debug,warn,trace)
pidfile=/tmp/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
nodaemon=false ; (start in foreground if true;default false)
minfds=1024 ; (min. avail startup file descriptors;default 1024)
minprocs=200 ; (min. avail process descriptors;default 200)
user=root
; the below section must remain in the config file for RPC
; (supervisorctl/web interface) to work, additional interfaces may be
; added by defining them in separate rpcinterface: sections
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[supervisorctl]
serverurl=unix:///dev/shm/supervisor.sock ; use a unix:// URL for a unix socket
[program:php-fpm]
command=php-fpm
autostart=true
autorestart=true
priority=5
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:account-queue-worker]
directory=/var/www/html
command=wait-for-it rabbitmq:5672 -- php yii account-queue
autostart=true
autorestart=true
priority=10

View File

@@ -0,0 +1,6 @@
[program:account-queue-worker]
directory=/var/www/html
command=wait-for-it rabbitmq:5672 -- php yii account-queue
autostart=true
autorestart=true
priority=10

View File

@@ -1,5 +1,6 @@
namespace: tests\codeception\api namespace: tests\codeception\api
actor: Tester actor: Tester
params: [env]
paths: paths:
tests: . tests: .
log: _output log: _output

View File

@@ -4,23 +4,16 @@ modules:
- Filesystem - Filesystem
- Yii2 - Yii2
- tests\codeception\common\_support\FixtureHelper - tests\codeception\common\_support\FixtureHelper
- tests\codeception\common\_support\amqp\Helper
- Redis - Redis
- AMQP
- Asserts - Asserts
- REST: - REST:
depends: Yii2 depends: Yii2
config: config:
Yii2: Yii2:
configFile: '../config/api/functional.php' configFile: '../config/api/functional.php'
cleanup: true cleanup: false
Redis: Redis:
host: testredis host: "%REDIS_HOST%"
port: 6379 port: 6379
database: 0 database: 0
AMQP:
host: testrabbit
port: 5672
username: 'ely-accounts-tester'
password: 'tester-password'
vhost: '/account.ely.by/tests'
queues: ['account-operations']

View File

@@ -81,7 +81,7 @@ class OauthAuthCodeCest {
public function testCompleteActionOnWrongConditions(FunctionalTester $I) { public function testCompleteActionOnWrongConditions(FunctionalTester $I) {
$I->loggedInAsActiveAccount(); $I->loggedInAsActiveAccount();
$I->wantTo('get accept_required if I dom\'t require any scope, but this is first time request'); $I->wantTo('get accept_required if I don\'t require any scope, but this is first time request');
$this->route->complete($this->buildQueryParams( $this->route->complete($this->buildQueryParams(
'ely', 'ely',
'http://ely.by', 'http://ely.by',

View File

@@ -23,14 +23,7 @@ class OauthRefreshTokenCest {
'ely', 'ely',
'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM' 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM'
)); ));
$I->canSeeResponseCodeIs(200); $this->canSeeRefreshTokenSuccess($I);
$I->canSeeResponseIsJson();
$I->canSeeResponseContainsJson([
'token_type' => 'Bearer',
]);
$I->canSeeResponseJsonMatchesJsonPath('$.access_token');
$I->canSeeResponseJsonMatchesJsonPath('$.refresh_token');
$I->canSeeResponseJsonMatchesJsonPath('$.expires_in');
} }
public function testRefreshTokenWithSameScopes(OauthSteps $I) { public function testRefreshTokenWithSameScopes(OauthSteps $I) {
@@ -41,14 +34,26 @@ class OauthRefreshTokenCest {
'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM', 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
[S::MINECRAFT_SERVER_SESSION, S::OFFLINE_ACCESS] [S::MINECRAFT_SERVER_SESSION, S::OFFLINE_ACCESS]
)); ));
$I->canSeeResponseCodeIs(200); $this->canSeeRefreshTokenSuccess($I);
$I->canSeeResponseIsJson(); }
$I->canSeeResponseContainsJson([
'token_type' => 'Bearer', public function testRefreshTokenTwice(OauthSteps $I) {
]); $refreshToken = $I->getRefreshToken([S::MINECRAFT_SERVER_SESSION]);
$I->canSeeResponseJsonMatchesJsonPath('$.access_token'); $this->route->issueToken($this->buildParams(
$I->canSeeResponseJsonMatchesJsonPath('$.refresh_token'); $refreshToken,
$I->canSeeResponseJsonMatchesJsonPath('$.expires_in'); 'ely',
'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
[S::MINECRAFT_SERVER_SESSION, S::OFFLINE_ACCESS]
));
$this->canSeeRefreshTokenSuccess($I);
$this->route->issueToken($this->buildParams(
$refreshToken,
'ely',
'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
[S::MINECRAFT_SERVER_SESSION, S::OFFLINE_ACCESS]
));
$this->canSeeRefreshTokenSuccess($I);
} }
public function testRefreshTokenWithNewScopes(OauthSteps $I) { public function testRefreshTokenWithNewScopes(OauthSteps $I) {
@@ -91,4 +96,15 @@ class OauthRefreshTokenCest {
return $params; return $params;
} }
private function canSeeRefreshTokenSuccess(OauthSteps $I) {
$I->canSeeResponseCodeIs(200);
$I->canSeeResponseIsJson();
$I->canSeeResponseContainsJson([
'token_type' => 'Bearer',
]);
$I->canSeeResponseJsonMatchesJsonPath('$.access_token');
$I->canSeeResponseJsonMatchesJsonPath('$.expires_in');
$I->cantSeeResponseJsonMatchesJsonPath('$.refresh_token');
}
} }

View File

@@ -3,6 +3,8 @@ modules:
enabled: enabled:
- Yii2: - Yii2:
part: [orm, email, fixtures] part: [orm, email, fixtures]
- tests\codeception\common\_support\amqp\Helper
config: config:
Yii2: Yii2:
configFile: '../config/api/unit.php' configFile: '../config/api/unit.php'
cleanup: false

View File

@@ -16,7 +16,6 @@ use tests\codeception\common\_support\ProtectedCaller;
use tests\codeception\common\fixtures\AccountFixture; use tests\codeception\common\fixtures\AccountFixture;
use tests\codeception\common\fixtures\AccountSessionFixture; use tests\codeception\common\fixtures\AccountSessionFixture;
use Yii; use Yii;
use yii\web\HeaderCollection;
use yii\web\Request; use yii\web\Request;
class ComponentTest extends TestCase { class ComponentTest extends TestCase {
@@ -24,7 +23,7 @@ class ComponentTest extends TestCase {
use ProtectedCaller; use ProtectedCaller;
/** /**
* @var Component * @var Component|\PHPUnit_Framework_MockObject_MockObject
*/ */
private $component; private $component;
@@ -40,6 +39,46 @@ class ComponentTest extends TestCase {
]; ];
} }
public function testGetIdentity() {
$this->specify('getIdentity should return null, if not authorization header', function() {
$this->mockAuthorizationHeader(null);
$this->assertNull($this->component->getIdentity());
});
$this->specify('getIdentity should return null, if passed bearer token don\'t return any account', function() {
$this->mockAuthorizationHeader('some-auth');
/** @var Component|\PHPUnit_Framework_MockObject_MockObject $component */
$component = $this->getMockBuilder(Component::class)
->setMethods(['loginByAccessToken'])
->setConstructorArgs([$this->getComponentArguments()])
->getMock();
$component
->expects($this->once())
->method('loginByAccessToken')
->willReturn(null);
$this->assertNull($component->getIdentity());
});
$this->specify('getIdentity should return identity from loginByAccessToken method', function() {
$identity = new AccountIdentity();
$this->mockAuthorizationHeader('some-auth');
/** @var Component|\PHPUnit_Framework_MockObject_MockObject $component */
$component = $this->getMockBuilder(Component::class)
->setMethods(['loginByAccessToken'])
->setConstructorArgs([$this->getComponentArguments()])
->getMock();
$component
->expects($this->once())
->method('loginByAccessToken')
->willReturn($identity);
$this->assertEquals($identity, $component->getIdentity());
});
}
public function testLogin() { public function testLogin() {
$this->mockRequest(); $this->mockRequest();
$this->specify('success get LoginResult object without session value', function() { $this->specify('success get LoginResult object without session value', function() {
@@ -117,30 +156,9 @@ class ComponentTest extends TestCase {
$component $component
->expects($this->any()) ->expects($this->any())
->method('getIsGuest') ->method('getIsGuest')
->will($this->returnValue(false)); ->willReturn(false);
/** @var HeaderCollection|\PHPUnit_Framework_MockObject_MockObject $headersCollection */ $this->mockAuthorizationHeader($result->getJwt());
$headersCollection = $this->getMockBuilder(HeaderCollection::class)
->setMethods(['get'])
->getMock();
$headersCollection
->expects($this->any())
->method('get')
->with($this->equalTo('Authorization'))
->will($this->returnValue('Bearer ' . $result->getJwt()));
/** @var Request|\PHPUnit_Framework_MockObject_MockObject $request */
$request = $this->getMockBuilder(Request::class)
->setMethods(['getHeaders'])
->getMock();
$request
->expects($this->any())
->method('getHeaders')
->will($this->returnValue($headersCollection));
Yii::$app->set('request', $request);
$session = $component->getActiveSession(); $session = $component->getActiveSession();
expect($session)->isInstanceOf(AccountSession::class); expect($session)->isInstanceOf(AccountSession::class);
@@ -203,6 +221,17 @@ class ComponentTest extends TestCase {
return $request; return $request;
} }
/**
* @param string $bearerToken
*/
private function mockAuthorizationHeader($bearerToken = null) {
if ($bearerToken !== null) {
$bearerToken = 'Bearer ' . $bearerToken;
}
Yii::$app->request->headers->set('Authorization', $bearerToken);
}
private function getComponentArguments() { private function getComponentArguments() {
return [ return [
'identityClass' => AccountIdentity::class, 'identityClass' => AccountIdentity::class,

View File

@@ -0,0 +1,57 @@
<?php
namespace tests\codeception\api\unit\filters;
use api\filters\NginxCache;
use tests\codeception\api\unit\TestCase;
use Yii;
use yii\base\Action;
use yii\web\Controller;
use yii\web\HeaderCollection;
use yii\web\Request;
class NginxCacheTest extends TestCase {
public function testAfterAction() {
$this->testAfterActionInternal(3600, 3600);
$this->testAfterActionInternal('@' . (time() + 30), '@' . (time() + 30));
$this->testAfterActionInternal(function() {
return 3000;
}, 3000);
}
private function testAfterActionInternal($ruleConfig, $expected) {
/** @var HeaderCollection|\PHPUnit_Framework_MockObject_MockObject $headers */
$headers = $this->getMockBuilder(HeaderCollection::class)
->setMethods(['set'])
->getMock();
$headers->expects($this->once())
->method('set')
->with('X-Accel-Expires', $expected);
/** @var Request|\PHPUnit_Framework_MockObject_MockObject $request */
$request = $this->getMockBuilder(Request::class)
->setMethods(['getHeaders'])
->getMock();
$request->expects($this->any())
->method('getHeaders')
->willReturn($headers);
Yii::$app->set('response', $request);
/** @var Controller|\PHPUnit_Framework_MockObject_MockObject $controller */
$controller = $this->getMockBuilder(Controller::class)
->setConstructorArgs(['mock', Yii::$app])
->getMock();
$component = new NginxCache([
'rules' => [
'index' => $ruleConfig,
],
]);
$component->afterAction(new Action('index', $controller), '');
}
}

View File

@@ -25,9 +25,15 @@ class ConfirmEmailFormTest extends TestCase {
$this->assertInstanceOf(AccountSession::class, $result->getSession(), 'session was generated'); $this->assertInstanceOf(AccountSession::class, $result->getSession(), 'session was generated');
$activationExists = EmailActivation::find()->andWhere(['key' => $fixture['key']])->exists(); $activationExists = EmailActivation::find()->andWhere(['key' => $fixture['key']])->exists();
$this->assertFalse($activationExists, 'email activation key is not exist'); $this->assertFalse($activationExists, 'email activation key is not exist');
/** @var Account $user */ /** @var Account $account */
$user = Account::findOne($fixture['account_id']); $account = Account::findOne($fixture['account_id']);
$this->assertEquals(Account::STATUS_ACTIVE, $user->status, 'user status changed to active'); $this->assertEquals(Account::STATUS_ACTIVE, $account->status, 'user status changed to active');
$message = $this->tester->grabLastSentAmqpMessage('events');
$body = json_decode($message->getBody(), true);
$this->assertEquals($account->id, $body['accountId']);
$this->assertEquals($account->username, $body['newUsername']);
$this->assertNull($body['oldUsername']);
} }
private function createModel($key) { private function createModel($key) {

View File

@@ -18,9 +18,8 @@ class ConfirmNewEmailFormTest extends TestCase {
} }
public function testChangeEmail() { public function testChangeEmail() {
$accountId = $this->tester->grabFixture('accounts', 'account-with-change-email-finish-state')['id'];
/** @var Account $account */ /** @var Account $account */
$account = Account::findOne($accountId); $account = Account::findOne($this->getAccountId());
$newEmailConfirmationFixture = $this->tester->grabFixture('emailActivations', 'newEmailConfirmation'); $newEmailConfirmationFixture = $this->tester->grabFixture('emailActivations', 'newEmailConfirmation');
$model = new ConfirmNewEmailForm($account, [ $model = new ConfirmNewEmailForm($account, [
'key' => $newEmailConfirmationFixture['key'], 'key' => $newEmailConfirmationFixture['key'],
@@ -32,6 +31,23 @@ class ConfirmNewEmailFormTest extends TestCase {
])); ]));
$data = unserialize($newEmailConfirmationFixture['_data']); $data = unserialize($newEmailConfirmationFixture['_data']);
$this->assertEquals($data['newEmail'], $account->email); $this->assertEquals($data['newEmail'], $account->email);
$this->tester->canSeeAmqpMessageIsCreated('events');
}
public function testCreateTask() {
/** @var Account $account */
$account = Account::findOne($this->getAccountId());
$model = new ConfirmNewEmailForm($account);
$model->createTask(1, 'test1@ely.by', 'test@ely.by');
$message = $this->tester->grabLastSentAmqpMessage('events');
$body = json_decode($message->getBody(), true);
$this->assertEquals(1, $body['accountId']);
$this->assertEquals('test1@ely.by', $body['newEmail']);
$this->assertEquals('test@ely.by', $body['oldEmail']);
}
private function getAccountId() {
return $this->tester->grabFixture('accounts', 'account-with-change-email-finish-state')['id'];
} }
} }

View File

@@ -35,6 +35,7 @@ class ChangeUsernameFormTest extends TestCase {
$this->assertTrue($model->change()); $this->assertTrue($model->change());
$this->assertEquals('my_new_nickname', Account::findOne($this->getAccountId())->username); $this->assertEquals('my_new_nickname', Account::findOne($this->getAccountId())->username);
$this->assertInstanceOf(UsernameHistory::class, UsernameHistory::findOne(['username' => 'my_new_nickname'])); $this->assertInstanceOf(UsernameHistory::class, UsernameHistory::findOne(['username' => 'my_new_nickname']));
$this->tester->canSeeAmqpMessageIsCreated('events');
} }
public function testChangeWithoutChange() { public function testChangeWithoutChange() {
@@ -49,7 +50,8 @@ class ChangeUsernameFormTest extends TestCase {
'AND', 'AND',
'username' => $username, 'username' => $username,
['>=', 'applied_in', $callTime], ['>=', 'applied_in', $callTime],
]), 'no new UsernameHistory record, if we don\'t change nickname'); ]), 'no new UsernameHistory record, if we don\'t change username');
$this->tester->cantSeeAmqpMessageIsCreated('events');
} }
public function testChangeCase() { public function testChangeCase() {
@@ -65,13 +67,17 @@ class ChangeUsernameFormTest extends TestCase {
UsernameHistory::findOne(['username' => $newUsername]), UsernameHistory::findOne(['username' => $newUsername]),
'username should change, if we change case of some letters' 'username should change, if we change case of some letters'
); );
$this->tester->canSeeAmqpMessageIsCreated('events');
} }
public function testCreateTask() { public function testCreateTask() {
$model = new ChangeUsernameForm(); $model = new ChangeUsernameForm();
$model->createEventTask('1', 'test1', 'test'); $model->createEventTask(1, 'test1', 'test');
// TODO: у меня пока нет идей о том, чтобы это как-то успешно протестировать, увы $message = $this->tester->grabLastSentAmqpMessage('events');
// но по крайней мере можно убедиться, что оно не падает где-то на этом шаге $body = json_decode($message->getBody(), true);
$this->assertEquals(1, $body['accountId']);
$this->assertEquals('test1', $body['newUsername']);
$this->assertEquals('test', $body['oldUsername']);
} }
private function getAccountId() { private function getAccountId() {

View File

@@ -1,35 +0,0 @@
<?php
namespace tests\codeception\api\traits;
use api\traits\ApiNormalize;
use tests\codeception\api\unit\TestCase;
class ApiNormalizeTestClass {
use ApiNormalize;
}
class ApiNormalizerTest extends TestCase {
public function testNormalizeModelErrors() {
$object = new ApiNormalizeTestClass();
$normalized = $object->normalizeModelErrors([
'rulesAgreement' => [
'error.you_must_accept_rules',
],
'email' => [
'error.email_required',
],
'username' => [
'error.username_too_short',
'error.username_not_unique',
],
]);
$this->assertEquals([
'rulesAgreement' => 'error.you_must_accept_rules',
'email' => 'error.email_required',
'username' => 'error.username_too_short',
], $normalized);
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace tests\codeception\common\_support\amqp;
use Codeception\Exception\ModuleException;
use Codeception\Module;
use Codeception\Module\Yii2;
class Helper extends Module {
/**
* Checks that message is created.
*
* ```php
* <?php
* // check that at least 1 message was created
* $I->seeAmqpMessageIsCreated();
*
* // check that only 3 messages were created
* $I->seeAmqpMessageIsCreated(3);
* ```
*
* @param string|null $exchange
* @param int|null $num
*/
public function seeAmqpMessageIsCreated($exchange = null, $num = null) {
if ($num === null) {
$this->assertNotEmpty($this->grabSentAmqpMessages($exchange), 'message were created');
return;
}
// TODO: заменить на assertCount() после релиза Codeception 2.2.7
// https://github.com/Codeception/Codeception/pull/3802
/** @noinspection PhpUnitTestsInspection */
$this->assertEquals(
$num,
count($this->grabSentAmqpMessages($exchange)),
'number of created messages is equal to ' . $num
);
}
/**
* Checks that no messages was created
*
* @param string|null $exchange
*/
public function dontSeeAmqpMessageIsCreated($exchange = null) {
$this->seeAmqpMessageIsCreated($exchange, 0);
}
/**
* Returns last sent message
*
* @param string|null $exchange
* @return \PhpAmqpLib\Message\AMQPMessage
*/
public function grabLastSentAmqpMessage($exchange = null) {
$this->seeAmqpMessageIsCreated();
$messages = $this->grabSentAmqpMessages($exchange);
return end($messages);
}
/**
* Returns array of all sent amqp messages.
* Each message is `\PhpAmqpLib\Message\AMQPMessage` instance.
* Useful to perform additional checks using `Asserts` module.
*
* @param string|null $exchange
* @return \PhpAmqpLib\Message\AMQPMessage[]
* @throws ModuleException
*/
public function grabSentAmqpMessages($exchange = null) {
$amqp = $this->grabComponent('amqp');
if (!$amqp instanceof TestComponent) {
throw new ModuleException($this, 'AMQP module is not mocked, can\'t test messages');
}
return $amqp->getSentMessages($exchange);
}
private function grabComponent(string $component) {
return $this->getYii2()->grabComponent($component);
}
private function getYii2() : Yii2 {
$yii2 = $this->getModule('Yii2');
if (!$yii2 instanceof Yii2) {
throw new ModuleException($this, 'Yii2 module must be configured');
}
return $yii2;
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace tests\codeception\common\_support\amqp;
use common\components\RabbitMQ\Component;
use PhpAmqpLib\Connection\AbstractConnection;
class TestComponent extends Component {
private $sentMessages = [];
public function init() {
\yii\base\Component::init();
}
public function getConnection() {
/** @noinspection MagicMethodsValidityInspection */
/** @noinspection PhpMissingParentConstructorInspection */
return new class extends AbstractConnection {
public function __construct(
$user,
$password,
$vhost,
$insist,
$login_method,
$login_response,
$locale,
\PhpAmqpLib\Wire\IO\AbstractIO $io,
$heartbeat
) {
// ничего не делаем
}
};
}
public function sendToExchange($exchangeName, $routingKey, $message, $exchangeArgs = [], $publishArgs = []) {
$this->sentMessages[$exchangeName][] = $this->prepareMessage($message);
}
/**
* @param string|null $exchangeName
* @return \PhpAmqpLib\Message\AMQPMessage[]
*/
public function getSentMessages(string $exchangeName = null) : array {
if ($exchangeName !== null) {
return $this->sentMessages[$exchangeName] ?? [];
} else {
$messages = [];
foreach($this->sentMessages as $exchangeGroup) {
foreach ($exchangeGroup as $message) {
$messages[] = $message;
}
}
return $messages;
}
}
}

View File

@@ -1,5 +1,6 @@
namespace: tests\codeception\common namespace: tests\codeception\common
actor: Tester actor: Tester
params: [env]
paths: paths:
tests: . tests: .
log: _output log: _output

View File

@@ -0,0 +1,17 @@
<?php
namespace tests\codeception\common\fixtures;
use common\models\OauthAccessToken;
use yii\test\ActiveFixture;
class OauthAccessTokenFixture extends ActiveFixture {
public $modelClass = OauthAccessToken::class;
public $dataFile = '@tests/codeception/common/fixtures/data/oauth-access-tokens.php';
public $depends = [
OauthSessionFixture::class,
];
}

View File

@@ -1,7 +1,6 @@
<?php <?php
namespace tests\codeception\common\fixtures; namespace tests\codeception\common\fixtures;
use common\models\OauthScope;
use common\models\OauthSession; use common\models\OauthSession;
use yii\test\ActiveFixture; use yii\test\ActiveFixture;

View File

@@ -0,0 +1,13 @@
<?php
return [
'admin-test1' => [
'access_token' => '07541285-831e-1e47-e314-b950309a6fca',
'session_id' => 1,
'expire_time' => time() + 3600,
],
'admin-test1-expired' => [
'access_token' => '2977ec21-3022-96f8-544db-2e1df936908',
'session_id' => 1,
'expire_time' => time() - 3600,
],
];

View File

@@ -6,7 +6,7 @@ return [
'name' => 'Ely.by', 'name' => 'Ely.by',
'description' => 'Всем знакомое елуби', 'description' => 'Всем знакомое елуби',
'redirect_uri' => 'http://ely.by', 'redirect_uri' => 'http://ely.by',
'account_id' => NULL, 'account_id' => null,
'is_trusted' => 0, 'is_trusted' => 0,
'created_at' => 1455309271, 'created_at' => 1455309271,
], ],
@@ -16,8 +16,18 @@ return [
'name' => 'TLauncher', 'name' => 'TLauncher',
'description' => 'Лучший альтернативный лаунчер для Minecraft с большим количеством версий и их модификаций, а также возмоностью входа как с лицензионным аккаунтом, так и без него.', 'description' => 'Лучший альтернативный лаунчер для Minecraft с большим количеством версий и их модификаций, а также возмоностью входа как с лицензионным аккаунтом, так и без него.',
'redirect_uri' => '', 'redirect_uri' => '',
'account_id' => NULL, 'account_id' => null,
'is_trusted' => 0, 'is_trusted' => 0,
'created_at' => 1455318468, 'created_at' => 1455318468,
], ],
'test1' => [
'id' => 'test1',
'secret' => 'eEvrKHF47sqiaX94HsX-xXzdGiz3mcsq',
'name' => 'Test1',
'description' => 'Some description',
'redirect_uri' => 'http://test1.net',
'account_id' => null,
'is_trusted' => 0,
'created_at' => 1479937982,
],
]; ];

View File

@@ -1,3 +1,10 @@
<?php <?php
return [ return [
'admin-test1' => [
'id' => 1,
'owner_type' => 'user',
'owner_id' => 1,
'client_id' => 'test1',
'client_redirect_uri' => 'http://test1.net/oauth',
],
]; ];

View File

@@ -6,3 +6,4 @@ modules:
config: config:
Yii2: Yii2:
configFile: '../config/common/unit.php' configFile: '../config/common/unit.php'
cleanup: false

View File

@@ -10,29 +10,18 @@ return [
], ],
], ],
'components' => [ 'components' => [
'db' => [
'dsn' => 'mysql:host=testdb;dbname=ely_accounts_test',
'username' => 'ely_accounts_tester',
'password' => 'ely_accounts_tester_password',
],
'mailer' => [
'useFileTransport' => true,
],
'urlManager' => [ 'urlManager' => [
'showScriptName' => true, 'showScriptName' => true,
], ],
'redis' => [
'hostname' => 'testredis',
],
'amqp' => [
'host' => 'testrabbit',
'user' => 'ely-accounts-tester',
'password' => 'tester-password',
'vhost' => '/account.ely.by/tests',
],
'security' => [ 'security' => [
// Для тестов нам не сильно важна безопасность, а вот время прохождения тестов значительно сокращается // Для тестов нам не сильно важна безопасность, а вот время прохождения тестов значительно сокращается
'passwordHashCost' => 4, 'passwordHashCost' => 4,
], ],
'amqp' => [
'class' => tests\codeception\common\_support\amqp\TestComponent::class,
],
'sentry' => [
'enabled' => false,
],
], ],
]; ];

View File

@@ -1,5 +1,6 @@
namespace: tests\codeception\console namespace: tests\codeception\console
actor: Tester actor: Tester
params: [env]
paths: paths:
tests: . tests: .
log: _output log: _output

View File

@@ -6,3 +6,4 @@ modules:
config: config:
Yii2: Yii2:
configFile: '../config/console/unit.php' configFile: '../config/console/unit.php'
cleanup: false

View File

@@ -0,0 +1,31 @@
<?php
namespace codeception\console\unit\controllers;
use common\models\OauthAccessToken;
use console\controllers\CleanupController;
use tests\codeception\common\fixtures\OauthAccessTokenFixture;
use tests\codeception\console\unit\TestCase;
use Yii;
class CleanupControllerTest extends TestCase {
public function _fixtures() {
return [
'accessTokens' => OauthAccessTokenFixture::class,
];
}
public function testActionAccessTokens() {
/** @var OauthAccessToken $validAccessToken */
$validAccessToken = $this->tester->grabFixture('accessTokens', 'admin-test1');
/** @var OauthAccessToken $expiredAccessToken */
$expiredAccessToken = $this->tester->grabFixture('accessTokens', 'admin-test1-expired');
$controller = new CleanupController('cleanup', Yii::$app);
$this->assertEquals(0, $controller->actionAccessTokens());
$this->tester->canSeeRecord(OauthAccessToken::class, ['access_token' => $validAccessToken->access_token]);
$this->tester->cantSeeRecord(OauthAccessToken::class, ['access_token' => $expiredAccessToken->access_token]);
}
}

View File

@@ -9,18 +9,21 @@ services:
depends_on: depends_on:
- testdb - testdb
- testredis - testredis
- testrabbit
volumes: volumes:
- ./codeception/_output:/var/www/html/tests/codeception/_output - ./..:/var/www/html
- ./codeception/api/_output:/var/www/html/tests/codeception/api/_output
- ./codeception/common/_output:/var/www/html/tests/codeception/common/_output
- ./codeception/console/_output:/var/www/html/tests/codeception/console/_output
environment: environment:
- YII_DEBUG=true YII_DEBUG: "true"
- YII_ENV=test YII_ENV: "test"
# DB config
DB_HOST: "testdb"
DB_DATABASE: "ely_accounts_test"
DB_USER: "ely_accounts_tester"
DB_PASSWORD: "ely_accounts_tester_password"
# Redis config
REDIS_HOST: "testredis"
# Это я потом, когда-нибудь, уберу # Это я потом, когда-нибудь, уберу
- XDEBUG_CONFIG=remote_host=10.254.254.254 XDEBUG_CONFIG: "remote_host=10.254.254.254"
- PHP_IDE_CONFIG=serverName=docker PHP_IDE_CONFIG: "serverName=docker"
testdb: testdb:
container_name: accountelyby_testdb container_name: accountelyby_testdb
@@ -36,11 +39,3 @@ services:
testredis: testredis:
container_name: accountelyby_testredis container_name: accountelyby_testredis
image: redis:3.0-alpine image: redis:3.0-alpine
testrabbit:
container_name: accountelyby_testrabbit
image: rabbitmq:3.6
environment:
RABBITMQ_DEFAULT_USER: "ely-accounts-tester"
RABBITMQ_DEFAULT_PASS: "tester-password"
RABBITMQ_DEFAULT_VHOST: "/account.ely.by/tests"