mirror of
https://github.com/elyby/accounts.git
synced 2024-11-30 02:32:26 +05:30
Реализована логика oAuth авторизации приложений, добавлен Redis, удалены лишние тесты, пофикшены старые.
This commit is contained in:
parent
59addfac07
commit
f5f93ddef1
@ -13,8 +13,9 @@ return [
|
||||
'controllerNamespace' => 'api\controllers',
|
||||
'components' => [
|
||||
'user' => [
|
||||
'identityClass' => 'common\models\Account',
|
||||
'identityClass' => \common\models\Account::class,
|
||||
'enableAutoLogin' => true,
|
||||
'loginUrl' => null,
|
||||
],
|
||||
'log' => [
|
||||
'traceLevel' => YII_DEBUG ? 3 : 0,
|
||||
@ -42,6 +43,10 @@ return [
|
||||
'response' => [
|
||||
'format' => \yii\web\Response::FORMAT_JSON,
|
||||
],
|
||||
'oauth' => [
|
||||
'class' => \common\components\oauth\Component::class,
|
||||
'grantTypes' => ['authorization_code'],
|
||||
],
|
||||
],
|
||||
'params' => $params,
|
||||
];
|
||||
|
@ -2,12 +2,14 @@
|
||||
namespace api\controllers;
|
||||
|
||||
use api\traits\ApiNormalize;
|
||||
use Yii;
|
||||
|
||||
/**
|
||||
* @property \common\models\Account|null $account
|
||||
*/
|
||||
class Controller extends \yii\rest\Controller {
|
||||
use ApiNormalize;
|
||||
|
||||
public $enableCsrfValidation = true;
|
||||
|
||||
public function behaviors() {
|
||||
$parentBehaviors = parent::behaviors();
|
||||
// xml нам не понадобится
|
||||
@ -16,4 +18,11 @@ class Controller extends \yii\rest\Controller {
|
||||
return $parentBehaviors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \common\models\Account|null
|
||||
*/
|
||||
public function getAccount() {
|
||||
return Yii::$app->getUser()->getIdentity();
|
||||
}
|
||||
|
||||
}
|
||||
|
219
api/controllers/OauthController.php
Normal file
219
api/controllers/OauthController.php
Normal file
@ -0,0 +1,219 @@
|
||||
<?php
|
||||
namespace api\controllers;
|
||||
|
||||
use common\components\oauth\Exception\AcceptRequiredException;
|
||||
use common\components\oauth\Exception\AccessDeniedException;
|
||||
use common\models\OauthClient;
|
||||
use League\OAuth2\Server\Exception\OAuthException;
|
||||
use Yii;
|
||||
use yii\filters\AccessControl;
|
||||
use yii\helpers\ArrayHelper;
|
||||
|
||||
class OauthController extends Controller {
|
||||
|
||||
public function behaviors() {
|
||||
return array_merge(parent::behaviors(), [
|
||||
'access' => [
|
||||
'class' => AccessControl::class,
|
||||
'rules' => [
|
||||
[
|
||||
'actions' => ['validate'],
|
||||
'allow' => true,
|
||||
],
|
||||
[
|
||||
'actions' => ['complete'],
|
||||
'allow' => true,
|
||||
'roles' => ['@'],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function verbs() {
|
||||
return [
|
||||
'validate' => ['GET'],
|
||||
'complete' => ['POST'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \League\OAuth2\Server\AuthorizationServer
|
||||
*/
|
||||
protected function getServer() {
|
||||
/** @var \common\components\oauth\Component $oauth */
|
||||
$oauth = Yii::$app->get('oauth');
|
||||
return $oauth->authServer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \League\OAuth2\Server\Grant\AuthCodeGrant
|
||||
*/
|
||||
protected function getGrantType() {
|
||||
return $this->getServer()->getGrantType('authorization_code');
|
||||
}
|
||||
|
||||
/**
|
||||
* Запрос, который должен проверить переданные параметры oAuth авторизации
|
||||
* и сформировать ответ для нашего приложения на фронте
|
||||
*
|
||||
* Входными данными является стандартный список GET параметров по стандарту oAuth:
|
||||
* $_GET = [
|
||||
* client_id,
|
||||
* redirect_uri,
|
||||
* response_type,
|
||||
* scope,
|
||||
* state,
|
||||
* ]
|
||||
*
|
||||
* Кроме того можно передать значения description для переопределения описания приложения.
|
||||
*
|
||||
* @return array|\yii\web\Response
|
||||
*/
|
||||
public function actionValidate() {
|
||||
try {
|
||||
$authParams = $this->getGrantType()->checkAuthorizeParams();
|
||||
/** @var \League\OAuth2\Server\Entity\ClientEntity $client */
|
||||
$client = $authParams['client'];
|
||||
/** @var \common\models\OauthClient $clientModel */
|
||||
$clientModel = OauthClient::findOne($client->getId());
|
||||
$response = $this->buildSuccessResponse(
|
||||
Yii::$app->request->getQueryParams(),
|
||||
$clientModel,
|
||||
$authParams['scopes']
|
||||
);
|
||||
} catch (OAuthException $e) {
|
||||
$response = $this->buildErrorResponse($e);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Метод выполняется генерацию авторизационного кода (auth_code) и формирование ссылки
|
||||
* для дальнейшнешл редиректа пользователя назад на сайт клиента
|
||||
*
|
||||
* Входными данными является всё те же параметры, что были необходимы для валидации:
|
||||
* $_GET = [
|
||||
* client_id,
|
||||
* redirect_uri,
|
||||
* response_type,
|
||||
* scope,
|
||||
* state,
|
||||
* ];
|
||||
* А также поле accept, которое показывает, что пользователь нажал на кнопку "Принять". Если поле присутствует,
|
||||
* то оно будет интерпретироваться как любое приводимое к false значение. В ином случае, значение будет
|
||||
* интерпретировано, как положительный исход.
|
||||
*
|
||||
* @return array|\yii\web\Response
|
||||
*/
|
||||
public function actionComplete() {
|
||||
$grant = $this->getGrantType();
|
||||
try {
|
||||
$authParams = $grant->checkAuthorizeParams();
|
||||
$account = $this->getAccount();
|
||||
/** @var \League\OAuth2\Server\Entity\ClientEntity $client */
|
||||
$client = $authParams['client'];
|
||||
/** @var \common\models\OauthClient $clientModel */
|
||||
$clientModel = OauthClient::findOne($client->getId());
|
||||
|
||||
if (!$account->canAutoApprove($clientModel, $authParams['scopes'])) {
|
||||
$isAccept = Yii::$app->request->post('accept');
|
||||
if ($isAccept === null) {
|
||||
throw new AcceptRequiredException();
|
||||
}
|
||||
|
||||
if (!$isAccept) {
|
||||
throw new AccessDeniedException($authParams['redirect_uri']);
|
||||
}
|
||||
}
|
||||
|
||||
$redirectUri = $grant->newAuthorizeRequest('user', $account->id, $authParams);
|
||||
$response = [
|
||||
'success' => true,
|
||||
'redirectUri' => $redirectUri,
|
||||
];
|
||||
} catch (OAuthException $e) {
|
||||
$response = $this->buildErrorResponse($e);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Метод выполняется сервером приложения, которому был выдан auth_token.
|
||||
*
|
||||
* Входными данными является стандартный список GET параметров по стандарту oAuth:
|
||||
* $_GET = [
|
||||
* client_id,
|
||||
* client_secret,
|
||||
* redirect_uri,
|
||||
* code|refresh_token,
|
||||
* grant_type,
|
||||
* ]
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function actionIssueToken() {
|
||||
try {
|
||||
$response = $this->getServer()->issueAccessToken();
|
||||
} catch (OAuthException $e) {
|
||||
Yii::$app->response->statusCode = $e->httpStatusCode;
|
||||
$response = [
|
||||
'error' => $e->errorType,
|
||||
'message' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $queryParams
|
||||
* @param OauthClient $clientModel
|
||||
* @param \League\OAuth2\Server\Entity\ScopeEntity[] $scopes
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function buildSuccessResponse($queryParams, OauthClient $clientModel, $scopes) {
|
||||
return [
|
||||
'success' => true,
|
||||
// Возвращаем только те ключи, которые имеют реальное отношение к oAuth параметрам
|
||||
'oAuth' => array_intersect_key($queryParams, array_flip([
|
||||
'client_id',
|
||||
'redirect_uri',
|
||||
'response_type',
|
||||
'scope',
|
||||
'state',
|
||||
])),
|
||||
'client' => [
|
||||
'id' => $clientModel->id,
|
||||
'name' => $clientModel->name,
|
||||
'description' => ArrayHelper::getValue($queryParams, 'description', $clientModel->description),
|
||||
],
|
||||
'session' => [
|
||||
'scopes' => array_keys($scopes),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function buildErrorResponse(OAuthException $e) {
|
||||
$response = [
|
||||
'success' => false,
|
||||
'error' => $e->errorType,
|
||||
'parameter' => $e->parameter,
|
||||
'statusCode' => $e->httpStatusCode,
|
||||
];
|
||||
|
||||
if ($e->shouldRedirect()) {
|
||||
$response['redirectUri'] = $e->getRedirectUri();
|
||||
}
|
||||
|
||||
if ($e->httpStatusCode !== 200) {
|
||||
Yii::$app->response->setStatusCode($e->httpStatusCode);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
}
|
@ -18,7 +18,7 @@ class RegistrationForm extends BaseApiForm {
|
||||
|
||||
public function rules() {
|
||||
return [
|
||||
['rulesAgreement', 'boolean', 'message' => 'error.you_must_accept_rules'],
|
||||
['rulesAgreement', 'required', 'message' => 'error.you_must_accept_rules'],
|
||||
[[], ReCaptchaValidator::class, 'message' => 'error.captcha_invalid', 'when' => !YII_ENV_TEST],
|
||||
|
||||
['username', 'filter', 'filter' => 'trim'],
|
||||
|
@ -1,58 +0,0 @@
|
||||
<?php
|
||||
namespace api\models;
|
||||
|
||||
use common\models\Account;
|
||||
use yii\base\Model;
|
||||
use Yii;
|
||||
|
||||
/**
|
||||
* Signup form
|
||||
*/
|
||||
class SignupForm extends Model
|
||||
{
|
||||
public $username;
|
||||
public $email;
|
||||
public $password;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
['username', 'filter', 'filter' => 'trim'],
|
||||
['username', 'required'],
|
||||
['username', 'unique', 'targetClass' => '\common\models\User', 'message' => 'This username has already been taken.'],
|
||||
['username', 'string', 'min' => 2, 'max' => 255],
|
||||
|
||||
['email', 'filter', 'filter' => 'trim'],
|
||||
['email', 'required'],
|
||||
['email', 'email'],
|
||||
['email', 'string', 'max' => 255],
|
||||
['email', 'unique', 'targetClass' => '\common\models\User', 'message' => 'This email address has already been taken.'],
|
||||
|
||||
['password', 'required'],
|
||||
['password', 'string', 'min' => 6],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs user up.
|
||||
*
|
||||
* @return Account|null the saved model or null if saving fails
|
||||
*/
|
||||
public function signup()
|
||||
{
|
||||
if ($this->validate()) {
|
||||
$user = new Account();
|
||||
$user->email = $this->email;
|
||||
$user->setPassword($this->password);
|
||||
$user->generateAuthKey();
|
||||
if ($user->save()) {
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
63
common/components/oauth/Component.php
Normal file
63
common/components/oauth/Component.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
namespace common\components\oauth;
|
||||
|
||||
use common\components\oauth\Storage\Redis\AuthCodeStorage;
|
||||
use common\components\oauth\Storage\Yii2\AccessTokenStorage;
|
||||
use common\components\oauth\Storage\Yii2\ClientStorage;
|
||||
use common\components\oauth\Storage\Yii2\ScopeStorage;
|
||||
use common\components\oauth\Storage\Yii2\SessionStorage;
|
||||
use League\OAuth2\Server\AuthorizationServer;
|
||||
use yii\base\InvalidConfigException;
|
||||
|
||||
/**
|
||||
* @property AuthorizationServer $authServer
|
||||
*/
|
||||
class Component extends \yii\base\Component {
|
||||
|
||||
/**
|
||||
* @var AuthorizationServer
|
||||
*/
|
||||
private $_authServer;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
public $grantTypes = [];
|
||||
|
||||
/**
|
||||
* @var array grant type => class
|
||||
*/
|
||||
public $grantMap = [
|
||||
'authorization_code' => 'League\OAuth2\Server\Grant\AuthCodeGrant',
|
||||
'client_credentials' => 'League\OAuth2\Server\Grant\ClientCredentialsGrant',
|
||||
'password' => 'League\OAuth2\Server\Grant\PasswordGrant',
|
||||
'refresh_token' => 'League\OAuth2\Server\Grant\RefreshTokenGrant'
|
||||
];
|
||||
|
||||
public function getAuthServer() {
|
||||
if ($this->_authServer === null) {
|
||||
$authServer = new AuthorizationServer();
|
||||
$authServer
|
||||
->setAccessTokenStorage(new AccessTokenStorage())
|
||||
->setClientStorage(new ClientStorage())
|
||||
->setScopeStorage(new ScopeStorage())
|
||||
->setSessionStorage(new SessionStorage())
|
||||
->setAuthCodeStorage(new AuthCodeStorage())
|
||||
->setScopeDelimiter(',');
|
||||
|
||||
$this->_authServer = $authServer;
|
||||
|
||||
foreach ($this->grantTypes as $grantType) {
|
||||
if (!array_key_exists($grantType, $this->grantMap)) {
|
||||
throw new InvalidConfigException('Invalid grant type');
|
||||
}
|
||||
|
||||
$grant = new $this->grantMap[$grantType]();
|
||||
$this->_authServer->addGrantType($grant);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->_authServer;
|
||||
}
|
||||
|
||||
}
|
27
common/components/oauth/Entity/AccessTokenEntity.php
Normal file
27
common/components/oauth/Entity/AccessTokenEntity.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
namespace common\components\oauth\Entity;
|
||||
|
||||
use League\OAuth2\Server\Entity\EntityTrait;
|
||||
use League\OAuth2\Server\Entity\SessionEntity;
|
||||
|
||||
class AccessTokenEntity extends \League\OAuth2\Server\Entity\AccessTokenEntity {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
27
common/components/oauth/Entity/AuthCodeEntity.php
Normal file
27
common/components/oauth/Entity/AuthCodeEntity.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?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;
|
||||
}
|
||||
|
||||
}
|
27
common/components/oauth/Entity/SessionEntity.php
Normal file
27
common/components/oauth/Entity/SessionEntity.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
namespace common\components\oauth\Entity;
|
||||
|
||||
use League\OAuth2\Server\Entity\ClientEntity;
|
||||
use League\OAuth2\Server\Entity\EntityTrait;
|
||||
|
||||
class SessionEntity extends \League\OAuth2\Server\Entity\SessionEntity {
|
||||
use EntityTrait;
|
||||
|
||||
protected $clientId;
|
||||
|
||||
public function getClientId() {
|
||||
return $this->clientId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @return static
|
||||
*/
|
||||
public function associateClient(ClientEntity $client) {
|
||||
parent::associateClient($client);
|
||||
$this->clientId = $client->getId();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace common\components\oauth\Exception;
|
||||
|
||||
use League\OAuth2\Server\Exception\OAuthException;
|
||||
|
||||
class AcceptRequiredException extends OAuthException {
|
||||
|
||||
public $httpStatusCode = 401;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public $errorType = 'accept_required';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct() {
|
||||
parent::__construct('Client must accept authentication request.');
|
||||
}
|
||||
|
||||
}
|
11
common/components/oauth/Exception/AccessDeniedException.php
Normal file
11
common/components/oauth/Exception/AccessDeniedException.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
namespace common\components\oauth\Exception;
|
||||
|
||||
class AccessDeniedException extends \League\OAuth2\Server\Exception\AccessDeniedException {
|
||||
|
||||
public function __construct($redirectUri = null) {
|
||||
parent::__construct();
|
||||
$this->redirectUri = $redirectUri;
|
||||
}
|
||||
|
||||
}
|
84
common/components/oauth/Storage/Redis/AuthCodeStorage.php
Normal file
84
common/components/oauth/Storage/Redis/AuthCodeStorage.php
Normal file
@ -0,0 +1,84 @@
|
||||
<?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['sessionId'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
namespace Fahmiardi\OAuth2\Server\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();
|
||||
}
|
||||
|
||||
}
|
85
common/components/oauth/Storage/Yii2/AccessTokenStorage.php
Normal file
85
common/components/oauth/Storage/Yii2/AccessTokenStorage.php
Normal file
@ -0,0 +1,85 @@
|
||||
<?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([
|
||||
'access_token' => $token,
|
||||
'expire_time' => $expireTime,
|
||||
'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();
|
||||
}
|
||||
|
||||
}
|
73
common/components/oauth/Storage/Yii2/ClientStorage.php
Normal file
73
common/components/oauth/Storage/Yii2/ClientStorage.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
namespace common\components\oauth\Storage\Yii2;
|
||||
|
||||
use common\components\oauth\Entity\SessionEntity;
|
||||
use common\models\OauthClient;
|
||||
use League\OAuth2\Server\Entity\ClientEntity;
|
||||
use League\OAuth2\Server\Entity\SessionEntity as OriginalSessionEntity;
|
||||
use League\OAuth2\Server\Storage\AbstractStorage;
|
||||
use League\OAuth2\Server\Storage\ClientInterface;
|
||||
|
||||
class ClientStorage extends AbstractStorage implements ClientInterface {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function get($clientId, $clientSecret = null, $redirectUri = null, $grantType = null) {
|
||||
$query = OauthClient::find()
|
||||
->select(['id', 'name', 'secret'])
|
||||
->where([OauthClient::tableName() . '.id' => $clientId]);
|
||||
|
||||
if ($clientSecret !== null) {
|
||||
$query->andWhere(['secret' => $clientSecret]);
|
||||
}
|
||||
|
||||
if ($redirectUri !== null) {
|
||||
$query
|
||||
->addSelect(['redirect_uri'])
|
||||
->andWhere(['redirect_uri' => $redirectUri]);
|
||||
}
|
||||
|
||||
$model = $query->asArray()->one();
|
||||
if ($model === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$entity = new ClientEntity($this->server);
|
||||
$entity->hydrate([
|
||||
'id' => $model['id'],
|
||||
'name' => $model['name'],
|
||||
'secret' => $model['secret'],
|
||||
]);
|
||||
|
||||
if (isset($model['redirect_uri'])) {
|
||||
$entity->hydrate([
|
||||
'redirectUri' => $model['redirect_uri'],
|
||||
]);
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getBySession(OriginalSessionEntity $session) {
|
||||
if (!$session instanceof SessionEntity) {
|
||||
throw new \ErrorException('This module assumes that $session typeof ' . SessionEntity::class);
|
||||
}
|
||||
|
||||
$model = OauthClient::find()
|
||||
->select(['id', 'name'])
|
||||
->andWhere(['id' => $session->getClientId()])
|
||||
->asArray()
|
||||
->one();
|
||||
|
||||
if ($model === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (new ClientEntity($this->server))->hydrate($model);
|
||||
}
|
||||
|
||||
}
|
26
common/components/oauth/Storage/Yii2/ScopeStorage.php
Normal file
26
common/components/oauth/Storage/Yii2/ScopeStorage.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
namespace common\components\oauth\Storage\Yii2;
|
||||
|
||||
use common\models\OauthScope;
|
||||
use League\OAuth2\Server\Entity\ScopeEntity;
|
||||
use League\OAuth2\Server\Storage\AbstractStorage;
|
||||
use League\OAuth2\Server\Storage\ScopeInterface;
|
||||
|
||||
class ScopeStorage extends AbstractStorage implements ScopeInterface {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function get($scope, $grantType = null, $clientId = null) {
|
||||
$row = OauthScope::find()->andWhere(['id' => $scope])->asArray()->one();
|
||||
if ($row === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$entity = new ScopeEntity($this->server);
|
||||
$entity->hydrate($row);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
}
|
125
common/components/oauth/Storage/Yii2/SessionStorage.php
Normal file
125
common/components/oauth/Storage/Yii2/SessionStorage.php
Normal file
@ -0,0 +1,125 @@
|
||||
<?php
|
||||
namespace common\components\oauth\Storage\Yii2;
|
||||
|
||||
use common\components\oauth\Entity\AuthCodeEntity;
|
||||
use common\components\oauth\Entity\SessionEntity;
|
||||
use common\models\OauthSession;
|
||||
use League\OAuth2\Server\Entity\AccessTokenEntity as OriginalAccessTokenEntity;
|
||||
use League\OAuth2\Server\Entity\AuthCodeEntity as OriginalAuthCodeEntity;
|
||||
use League\OAuth2\Server\Entity\ScopeEntity;
|
||||
use League\OAuth2\Server\Entity\SessionEntity as OriginalSessionEntity;
|
||||
use League\OAuth2\Server\Storage\AbstractStorage;
|
||||
use League\OAuth2\Server\Storage\SessionInterface;
|
||||
use yii\db\ActiveQuery;
|
||||
use yii\db\Exception;
|
||||
|
||||
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
|
||||
* @return SessionEntity|null
|
||||
*/
|
||||
public function getSession($sessionId) {
|
||||
return $this->hydrateEntity($this->getSessionModel($sessionId));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getByAccessToken(OriginalAccessTokenEntity $accessToken) {
|
||||
/** @var OauthSession|null $model */
|
||||
$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) {
|
||||
if (!$authCode instanceof AuthCodeEntity) {
|
||||
throw new \ErrorException('This module assumes that $authCode typeof ' . AuthCodeEntity::class);
|
||||
}
|
||||
|
||||
return $this->getSession($authCode->getSessionId());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getScopes(OriginalSessionEntity $session) {
|
||||
$result = [];
|
||||
foreach ($this->getSessionModel($session->getId())->getScopes() as $scope) {
|
||||
// TODO: нужно проверить все выданные скоупы на их существование
|
||||
$result[] = (new ScopeEntity($this->server))->hydrate(['id' => $scope]);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function create($ownerType, $ownerId, $clientId, $clientRedirectUri = null) {
|
||||
$sessionId = OauthSession::find()
|
||||
->select('id')
|
||||
->andWhere([
|
||||
'client_id' => $clientId,
|
||||
'owner_type' => $ownerType,
|
||||
'owner_id' => $ownerId,
|
||||
])->scalar();
|
||||
|
||||
if ($sessionId === false) {
|
||||
$model = new OauthSession([
|
||||
'client_id' => $clientId,
|
||||
'owner_type' => $ownerType,
|
||||
'owner_id' => $ownerId,
|
||||
]);
|
||||
|
||||
if (!$model->save()) {
|
||||
throw new Exception('Cannot save ' . OauthSession::class . ' model.');
|
||||
}
|
||||
|
||||
$sessionId = $model->id;
|
||||
}
|
||||
|
||||
return $sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function associateScope(OriginalSessionEntity $session, ScopeEntity $scope) {
|
||||
$this->getSessionModel($session->getId())->getScopes()->add($scope->getId());
|
||||
}
|
||||
|
||||
}
|
58
common/components/redis/Key.php
Normal file
58
common/components/redis/Key.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
namespace common\components\redis;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Yii;
|
||||
|
||||
class Key {
|
||||
|
||||
protected $key;
|
||||
|
||||
/**
|
||||
* @return \yii\redis\Connection
|
||||
*/
|
||||
public function getRedis() {
|
||||
return Yii::$app->get('redis');
|
||||
}
|
||||
|
||||
public function getKey() {
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
public function getValue() {
|
||||
return $this->getRedis()->get(json_decode($this->key));
|
||||
}
|
||||
|
||||
public function setValue($value) {
|
||||
$this->getRedis()->set($this->key, json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
$this->getRedis()->executeCommand('DEL', [$this->key]);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function expire($ttl) {
|
||||
$this->getRedis()->executeCommand('EXPIRE', [$this->key, $ttl]);
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function buildKey(array $parts) {
|
||||
$keyParts = [];
|
||||
foreach($parts as $part) {
|
||||
$keyParts[] = str_replace('_', ':', $part);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
49
common/components/redis/Set.php
Normal file
49
common/components/redis/Set.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?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->get('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());
|
||||
}
|
||||
|
||||
}
|
@ -15,6 +15,9 @@ return [
|
||||
],
|
||||
'security' => [
|
||||
'passwordHashStrategy' => 'password_hash',
|
||||
]
|
||||
],
|
||||
'redis' => [
|
||||
'class' => 'yii\redis\Connection',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
@ -28,6 +28,7 @@ use yii\web\IdentityInterface;
|
||||
*
|
||||
* Отношения:
|
||||
* @property EmailActivation[] $emailActivations
|
||||
* @property OauthSession[] $sessions
|
||||
*
|
||||
* Поведения:
|
||||
* @mixin TimestampBehavior
|
||||
@ -216,4 +217,34 @@ class Account extends ActiveRecord implements IdentityInterface {
|
||||
return $this->hasMany(EmailActivation::class, ['id' => 'account_id']);
|
||||
}
|
||||
|
||||
public function getSessions() {
|
||||
return $this->hasMany(OauthSession::class, ['owner_id' => 'id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Метод проверяет, может ли текщий пользователь быть автоматически авторизован
|
||||
* для указанного клиента без запроса доступа к необходимому списку прав
|
||||
*
|
||||
* @param OauthClient $client
|
||||
* @param \League\OAuth2\Server\Entity\ScopeEntity[] $scopes
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canAutoApprove(OauthClient $client, array $scopes = []) {
|
||||
if ($client->is_trusted) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @var OauthSession|null $session */
|
||||
$session = $this->getSessions()->andWhere(['client_id' => $client->id])->one();
|
||||
if ($session !== null) {
|
||||
$existScopes = $session->getScopes()->members();
|
||||
if (empty(array_diff(array_keys($scopes), $existScopes))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
41
common/models/OauthAccessToken.php
Normal file
41
common/models/OauthAccessToken.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
namespace common\models;
|
||||
|
||||
use common\components\redis\Set;
|
||||
use Yii;
|
||||
use yii\db\ActiveRecord;
|
||||
|
||||
/**
|
||||
* This is the model class for table "oauth_access_tokens".
|
||||
*
|
||||
* @property string $access_token
|
||||
* @property string $session_id
|
||||
* @property integer $expire_time
|
||||
*
|
||||
* @property Set $scopes
|
||||
*/
|
||||
class OauthAccessToken extends ActiveRecord {
|
||||
|
||||
public static function tableName() {
|
||||
return '{{%oauth_access_tokens}}';
|
||||
}
|
||||
|
||||
public function getSession() {
|
||||
return $this->hasOne(OauthSession::class, ['id' => 'session_id']);
|
||||
}
|
||||
|
||||
public function getScopes() {
|
||||
return new Set($this->getDb()->getSchema()->getRawTableName($this->tableName()), $this->access_token, 'scopes');
|
||||
}
|
||||
|
||||
public function beforeDelete() {
|
||||
if (!$result = parent::beforeDelete()) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$this->getScopes()->delete();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
49
common/models/OauthClient.php
Normal file
49
common/models/OauthClient.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
namespace common\models;
|
||||
|
||||
use Yii;
|
||||
use yii\db\ActiveRecord;
|
||||
|
||||
/**
|
||||
* Поля модели:
|
||||
* @property string $id
|
||||
* @property string $secret
|
||||
* @property string $name
|
||||
* @property string $description
|
||||
* @property string $redirect_uri
|
||||
* @property integer $account_id
|
||||
* @property bool $is_trusted
|
||||
* @property integer $created_at
|
||||
*
|
||||
* Отношения:
|
||||
* @property Account $account
|
||||
* @property OauthSession[] $sessions
|
||||
*/
|
||||
class OauthClient extends ActiveRecord {
|
||||
|
||||
public static function tableName() {
|
||||
return '{{%oauth_clients}}';
|
||||
}
|
||||
|
||||
public function rules() {
|
||||
return [
|
||||
[['id'], 'required', 'when' => function(self $model) {
|
||||
return $model->isNewRecord;
|
||||
}],
|
||||
[['id'], 'unique', 'when' => function(self $model) {
|
||||
return $model->isNewRecord;
|
||||
}],
|
||||
[['name', 'description'], 'required'],
|
||||
[['name', 'description'], 'string', 'max' => 255],
|
||||
];
|
||||
}
|
||||
|
||||
public function getAccount() {
|
||||
return $this->hasOne(Account::class, ['id' => 'account_id']);
|
||||
}
|
||||
|
||||
public function getSessions() {
|
||||
return $this->hasMany(OauthSession::class, ['client_id' => 'id']);
|
||||
}
|
||||
|
||||
}
|
17
common/models/OauthScope.php
Normal file
17
common/models/OauthScope.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
namespace common\models;
|
||||
|
||||
use Yii;
|
||||
use yii\db\ActiveRecord;
|
||||
|
||||
/**
|
||||
* Поля:
|
||||
* @property string $id
|
||||
*/
|
||||
class OauthScope extends ActiveRecord {
|
||||
|
||||
public static function tableName() {
|
||||
return '{{%oauth_scopes}}';
|
||||
}
|
||||
|
||||
}
|
54
common/models/OauthSession.php
Normal file
54
common/models/OauthSession.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
namespace common\models;
|
||||
|
||||
use common\components\redis\Set;
|
||||
use Yii;
|
||||
use yii\db\ActiveRecord;
|
||||
|
||||
/**
|
||||
* Поля:
|
||||
* @property integer $id
|
||||
* @property string $owner_type
|
||||
* @property string $owner_id
|
||||
* @property string $client_id
|
||||
* @property string $client_redirect_uri
|
||||
*
|
||||
* Отношения
|
||||
* @property OauthAccessToken[] $accessTokens
|
||||
* @property OauthClient $client
|
||||
* @property Account $account
|
||||
* @property Set $scopes
|
||||
*/
|
||||
class OauthSession extends ActiveRecord {
|
||||
|
||||
public static function tableName() {
|
||||
return '{{%oauth_sessions}}';
|
||||
}
|
||||
|
||||
public function getOauthAccessTokens() {
|
||||
return $this->hasMany(OauthAccessToken::class, ['session_id' => 'id']);
|
||||
}
|
||||
|
||||
public function getClient() {
|
||||
return $this->hasOne(OauthClient::class, ['id' => 'client_id']);
|
||||
}
|
||||
|
||||
public function getAccount() {
|
||||
return $this->hasOne(Account::class, ['id' => 'owner_id']);
|
||||
}
|
||||
|
||||
public function getScopes() {
|
||||
return new Set($this->getDb()->getSchema()->getRawTableName($this->tableName()), $this->id, 'scopes');
|
||||
}
|
||||
|
||||
public function beforeDelete() {
|
||||
if (!$result = parent::beforeDelete()) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$this->getScopes()->delete();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
@ -18,14 +18,19 @@
|
||||
"yiisoft/yii2": ">=2.0.6",
|
||||
"yiisoft/yii2-bootstrap": "*",
|
||||
"yiisoft/yii2-swiftmailer": "*",
|
||||
"ramsey/uuid": "^3.1"
|
||||
"ramsey/uuid": "^3.1",
|
||||
"league/oauth2-server": "~4.1.5",
|
||||
"yiisoft/yii2-redis": "~2.0.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"yiisoft/yii2-codeception": "*",
|
||||
"yiisoft/yii2-debug": "*",
|
||||
"yiisoft/yii2-gii": "*",
|
||||
"yiisoft/yii2-faker": "*",
|
||||
"flow/jsonpath": "^0.3.1"
|
||||
"flow/jsonpath": "^0.3.1",
|
||||
"codeception/codeception": "2.0.*",
|
||||
"codeception/specify": "*",
|
||||
"codeception/verify": "*"
|
||||
},
|
||||
"config": {
|
||||
"process-timeout": 1800
|
||||
|
@ -11,11 +11,34 @@ class Migration extends YiiMigration {
|
||||
public function getTableOptions($engine = 'InnoDB') {
|
||||
$tableOptions = null;
|
||||
if ($this->db->driverName === 'mysql') {
|
||||
// http://stackoverflow.com/questions/766809/whats-the-difference-between-utf8-general-ci-and-utf8-unicode-ci
|
||||
$tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=' . $engine;
|
||||
}
|
||||
|
||||
return $tableOptions;
|
||||
}
|
||||
|
||||
protected function primary(...$columns) {
|
||||
switch (count($columns)) {
|
||||
case 0:
|
||||
$key = '';
|
||||
break;
|
||||
case 1:
|
||||
$key = $columns[0];
|
||||
break;
|
||||
default:
|
||||
$key = $this->buildKey($columns);
|
||||
}
|
||||
|
||||
return " PRIMARY KEY ($key) ";
|
||||
}
|
||||
|
||||
private function buildKey(array $columns) {
|
||||
$key = '';
|
||||
foreach ($columns as $i => $column) {
|
||||
$key .= $i == count($columns) ? $column : "$column,";
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
}
|
||||
|
77
console/migrations/m160201_055928_oauth.php
Normal file
77
console/migrations/m160201_055928_oauth.php
Normal file
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
use console\db\Migration;
|
||||
|
||||
class m160201_055928_oauth extends Migration {
|
||||
|
||||
public function safeUp() {
|
||||
$this->createTable('{{%oauth_clients}}', [
|
||||
'id' => $this->string(64),
|
||||
'secret' => $this->string()->notNull(),
|
||||
'name' => $this->string()->notNull(),
|
||||
'description' => $this->string(),
|
||||
'redirect_uri' => $this->string()->notNull(),
|
||||
'account_id' => $this->getDb()->getTableSchema('{{%accounts}}')->getColumn('id')->dbType,
|
||||
'is_trusted' => $this->boolean()->defaultValue(false)->notNull(),
|
||||
'created_at' => $this->integer()->notNull(),
|
||||
$this->primary('id'),
|
||||
], $this->tableOptions);
|
||||
|
||||
$this->createTable('{{%oauth_scopes}}', [
|
||||
'id' => $this->string(64),
|
||||
$this->primary('id'),
|
||||
], $this->tableOptions);
|
||||
|
||||
$this->createTable('{{%oauth_sessions}}', [
|
||||
'id' => $this->primaryKey(),
|
||||
'owner_type' => $this->string()->notNull(),
|
||||
'owner_id' => $this->string()->notNull(),
|
||||
'client_id' => $this->getDb()->getTableSchema('{{%oauth_clients}}')->getColumn('id')->dbType,
|
||||
'client_redirect_uri' => $this->string(),
|
||||
], $this->tableOptions);
|
||||
|
||||
$this->createTable('{{%oauth_access_tokens}}', [
|
||||
'access_token' => $this->string(64),
|
||||
'session_id' => $this->getDb()->getTableSchema('{{%oauth_sessions}}')->getColumn('id')->dbType,
|
||||
'expire_time' => $this->integer()->notNull(),
|
||||
$this->primary('access_token'),
|
||||
], $this->tableOptions);
|
||||
|
||||
$this->addForeignKey(
|
||||
'FK_oauth_client_to_accounts',
|
||||
'{{%oauth_clients}}',
|
||||
'account_id',
|
||||
'{{%accounts}}',
|
||||
'id',
|
||||
'CASCADE'
|
||||
);
|
||||
|
||||
$this->addForeignKey(
|
||||
'FK_oauth_session_to_client',
|
||||
'{{%oauth_sessions}}',
|
||||
'client_id',
|
||||
'{{%oauth_clients}}',
|
||||
'id',
|
||||
'CASCADE',
|
||||
'CASCADE'
|
||||
);
|
||||
|
||||
$this->addForeignKey(
|
||||
'FK_oauth_access_toke_to_oauth_session',
|
||||
'{{%oauth_access_tokens}}',
|
||||
'session_id',
|
||||
'{{%oauth_sessions}}',
|
||||
'id',
|
||||
'CASCADE',
|
||||
'SET NULL'
|
||||
);
|
||||
}
|
||||
|
||||
public function safeDown() {
|
||||
$this->dropTable('{{%oauth_access_tokens}}');
|
||||
$this->dropTable('{{%oauth_sessions}}');
|
||||
$this->dropTable('{{%oauth_scopes}}');
|
||||
$this->dropTable('{{%oauth_clients}}');
|
||||
}
|
||||
|
||||
}
|
@ -6,6 +6,9 @@ $config = [
|
||||
// !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
|
||||
'cookieValidationKey' => '',
|
||||
],
|
||||
'reCaptcha' => [
|
||||
'secret' => '',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
|
@ -12,5 +12,11 @@ return [
|
||||
// for the mailer to send real emails.
|
||||
'useFileTransport' => true,
|
||||
],
|
||||
'redis' => [
|
||||
'hostname' => 'localhost',
|
||||
'password' => null,
|
||||
'port' => 6379,
|
||||
'database' => 0,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
12
environments/prod/api/config/main-local.php
Normal file
12
environments/prod/api/config/main-local.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
return [
|
||||
'components' => [
|
||||
'request' => [
|
||||
// !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
|
||||
'cookieValidationKey' => '',
|
||||
],
|
||||
'reCaptcha' => [
|
||||
'secret' => '',
|
||||
],
|
||||
],
|
||||
];
|
3
environments/prod/api/config/params-local.php
Normal file
3
environments/prod/api/config/params-local.php
Normal file
@ -0,0 +1,3 @@
|
||||
<?php
|
||||
return [
|
||||
];
|
18
environments/prod/api/web/index.php
Normal file
18
environments/prod/api/web/index.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
defined('YII_DEBUG') or define('YII_DEBUG', false);
|
||||
defined('YII_ENV') or define('YII_ENV', 'prod');
|
||||
|
||||
require(__DIR__ . '/../../vendor/autoload.php');
|
||||
require(__DIR__ . '/../../vendor/yiisoft/yii2/Yii.php');
|
||||
require(__DIR__ . '/../../common/config/bootstrap.php');
|
||||
require(__DIR__ . '/../config/bootstrap.php');
|
||||
|
||||
$config = yii\helpers\ArrayHelper::merge(
|
||||
require(__DIR__ . '/../../common/config/main.php'),
|
||||
require(__DIR__ . '/../../common/config/main-local.php'),
|
||||
require(__DIR__ . '/../config/main.php'),
|
||||
require(__DIR__ . '/../config/main-local.php')
|
||||
);
|
||||
|
||||
$application = new yii\web\Application($config);
|
||||
$application->run();
|
@ -6,5 +6,11 @@ return [
|
||||
'username' => 'root',
|
||||
'password' => '',
|
||||
],
|
||||
'redis' => [
|
||||
'hostname' => 'localhost',
|
||||
'password' => null,
|
||||
'port' => 6379,
|
||||
'database' => 0,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
@ -21,3 +21,6 @@ $_SERVER['SERVER_NAME'] = parse_url(\Codeception\Configuration::config()['confi
|
||||
$_SERVER['SERVER_PORT'] = parse_url(\Codeception\Configuration::config()['config']['test_entry_url'], PHP_URL_PORT) ?: '80';
|
||||
|
||||
Yii::setAlias('@tests', dirname(dirname(__DIR__)));
|
||||
|
||||
// disable deep cloning of properties inside specify block
|
||||
\Codeception\Specify\Config::setDeepClone(false);
|
||||
|
21
tests/codeception/api/_pages/OauthRoute.php
Normal file
21
tests/codeception/api/_pages/OauthRoute.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
namespace tests\codeception\api\_pages;
|
||||
|
||||
use yii\codeception\BasePage;
|
||||
|
||||
/**
|
||||
* @property \tests\codeception\api\FunctionalTester $actor
|
||||
*/
|
||||
class OauthRoute extends BasePage {
|
||||
|
||||
public function validate($queryParams) {
|
||||
$this->route = ['oauth/validate'];
|
||||
$this->actor->sendGET($this->getUrl($queryParams));
|
||||
}
|
||||
|
||||
public function complete($queryParams = [], $postParams = []) {
|
||||
$this->route = ['oauth/complete'];
|
||||
$this->actor->sendPOST($this->getUrl($queryParams), $postParams);
|
||||
}
|
||||
|
||||
}
|
@ -13,6 +13,11 @@ modules:
|
||||
- Yii2
|
||||
- tests\codeception\common\_support\FixtureHelper
|
||||
- REST
|
||||
- Redis
|
||||
config:
|
||||
Yii2:
|
||||
configFile: '../config/api/functional.php'
|
||||
Redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
database: 1
|
||||
|
@ -1,47 +0,0 @@
|
||||
<?php
|
||||
use tests\codeception\api\FunctionalTester;
|
||||
use tests\codeception\api\_pages\ContactPage;
|
||||
|
||||
/* @var $scenario Codeception\Scenario */
|
||||
|
||||
$I = new FunctionalTester($scenario);
|
||||
$I->wantTo('ensure that contact works');
|
||||
|
||||
$contactPage = ContactPage::openBy($I);
|
||||
|
||||
$I->see('Contact', 'h1');
|
||||
|
||||
$I->amGoingTo('submit contact form with no data');
|
||||
$contactPage->submit([]);
|
||||
$I->expectTo('see validations errors');
|
||||
$I->see('Contact', 'h1');
|
||||
$I->see('Name cannot be blank', '.help-block');
|
||||
$I->see('Email cannot be blank', '.help-block');
|
||||
$I->see('Subject cannot be blank', '.help-block');
|
||||
$I->see('Body cannot be blank', '.help-block');
|
||||
$I->see('The verification code is incorrect', '.help-block');
|
||||
|
||||
$I->amGoingTo('submit contact form with not correct email');
|
||||
$contactPage->submit([
|
||||
'name' => 'tester',
|
||||
'email' => 'tester.email',
|
||||
'subject' => 'test subject',
|
||||
'body' => 'test content',
|
||||
'verifyCode' => 'testme',
|
||||
]);
|
||||
$I->expectTo('see that email adress is wrong');
|
||||
$I->dontSee('Name cannot be blank', '.help-block');
|
||||
$I->see('Email is not a valid email address.', '.help-block');
|
||||
$I->dontSee('Subject cannot be blank', '.help-block');
|
||||
$I->dontSee('Body cannot be blank', '.help-block');
|
||||
$I->dontSee('The verification code is incorrect', '.help-block');
|
||||
|
||||
$I->amGoingTo('submit contact form with correct data');
|
||||
$contactPage->submit([
|
||||
'name' => 'tester',
|
||||
'email' => 'tester@example.com',
|
||||
'subject' => 'test subject',
|
||||
'body' => 'test content',
|
||||
'verifyCode' => 'testme',
|
||||
]);
|
||||
$I->see('Thank you for contacting us. We will respond to you as soon as possible.');
|
294
tests/codeception/api/functional/OauthCest.php
Normal file
294
tests/codeception/api/functional/OauthCest.php
Normal file
@ -0,0 +1,294 @@
|
||||
<?php
|
||||
namespace tests\codeception\api;
|
||||
|
||||
use tests\codeception\api\_pages\OauthRoute;
|
||||
use tests\codeception\api\functional\_steps\AccountSteps;
|
||||
|
||||
class OauthCest {
|
||||
|
||||
/**
|
||||
* @var OauthRoute
|
||||
*/
|
||||
private $route;
|
||||
|
||||
public function _before(FunctionalTester $I) {
|
||||
$this->route = new OauthRoute($I);
|
||||
}
|
||||
|
||||
public function testValidateRequest(FunctionalTester $I) {
|
||||
$this->testOauthParamsValidation($I, 'validate');
|
||||
|
||||
$I->wantTo('validate and obtain information about new auth request');
|
||||
$this->route->validate($this->buildQueryParams(
|
||||
'ely',
|
||||
'http://ely.by',
|
||||
'code',
|
||||
[
|
||||
'minecraft_server_session'
|
||||
],
|
||||
'test-state'
|
||||
));
|
||||
$I->canSeeResponseCodeIs(200);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => true,
|
||||
'oAuth' => [
|
||||
'client_id' => 'ely',
|
||||
'redirect_uri' => 'http://ely.by',
|
||||
'response_type' => 'code',
|
||||
'scope' => 'minecraft_server_session',
|
||||
'state' => 'test-state',
|
||||
],
|
||||
'client' => [
|
||||
'id' => 'ely',
|
||||
'name' => 'Ely.by',
|
||||
'description' => 'Всем знакомое елуби',
|
||||
],
|
||||
'session' => [
|
||||
'scopes' => [
|
||||
'minecraft_server_session',
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function testValidateWithDescriptionReplaceRequest(FunctionalTester $I) {
|
||||
$I->wantTo('validate and get information with description replacement');
|
||||
$this->route->validate($this->buildQueryParams(
|
||||
'ely',
|
||||
'http://ely.by',
|
||||
'code',
|
||||
null,
|
||||
null,
|
||||
[
|
||||
'description' => 'all familiar eliby',
|
||||
]
|
||||
));
|
||||
$I->canSeeResponseCodeIs(200);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'client' => [
|
||||
'description' => 'all familiar eliby',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function testCompleteValidationAction($I, $scenario) {
|
||||
$I = new AccountSteps($scenario);
|
||||
$I->loggedInAsActiveAccount();
|
||||
$I->wantTo('validate all oAuth params on complete request');
|
||||
$this->testOauthParamsValidation($I, 'complete');
|
||||
}
|
||||
|
||||
public function testCompleteActionOnWrongConditions($I, $scenario) {
|
||||
$I = new AccountSteps($scenario);
|
||||
$I->loggedInAsActiveAccount();
|
||||
|
||||
$I->wantTo('get accept_required if I dom\'t require any scope, but this is first time request');
|
||||
$I->cleanupRedis();
|
||||
$this->route->complete($this->buildQueryParams(
|
||||
'ely',
|
||||
'http://ely.by',
|
||||
'code'
|
||||
));
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => false,
|
||||
'error' => 'accept_required',
|
||||
'parameter' => '',
|
||||
'statusCode' => 401,
|
||||
]);
|
||||
|
||||
$I->wantTo('get accept_required if I require some scopes on first time');
|
||||
$this->route->complete($this->buildQueryParams(
|
||||
'ely',
|
||||
'http://ely.by',
|
||||
'code',
|
||||
['minecraft_server_session']
|
||||
));
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => false,
|
||||
'error' => 'accept_required',
|
||||
'parameter' => '',
|
||||
'statusCode' => 401,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testCompleteActionSuccess($I, $scenario) {
|
||||
$I = new AccountSteps($scenario);
|
||||
$I->loggedInAsActiveAccount();
|
||||
|
||||
$I->wantTo('get auth code if I require some scope and pass accept field');
|
||||
$this->route->complete($this->buildQueryParams(
|
||||
'ely',
|
||||
'http://ely.by',
|
||||
'code',
|
||||
['minecraft_server_session']
|
||||
), ['accept' => true]);
|
||||
$I->canSeeResponseCodeIs(200);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => true,
|
||||
]);
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.redirectUri');
|
||||
|
||||
$I->wantTo('get auth code if I don\'t require any scope and don\'t pass accept field, but previously have ' .
|
||||
'successful request');
|
||||
$this->route->complete($this->buildQueryParams(
|
||||
'ely',
|
||||
'http://ely.by',
|
||||
'code'
|
||||
));
|
||||
$I->canSeeResponseCodeIs(200);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => true,
|
||||
]);
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.redirectUri');
|
||||
|
||||
$I->wantTo('get auth code if I require some scopes and don\'t pass accept field, but previously have successful ' .
|
||||
'request with same scopes');
|
||||
$this->route->complete($this->buildQueryParams(
|
||||
'ely',
|
||||
'http://ely.by',
|
||||
'code',
|
||||
['minecraft_server_session']
|
||||
));
|
||||
$I->canSeeResponseCodeIs(200);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => true,
|
||||
]);
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.redirectUri');
|
||||
}
|
||||
|
||||
public function testAcceptRequiredOnNewScope($I, $scenario) {
|
||||
$I = new AccountSteps($scenario);
|
||||
$I->loggedInAsActiveAccount();
|
||||
$I->wantTo('get accept_required if I have previous successful request, but now require some new scope');
|
||||
$this->route->complete($this->buildQueryParams(
|
||||
'ely',
|
||||
'http://ely.by',
|
||||
'code',
|
||||
['minecraft_server_session']
|
||||
), ['accept' => true]);
|
||||
$this->route->complete($this->buildQueryParams(
|
||||
'ely',
|
||||
'http://ely.by',
|
||||
'code',
|
||||
['minecraft_server_session', 'change_skin']
|
||||
));
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => false,
|
||||
'error' => 'accept_required',
|
||||
'parameter' => '',
|
||||
'statusCode' => 401,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testCompleteActionWithDismissState($I, $scenario) {
|
||||
$I = new AccountSteps($scenario);
|
||||
$I->loggedInAsActiveAccount();
|
||||
$I->wantTo('get access_denied error if I pass accept in false state');
|
||||
$this->route->complete($this->buildQueryParams(
|
||||
'ely',
|
||||
'http://ely.by',
|
||||
'code',
|
||||
['minecraft_server_session']
|
||||
), ['accept' => false]);
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => false,
|
||||
'error' => 'access_denied',
|
||||
'parameter' => '',
|
||||
'statusCode' => 401,
|
||||
]);
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.redirectUri');
|
||||
}
|
||||
|
||||
private function buildQueryParams(
|
||||
$clientId = null,
|
||||
$redirectUri = null,
|
||||
$responseType = null,
|
||||
$scopes = [],
|
||||
$state = null,
|
||||
$customData = []
|
||||
) {
|
||||
$params = $customData;
|
||||
if ($clientId !== null) {
|
||||
$params['client_id'] = $clientId;
|
||||
}
|
||||
|
||||
if ($redirectUri !== null) {
|
||||
$params['redirect_uri'] = $redirectUri;
|
||||
}
|
||||
|
||||
if ($responseType !== null) {
|
||||
$params['response_type'] = $responseType;
|
||||
}
|
||||
|
||||
if ($state !== null) {
|
||||
$params['state'] = $state;
|
||||
}
|
||||
|
||||
if (!empty($scopes)) {
|
||||
if (is_array($scopes)) {
|
||||
$scopes = implode(',', $scopes);
|
||||
}
|
||||
|
||||
$params['scope'] = $scopes;
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
private function testOauthParamsValidation(FunctionalTester $I, $action) {
|
||||
$I->wantTo('check behavior on invalid request without one or few params');
|
||||
$this->route->$action($this->buildQueryParams());
|
||||
$I->canSeeResponseCodeIs(400);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => false,
|
||||
'error' => 'invalid_request',
|
||||
'parameter' => 'client_id',
|
||||
'statusCode' => 400,
|
||||
]);
|
||||
|
||||
$I->wantTo('check behavior on invalid client id');
|
||||
$this->route->$action($this->buildQueryParams('non-exists-client', 'http://some-resource.by', 'code'));
|
||||
$I->canSeeResponseCodeIs(401);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => false,
|
||||
'error' => 'invalid_client',
|
||||
'statusCode' => 401,
|
||||
]);
|
||||
|
||||
$I->wantTo('check behavior on invalid response type');
|
||||
$this->route->$action($this->buildQueryParams('ely', 'http://ely.by', 'kitty'));
|
||||
$I->canSeeResponseCodeIs(400);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => false,
|
||||
'error' => 'unsupported_response_type',
|
||||
'parameter' => 'kitty',
|
||||
'statusCode' => 400,
|
||||
]);
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.redirectUri');
|
||||
|
||||
$I->wantTo('check behavior on some invalid scopes');
|
||||
$this->route->$action($this->buildQueryParams('ely', 'http://ely.by', 'code', [
|
||||
'minecraft_server_session',
|
||||
'some_wrong_scope',
|
||||
]));
|
||||
$I->canSeeResponseCodeIs(400);
|
||||
$I->canSeeResponseIsJson();
|
||||
$I->canSeeResponseContainsJson([
|
||||
'success' => false,
|
||||
'error' => 'invalid_scope',
|
||||
'parameter' => 'some_wrong_scope',
|
||||
'statusCode' => 400,
|
||||
]);
|
||||
$I->canSeeResponseJsonMatchesJsonPath('$.redirectUri');
|
||||
}
|
||||
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
<?php
|
||||
|
||||
new yii\web\Application(require(dirname(dirname(__DIR__)) . '/config/api/functional.php'));
|
||||
|
||||
\Codeception\Util\Autoload::registerSuffix('Steps', __DIR__ . DIRECTORY_SEPARATOR);
|
||||
|
16
tests/codeception/api/functional/_steps/AccountSteps.php
Normal file
16
tests/codeception/api/functional/_steps/AccountSteps.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
namespace tests\codeception\api\functional\_steps;
|
||||
|
||||
use tests\codeception\api\_pages\LoginRoute;
|
||||
use tests\codeception\api\FunctionalTester;
|
||||
|
||||
class AccountSteps extends FunctionalTester {
|
||||
|
||||
public function loggedInAsActiveAccount() {
|
||||
$I = $this;
|
||||
$route = new LoginRoute($I);
|
||||
$route->login('Admin', 'password_0');
|
||||
$I->canSeeResponseIsJson();
|
||||
}
|
||||
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace tests\codeception\api\unit\models;
|
||||
|
||||
use Yii;
|
||||
use tests\codeception\api\unit\TestCase;
|
||||
use api\models\ContactForm;
|
||||
|
||||
class ContactFormTest extends TestCase
|
||||
{
|
||||
|
||||
use \Codeception\Specify;
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
Yii::$app->mailer->fileTransportCallback = function ($mailer, $message) {
|
||||
return 'testing_message.eml';
|
||||
};
|
||||
}
|
||||
|
||||
protected function tearDown()
|
||||
{
|
||||
unlink($this->getMessageFile());
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testContact()
|
||||
{
|
||||
$model = new ContactForm();
|
||||
|
||||
$model->attributes = [
|
||||
'name' => 'Tester',
|
||||
'email' => 'tester@example.com',
|
||||
'subject' => 'very important letter subject',
|
||||
'body' => 'body of current message',
|
||||
];
|
||||
|
||||
$model->sendEmail('admin@example.com');
|
||||
|
||||
$this->specify('email should be send', function () {
|
||||
expect('email file should exist', file_exists($this->getMessageFile()))->true();
|
||||
});
|
||||
|
||||
$this->specify('message should contain correct data', function () use ($model) {
|
||||
$emailMessage = file_get_contents($this->getMessageFile());
|
||||
|
||||
expect('email should contain user name', $emailMessage)->contains($model->name);
|
||||
expect('email should contain sender email', $emailMessage)->contains($model->email);
|
||||
expect('email should contain subject', $emailMessage)->contains($model->subject);
|
||||
expect('email should contain body', $emailMessage)->contains($model->body);
|
||||
});
|
||||
}
|
||||
|
||||
private function getMessageFile()
|
||||
{
|
||||
return Yii::getAlias(Yii::$app->mailer->fileTransportPath) . '/testing_message.eml';
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace tests\codeception\api\models;
|
||||
|
||||
use Yii;
|
||||
use tests\codeception\api\unit\DbTestCase;
|
||||
use api\models\PasswordResetRequestForm;
|
||||
use tests\codeception\common\fixtures\UserFixture;
|
||||
use common\models\Account;
|
||||
use Codeception\Specify;
|
||||
|
||||
class PasswordResetRequestFormTest extends DbTestCase
|
||||
{
|
||||
use Specify;
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Yii::$app->mailer->fileTransportCallback = function ($mailer, $message) {
|
||||
return 'testing_message.eml';
|
||||
};
|
||||
}
|
||||
|
||||
protected function tearDown()
|
||||
{
|
||||
@unlink($this->getMessageFile());
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testSendEmailWrongUser()
|
||||
{
|
||||
$this->specify('no user with such email, message should not be sent', function () {
|
||||
|
||||
$model = new PasswordResetRequestForm();
|
||||
$model->email = 'not-existing-email@example.com';
|
||||
|
||||
expect('email not sent', $model->sendEmail())->false();
|
||||
|
||||
});
|
||||
|
||||
$this->specify('user is not active, message should not be sent', function () {
|
||||
|
||||
$model = new PasswordResetRequestForm();
|
||||
$model->email = $this->user[1]['email'];
|
||||
|
||||
expect('email not sent', $model->sendEmail())->false();
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public function testSendEmailCorrectUser()
|
||||
{
|
||||
$model = new PasswordResetRequestForm();
|
||||
$model->email = $this->user[0]['email'];
|
||||
$user = Account::findOne(['password_reset_token' => $this->user[0]['password_reset_token']]);
|
||||
|
||||
expect('email sent', $model->sendEmail())->true();
|
||||
expect('user has valid token', $user->password_reset_token)->notNull();
|
||||
|
||||
$this->specify('message has correct format', function () use ($model) {
|
||||
|
||||
expect('message file exists', file_exists($this->getMessageFile()))->true();
|
||||
|
||||
$message = file_get_contents($this->getMessageFile());
|
||||
expect('message "from" is correct', $message)->contains(Yii::$app->params['supportEmail']);
|
||||
expect('message "to" is correct', $message)->contains($model->email);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public function fixtures()
|
||||
{
|
||||
return [
|
||||
'user' => [
|
||||
'class' => UserFixture::className(),
|
||||
'dataFile' => '@tests/codeception/api/unit/fixtures/data/models/user.php'
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function getMessageFile()
|
||||
{
|
||||
return Yii::getAlias(Yii::$app->mailer->fileTransportPath) . '/testing_message.eml';
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace tests\codeception\api\unit\models;
|
||||
|
||||
use tests\codeception\api\unit\DbTestCase;
|
||||
use tests\codeception\common\fixtures\UserFixture;
|
||||
use api\models\ResetPasswordForm;
|
||||
|
||||
class ResetPasswordFormTest extends DbTestCase
|
||||
{
|
||||
|
||||
/**
|
||||
* @expectedException \yii\base\InvalidParamException
|
||||
*/
|
||||
public function testResetWrongToken()
|
||||
{
|
||||
new ResetPasswordForm('notexistingtoken_1391882543');
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \yii\base\InvalidParamException
|
||||
*/
|
||||
public function testResetEmptyToken()
|
||||
{
|
||||
new ResetPasswordForm('');
|
||||
}
|
||||
|
||||
public function testResetCorrectToken()
|
||||
{
|
||||
$form = new ResetPasswordForm($this->user[0]['password_reset_token']);
|
||||
expect('password should be resetted', $form->resetPassword())->true();
|
||||
}
|
||||
|
||||
public function fixtures()
|
||||
{
|
||||
return [
|
||||
'user' => [
|
||||
'class' => UserFixture::className(),
|
||||
'dataFile' => '@tests/codeception/api/unit/fixtures/data/models/user.php'
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@ -4,6 +4,9 @@ namespace tests\codeception\common\_support;
|
||||
use Codeception\Module;
|
||||
use tests\codeception\common\fixtures\AccountFixture;
|
||||
use tests\codeception\common\fixtures\EmailActivationFixture;
|
||||
use tests\codeception\common\fixtures\OauthClientFixture;
|
||||
use tests\codeception\common\fixtures\OauthScopeFixture;
|
||||
use tests\codeception\common\fixtures\OauthSessionFixture;
|
||||
use yii\test\FixtureTrait;
|
||||
use yii\test\InitDbFixture;
|
||||
|
||||
@ -26,35 +29,20 @@ class FixtureHelper extends Module {
|
||||
getFixture as protected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called before any suite tests run. Loads User fixture login user
|
||||
* to use in functional tests.
|
||||
*
|
||||
* @param array $settings
|
||||
*/
|
||||
public function _beforeSuite($settings = []) {
|
||||
$this->loadFixtures();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method is called after all suite tests run
|
||||
*/
|
||||
public function _afterSuite() {
|
||||
$this->unloadFixtures();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function globalFixtures() {
|
||||
return [
|
||||
InitDbFixture::className(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function fixtures() {
|
||||
return [
|
||||
'accounts' => [
|
||||
@ -65,6 +53,19 @@ class FixtureHelper extends Module {
|
||||
'class' => EmailActivationFixture::class,
|
||||
'dataFile' => '@tests/codeception/common/fixtures/data/email-activations.php',
|
||||
],
|
||||
'oauthClients' => [
|
||||
'class' => OauthClientFixture::class,
|
||||
'dataFile' => '@tests/codeception/common/fixtures/data/oauth-clients.php',
|
||||
],
|
||||
'oauthScopes' => [
|
||||
'class' => OauthScopeFixture::class,
|
||||
'dataFile' => '@tests/codeception/common/fixtures/data/oauth-scopes.php',
|
||||
],
|
||||
'oauthSessions' => [
|
||||
'class' => OauthSessionFixture::class,
|
||||
'dataFile' => '@tests/codeception/common/fixtures/data/oauth-sessions.php',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
15
tests/codeception/common/fixtures/OauthClientFixture.php
Normal file
15
tests/codeception/common/fixtures/OauthClientFixture.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
namespace tests\codeception\common\fixtures;
|
||||
|
||||
use common\models\OauthClient;
|
||||
use yii\test\ActiveFixture;
|
||||
|
||||
class OauthClientFixture extends ActiveFixture {
|
||||
|
||||
public $modelClass = OauthClient::class;
|
||||
|
||||
public $depends = [
|
||||
AccountFixture::class,
|
||||
];
|
||||
|
||||
}
|
11
tests/codeception/common/fixtures/OauthScopeFixture.php
Normal file
11
tests/codeception/common/fixtures/OauthScopeFixture.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
namespace tests\codeception\common\fixtures;
|
||||
|
||||
use common\models\OauthScope;
|
||||
use yii\test\ActiveFixture;
|
||||
|
||||
class OauthScopeFixture extends ActiveFixture {
|
||||
|
||||
public $modelClass = OauthScope::class;
|
||||
|
||||
}
|
17
tests/codeception/common/fixtures/OauthSessionFixture.php
Normal file
17
tests/codeception/common/fixtures/OauthSessionFixture.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
namespace tests\codeception\common\fixtures;
|
||||
|
||||
use common\models\OauthScope;
|
||||
use common\models\OauthSession;
|
||||
use yii\test\ActiveFixture;
|
||||
|
||||
class OauthSessionFixture extends ActiveFixture {
|
||||
|
||||
public $modelClass = OauthSession::class;
|
||||
|
||||
public $depends = [
|
||||
OauthClientFixture::class,
|
||||
AccountFixture::class,
|
||||
];
|
||||
|
||||
}
|
23
tests/codeception/common/fixtures/data/oauth-clients.php
Normal file
23
tests/codeception/common/fixtures/data/oauth-clients.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
return [
|
||||
'ely' => [
|
||||
'id' => 'ely',
|
||||
'secret' => 'ZuM1vGchJz-9_UZ5HC3H3Z9Hg5PzdbkM',
|
||||
'name' => 'Ely.by',
|
||||
'description' => 'Всем знакомое елуби',
|
||||
'redirect_uri' => 'http://ely.by',
|
||||
'account_id' => NULL,
|
||||
'is_trusted' => 0,
|
||||
'created_at' => 1455309271,
|
||||
],
|
||||
'tlauncher' => [
|
||||
'id' => 'tlauncher',
|
||||
'secret' => 'HsX-xXzdGiz3mcsqeEvrKHF47sqiaX94',
|
||||
'name' => 'TLauncher',
|
||||
'description' => 'Лучший альтернативный лаунчер для Minecraft с большим количеством версий и их модификаций, а также возмоностью входа как с лицензионным аккаунтом, так и без него.',
|
||||
'redirect_uri' => '',
|
||||
'account_id' => NULL,
|
||||
'is_trusted' => 0,
|
||||
'created_at' => 1455318468,
|
||||
],
|
||||
];
|
9
tests/codeception/common/fixtures/data/oauth-scopes.php
Normal file
9
tests/codeception/common/fixtures/data/oauth-scopes.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
return [
|
||||
'minecraft_server_session' => [
|
||||
'id' => 'minecraft_server_session',
|
||||
],
|
||||
'change_skin' => [
|
||||
'id' => 'change_skin',
|
||||
],
|
||||
];
|
@ -0,0 +1,3 @@
|
||||
<?php
|
||||
return [
|
||||
];
|
@ -22,5 +22,8 @@ return [
|
||||
'urlManager' => [
|
||||
'showScriptName' => true,
|
||||
],
|
||||
'redis' => [
|
||||
'database' => 1,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
Loading…
Reference in New Issue
Block a user