Upgrade project to PHP 8.3, add PHPStan, upgrade almost every dependency (#36)

* start updating to PHP 8.3

* taking off!

Co-authored-by: ErickSkrauch <erickskrauch@yandex.ru>
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>

* dropped this

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>

* migrate to symfonymailer

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>

* this is so stupid 😭

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>

* ah, free, at last.

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>

* oh, Gabriel.

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>

* now dawns thy reckoning.

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>

* and thy gore shall GLISTEN before the temples of man.

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>

* creature of steel.

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>

* my gratitude upon thee for my freedom.

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>

* but the crimes thy kind has committed against humanity

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>

* Upgrade PHP-CS-Fixer and do fix the codebase

* First review round (maybe I have broken something)

* are NOT forgotten.

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>

* Enable parallel PHP-CS-Fixer runner

* PHPStan level 1

* PHPStan level 2

* PHPStan level 3

* PHPStan level 4

* PHPStan level 5

* Levels 6 and 7 takes too much effort. Generate a baseline and fix them eventually

* Resolve TODO's related to the php-mock

* Drastically reduce baseline size with the Rector

* More code modernization with help of the Rector

* Update GitLab CI

---------

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
Co-authored-by: ErickSkrauch <erickskrauch@yandex.ru>
This commit is contained in:
Octol1ttle 2024-12-02 15:10:55 +05:00 committed by GitHub
parent 625250b367
commit 57d492da8a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
356 changed files with 10531 additions and 4761 deletions

14
.gitignore vendored
View File

@ -2,25 +2,23 @@
.idea
# Composer
composer.phar
/vendor
# Mac DS_Store Files
.DS_Store
# npm debug
npm-debug*
# Docker and related files
/docker-compose.yml
/docker-compose.override.yml
/.env
# id_rsa
/id_rsa
# PHP-CS-Fixer
.php_cs
.php_cs.cache
.php-cs-fixer.php
.php-cs-fixer.cache
# PHPStan
phpstan.neon
# Codeception
codeception.yml

View File

@ -1,4 +1,4 @@
image: edbizarro/gitlab-ci-pipeline-php:7.4-alpine
image: thecodingmachine/php:8.3-v4-cli
stages:
- prepare
@ -46,9 +46,8 @@ variables:
# Steps to extend #
###################
.vendorCache:
cache:
key: backend-deps
.vendorCache: &vendorCache
key: composer
paths:
- vendor
policy: pull
@ -59,9 +58,8 @@ variables:
Composer:
stage: prepare
extends:
- .vendorCache
cache:
<<: *vendorCache
policy: pull-push
before_script:
- sudo composer self-update --2
@ -76,15 +74,20 @@ Composer:
PHP-CS-Fixer:
stage: testing
extends:
- .vendorCache
cache:
- *vendorCache
- key: php-cs-fixer-$CI_COMMIT_REF_SLUG
fallback_keys:
- php-cs-fixer-$CI_DEFAULT_BRANCH
paths:
- .php-cs-fixer.cache
when: always
script:
- vendor/bin/php-cs-fixer fix -v --dry-run
Codeception:
stage: testing
extends:
- .vendorCache
cache: *vendorCache
services:
- name: redis:4.0.10-alpine
alias: redis
@ -114,6 +117,24 @@ Codeception:
- wait-for-it "${DB_HOST}:3306" -s -t 0 -- php yii migrate/up --interactive=0
- vendor/bin/codecept run
PHPStan:
stage: testing
image: $PHP_BUILD_IMAGE
cache:
- *vendorCache
- key: phpstan-$CI_COMMIT_REF_SLUG
fallback_keys:
- phpstan-$CI_DEFAULT_BRANCH
paths:
- .phpstan
when: on_success
before_script:
- |
echo -e "includes: [phpstan.dist.neon]\nparameters:\n tmpDir: .phpstan\n reportUnmatchedIgnoredErrors: false" > phpstan.neon
script:
- vendor/bin/codecept build
- vendor/bin/phpstan analyse --no-progress --memory-limit 2G
###############
# Build stage #
###############

28
.php-cs-fixer.dist.php Normal file
View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
/**
* Do not adjust this file for your environment manually.
* Copy it as .php-cs-fixer.php (without .dist part) and then adjust.
*/
$finder = PhpCsFixer\Finder::create()
->in(__DIR__)
->exclude('data')
->exclude('docker')
->exclude('frontend')
->notPath('common/emails/views')
->notPath('common/mail/layouts')
->notPath('#.*/runtime#')
->notPath('autocompletion.php')
->exclude('_output')
->exclude('_generated')
// Remove the line below if your host OS is Windows, because it'll try to fix file, that should be executed
// on Linux environment
->name('yii');
return Ely\CS\Config::create([
'self_accessor' => false,
])
->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect())
->setFinder($finder);

View File

@ -1,17 +0,0 @@
<?php
$finder = \PhpCsFixer\Finder::create()
->in(__DIR__)
->exclude('data')
->exclude('docker')
->exclude('frontend')
->notPath('common/emails/views')
->notPath('common/mail/layouts')
->notPath('/.*\/runtime/')
->notPath('autocompletion.php')
->notPath('/.*\/tests\/_output/')
->notPath('/.*\/tests\/_support\/_generated/')
->name('yii');
return \Ely\CS\Config::create([
'self_accessor' => false,
])->setFinder($finder);

View File

@ -1,4 +1,4 @@
FROM php:7.4.33-fpm-alpine3.16 AS app
FROM php:8.3.12-fpm-alpine3.20 AS app
# bash needed to support wait-for-it script
RUN apk add --update --no-cache git bash patch openssh dcron \
@ -6,7 +6,7 @@ RUN apk add --update --no-cache git bash patch openssh dcron \
-o /usr/local/bin/install-php-extensions \
https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions \
&& chmod +x /usr/local/bin/install-php-extensions \
&& install-php-extensions @composer zip pdo_mysql intl pcntl opcache imagick xdebug-^2 \
&& install-php-extensions @composer zip pdo_mysql intl pcntl opcache imagick xdebug \
# Create cron directory
&& mkdir -p /etc/cron.d \
# Install wait-for-it script
@ -16,7 +16,6 @@ RUN apk add --update --no-cache git bash patch openssh dcron \
# Feature: https://docs.docker.com/develop/develop-images/build_enhancements/#new-docker-build-secret-information
# Track issues: https://github.com/docker/compose/issues/6358, https://github.com/compose-spec/compose-spec/issues/81
COPY ./patches /var/www/html/patches/
COPY ./composer.* /var/www/html/
ARG build_env=prod

View File

@ -3,7 +3,7 @@ actor_suffix: Tester
bootstrap: _bootstrap.php
paths:
tests: tests
log: tests/_output
output: tests/_output
data: tests/_data
helpers: tests/_support
settings:

View File

@ -18,7 +18,7 @@ class ErrorHandler extends \yii\web\ErrorHandler {
return parent::convertExceptionToArray($exception);
}
public function logException($exception) {
public function logException($exception): void {
if ($exception instanceof AuthserverException) {
Yii::error($exception, AuthserverException::class . ':' . $exception->getName());
} elseif ($exception instanceof SessionServerException) {

View File

@ -8,12 +8,9 @@ use DateInterval;
use League\OAuth2\Server\AuthorizationServer;
use yii\base\Component as BaseComponent;
class Component extends BaseComponent {
final class Component extends BaseComponent {
/**
* @var AuthorizationServer
*/
private $_authServer;
private ?AuthorizationServer $_authServer = null;
public function getAuthServer(): AuthorizationServer {
if ($this->_authServer === null) {
@ -39,7 +36,7 @@ class Component extends BaseComponent {
new Repositories\EmptyScopeRepository(),
new Keys\EmptyKey(),
'', // Omit the key because we use our own encryption mechanism
new ResponseTypes\BearerTokenResponse()
new ResponseTypes\BearerTokenResponse(),
);
/** @noinspection PhpUnhandledExceptionInspection */
$authCodeGrant = new Grants\AuthCodeGrant($authCodesRepo, $refreshTokensRepo, new DateInterval('PT10M'));

View File

@ -25,7 +25,7 @@ trait CryptTrait {
protected function decrypt($encryptedData): string {
try {
return Yii::$app->tokens->decryptValue($encryptedData);
} catch (SodiumException | RangeException $e) {
} catch (SodiumException|RangeException $e) {
throw new LogicException($e->getMessage(), 0, $e);
}
}

View File

@ -9,12 +9,12 @@ use League\OAuth2\Server\Entities\Traits\EntityTrait;
use League\OAuth2\Server\Entities\Traits\TokenEntityTrait;
use Yii;
class AccessTokenEntity implements AccessTokenEntityInterface {
final class AccessTokenEntity implements AccessTokenEntityInterface {
use EntityTrait;
use TokenEntityTrait;
public function __toString(): string {
return (string)Yii::$app->tokensFactory->createForOAuthClient($this);
public function toString(): string {
return Yii::$app->tokensFactory->createForOAuthClient($this)->toString();
}
public function setPrivateKey(CryptKeyInterface $privateKey): void {

View File

@ -12,15 +12,18 @@ class ClientEntity implements ClientEntityInterface {
use ClientTrait;
/**
* @var bool
* @param non-empty-string $id
* @param string|string[] $redirectUri
*/
private $isTrusted;
public function __construct(string $id, string $name, $redirectUri, bool $isTrusted) {
public function __construct(
string $id,
string $name,
string|array $redirectUri,
private readonly bool $isTrusted,
) {
$this->identifier = $id;
$this->name = $name;
$this->redirectUri = $redirectUri;
$this->isTrusted = $isTrusted;
}
public function isConfidential(): bool {

View File

@ -10,7 +10,7 @@ class UserEntity implements UserEntityInterface {
use EntityTrait;
public function __construct(int $id) {
$this->identifier = $id;
$this->identifier = (string)$id;
}
}

View File

@ -3,7 +3,7 @@ declare(strict_types=1);
namespace api\components\OAuth2\Events;
use League\Event\AbstractEvent;
use League\OAuth2\Server\EventEmitting\AbstractEvent;
class RequestedRefreshToken extends AbstractEvent {

View File

@ -9,7 +9,9 @@ use api\components\OAuth2\Repositories\PublicScopeRepository;
use DateInterval;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\ScopeEntityInterface;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException;
use League\OAuth2\Server\Grant\AuthCodeGrant as BaseAuthCodeGrant;
use League\OAuth2\Server\RequestEvent;
use Psr\Http\Message\ServerRequestInterface;
@ -22,22 +24,22 @@ class AuthCodeGrant extends BaseAuthCodeGrant {
* @param DateInterval $accessTokenTTL
* @param ClientEntityInterface $client
* @param string|null $userIdentifier
* @param \League\OAuth2\Server\Entities\ScopeEntityInterface[] $scopes
* @param ScopeEntityInterface[] $scopes
*
* @return AccessTokenEntityInterface
* @throws \League\OAuth2\Server\Exception\OAuthServerException
* @throws \League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException
* @throws OAuthServerException
* @throws UniqueTokenIdentifierConstraintViolationException
*/
protected function issueAccessToken(
DateInterval $accessTokenTTL,
ClientEntityInterface $client,
$userIdentifier,
array $scopes = []
?string $userIdentifier,
array $scopes = [],
): AccessTokenEntityInterface {
foreach ($scopes as $i => $scope) {
if ($scope->getIdentifier() === PublicScopeRepository::OFFLINE_ACCESS) {
unset($scopes[$i]);
$this->getEmitter()->emit(new RequestedRefreshToken());
$this->getEmitter()->emit(new RequestedRefreshToken('refresh_token_requested'));
}
}
@ -47,7 +49,7 @@ class AuthCodeGrant extends BaseAuthCodeGrant {
protected function validateRedirectUri(
string $redirectUri,
ClientEntityInterface $client,
ServerRequestInterface $request
ServerRequestInterface $request,
): void {
$allowedRedirectUris = (array)$client->getRedirectUri();
foreach ($allowedRedirectUris as $allowedRedirectUri) {

View File

@ -5,15 +5,17 @@ namespace api\components\OAuth2\Grants;
use api\components\OAuth2\CryptTrait;
use api\components\Tokens\TokenReader;
use Carbon\Carbon;
use Carbon\FactoryImmutable;
use common\models\OauthSession;
use InvalidArgumentException;
use Lcobucci\JWT\ValidationData;
use Lcobucci\JWT\Validation\Constraint\LooseValidAt;
use Lcobucci\JWT\Validation\Validator;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Grant\RefreshTokenGrant as BaseRefreshTokenGrant;
use Psr\Http\Message\ServerRequestInterface;
use Throwable;
use Yii;
class RefreshTokenGrant extends BaseRefreshTokenGrant {
@ -30,7 +32,7 @@ class RefreshTokenGrant extends BaseRefreshTokenGrant {
* @return array
* @throws OAuthServerException
*/
protected function validateOldRefreshToken(ServerRequestInterface $request, $clientId): array {
protected function validateOldRefreshToken(ServerRequestInterface $request, string $clientId): array {
$refreshToken = $this->getRequestParameter('refresh_token', $request);
if ($refreshToken !== null && mb_strlen($refreshToken) === 40) {
return $this->validateLegacyRefreshToken($refreshToken);
@ -41,7 +43,7 @@ class RefreshTokenGrant extends BaseRefreshTokenGrant {
/**
* Currently we're not rotating refresh tokens.
* So we overriding this method to always return null, which means,
* So we're overriding this method to always return null, which means,
* that refresh_token will not be issued.
*
* @param AccessTokenEntityInterface $accessToken
@ -67,8 +69,8 @@ class RefreshTokenGrant extends BaseRefreshTokenGrant {
[
'access_token_id' => $accessTokenId,
'session_id' => $sessionId,
] = json_decode($result, true, 512, JSON_THROW_ON_ERROR);
} catch (\Exception $e) {
] = json_decode((string)$result, true, 512, JSON_THROW_ON_ERROR);
} catch (Throwable $e) {
throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token', $e);
}
@ -89,8 +91,14 @@ class RefreshTokenGrant extends BaseRefreshTokenGrant {
}
/**
* @param string $jwt
* @return array
* @return array{
* client_id: string,
* refresh_token_id?: string,
* access_token_id?: string,
* scopes: list<string>|null,
* user_id: string|null,
* expire_time: int|null,
* }
* @throws OAuthServerException
*/
private function validateAccessToken(string $jwt): array {
@ -104,7 +112,7 @@ class RefreshTokenGrant extends BaseRefreshTokenGrant {
throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token');
}
if (!$token->validate(new ValidationData(Carbon::now()->getTimestamp()))) {
if (!(new Validator())->validate($token, new LooseValidAt(FactoryImmutable::getDefaultInstance()))) {
throw OAuthServerException::invalidRefreshToken('Token has expired');
}

View File

@ -15,4 +15,8 @@ class EmptyKey implements CryptKeyInterface {
return null;
}
public function getKeyContents(): string {
return '';
}
}

View File

@ -22,11 +22,11 @@ class AccessTokenRepository implements AccessTokenRepositoryInterface {
public function getNewToken(
ClientEntityInterface $clientEntity,
array $scopes,
$userIdentifier = null
$userIdentifier = null,
): AccessTokenEntityInterface {
$accessToken = new AccessTokenEntity();
$accessToken->setClient($clientEntity);
array_map([$accessToken, 'addScope'], $scopes);
array_map($accessToken->addScope(...), $scopes);
if ($userIdentifier !== null) {
$accessToken->setUserIdentifier($userIdentifier);
}

View File

@ -21,8 +21,9 @@ class EmptyScopeRepository implements ScopeRepositoryInterface {
public function finalizeScopes(
array $scopes,
$grantType,
ClientEntityInterface $client,
$userIdentifier = null
ClientEntityInterface $clientEntity,
$userIdentifier = null,
?string $authCodeId = null,
): array {
return $scopes;
}

View File

@ -10,11 +10,10 @@ use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\ScopeEntityInterface;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
use Webmozart\Assert\Assert;
class InternalScopeRepository implements ScopeRepositoryInterface {
private const ALLOWED_SCOPES = [
private const array ALLOWED_SCOPES = [
P::CHANGE_ACCOUNT_USERNAME,
P::CHANGE_ACCOUNT_PASSWORD,
P::BLOCK_ACCOUNT,
@ -22,7 +21,7 @@ class InternalScopeRepository implements ScopeRepositoryInterface {
P::ESCAPE_IDENTITY_VERIFICATION,
];
private const PUBLIC_SCOPES_TO_INTERNAL_PERMISSIONS = [
private const array PUBLIC_SCOPES_TO_INTERNAL_PERMISSIONS = [
'internal_account_info' => P::OBTAIN_EXTENDED_ACCOUNT_INFO,
];
@ -35,21 +34,23 @@ class InternalScopeRepository implements ScopeRepositoryInterface {
return new ScopeEntity($identifier);
}
/**
* @throws OAuthServerException
*/
public function finalizeScopes(
array $scopes,
$grantType,
ClientEntityInterface $client,
$userIdentifier = null
ClientEntityInterface $clientEntity,
$userIdentifier = null,
?string $authCodeId = null,
): array {
/** @var ClientEntity $client */
Assert::isInstanceOf($client, ClientEntity::class);
if (empty($scopes)) {
return $scopes;
}
/** @var ClientEntity $clientEntity */
// Right now we have no available scopes for the client_credentials grant
if (!$client->isTrusted()) {
if (!$clientEntity->isTrusted()) {
throw OAuthServerException::invalidScope($scopes[0]->getIdentifier());
}

View File

@ -11,18 +11,18 @@ use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
class PublicScopeRepository implements ScopeRepositoryInterface {
public const OFFLINE_ACCESS = 'offline_access';
public const CHANGE_SKIN = 'change_skin';
public const string OFFLINE_ACCESS = 'offline_access';
public const string CHANGE_SKIN = 'change_skin';
private const ACCOUNT_INFO = 'account_info';
private const ACCOUNT_EMAIL = 'account_email';
private const string ACCOUNT_INFO = 'account_info';
private const string ACCOUNT_EMAIL = 'account_email';
private const PUBLIC_SCOPES_TO_INTERNAL_PERMISSIONS = [
private const array PUBLIC_SCOPES_TO_INTERNAL_PERMISSIONS = [
self::ACCOUNT_INFO => P::OBTAIN_OWN_ACCOUNT_INFO,
self::ACCOUNT_EMAIL => P::OBTAIN_ACCOUNT_EMAIL,
];
private const ALLOWED_SCOPES = [
private const array ALLOWED_SCOPES = [
P::OBTAIN_OWN_ACCOUNT_INFO,
P::OBTAIN_ACCOUNT_EMAIL,
P::MINECRAFT_SERVER_SESSION,
@ -43,7 +43,8 @@ class PublicScopeRepository implements ScopeRepositoryInterface {
array $scopes,
$grantType,
ClientEntityInterface $clientEntity,
$userIdentifier = null
$userIdentifier = null,
?string $authCodeId = null,
): array {
return $scopes;
}

View File

@ -9,7 +9,7 @@ class Component extends \yii\base\Component {
public $secret;
public function init() {
public function init(): void {
if ($this->public === null) {
throw new InvalidConfigException('Public is required');
}

View File

@ -12,30 +12,27 @@ use yii\di\Instance;
class Validator extends \yii\validators\Validator {
private const SITE_VERIFY_URL = 'https://recaptcha.net/recaptcha/api/siteverify';
private const string SITE_VERIFY_URL = 'https://recaptcha.net/recaptcha/api/siteverify';
private const REPEAT_LIMIT = 3;
private const REPEAT_TIMEOUT = 1;
private const int REPEAT_LIMIT = 3;
private const int REPEAT_TIMEOUT = 1;
public $skipOnEmpty = false;
public $message = E::CAPTCHA_INVALID;
public $requiredMessage = E::CAPTCHA_REQUIRED;
public string $requiredMessage = E::CAPTCHA_REQUIRED;
/**
* @var Component|string
*/
public $component = 'reCaptcha';
public Component|string $component = 'reCaptcha';
private $client;
public function __construct(ClientInterface $client, array $config = []) {
public function __construct(
private readonly ClientInterface $client,
array $config = [],
) {
parent::__construct($config);
$this->client = $client;
}
public function init() {
public function init(): void {
parent::init();
$this->component = Instance::ensure($this->component, Component::class);
}
@ -43,7 +40,7 @@ class Validator extends \yii\validators\Validator {
/**
* @inheritdoc
*/
protected function validateValue($value) {
protected function validateValue($value): ?array {
if (empty($value)) {
return [$this->requiredMessage, []];
}
@ -53,7 +50,7 @@ class Validator extends \yii\validators\Validator {
$isSuccess = true;
try {
$response = $this->performRequest($value);
} catch (ConnectException | ServerException $e) {
} catch (ConnectException|ServerException $e) {
if (++$repeats >= self::REPEAT_LIMIT) {
throw $e;
}
@ -77,9 +74,7 @@ class Validator extends \yii\validators\Validator {
}
/**
* @param string $value
* @throws \GuzzleHttp\Exception\GuzzleException
* @return ResponseInterface
*/
protected function performRequest(string $value): ResponseInterface {
return $this->client->request('POST', self::SITE_VERIFY_URL, [

View File

@ -6,22 +6,20 @@ namespace api\components\Tokens\Algorithms;
use Lcobucci\JWT\Signer;
use Lcobucci\JWT\Signer\Ecdsa\Sha256;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Signer\Key\InMemory;
final class ES256 implements AlgorithmInterface {
private string $privateKeyPath;
private ?string $privateKeyPass;
private ?Key $privateKey = null;
private ?Key $publicKey = null;
private Sha256 $signer;
private readonly Sha256 $signer;
public function __construct(string $privateKeyPath, ?string $privateKeyPass = null) {
$this->privateKeyPath = $privateKeyPath;
$this->privateKeyPass = $privateKeyPass;
public function __construct(
private readonly string $privateKeyPath,
private readonly ?string $privateKeyPass = null,
) {
$this->signer = new Sha256();
}
@ -31,7 +29,7 @@ final class ES256 implements AlgorithmInterface {
public function getPrivateKey(): Key {
if ($this->privateKey === null) {
$this->privateKey = new Key($this->privateKeyPath, $this->privateKeyPass);
$this->privateKey = InMemory::plainText($this->privateKeyPath, $this->privateKeyPass ?? '');
}
return $this->privateKey;
@ -40,9 +38,9 @@ final class ES256 implements AlgorithmInterface {
public function getPublicKey(): Key {
if ($this->publicKey === null) {
$privateKey = $this->getPrivateKey();
$privateKeyOpenSSL = openssl_pkey_get_private($privateKey->getContent(), $privateKey->getPassphrase() ?? '');
$privateKeyOpenSSL = openssl_pkey_get_private($privateKey->contents(), $privateKey->passphrase());
$publicPem = openssl_pkey_get_details($privateKeyOpenSSL)['key'];
$this->publicKey = new Key($publicPem);
$this->publicKey = InMemory::plainText($publicPem);
}
return $this->publicKey;

View File

@ -6,21 +6,18 @@ namespace api\components\Tokens\Algorithms;
use Lcobucci\JWT\Signer;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Signer\Key\InMemory;
class HS256 implements AlgorithmInterface {
final class HS256 implements AlgorithmInterface {
private ?InMemory $loadedKey = null;
/**
* @var string
* @param non-empty-string $key
*/
private $key;
/**
* @var Key|null
*/
private $loadedKey;
public function __construct(string $key) {
$this->key = $key;
public function __construct(
private readonly string $key,
) {
}
public function getSigner(): Signer {
@ -37,7 +34,7 @@ class HS256 implements AlgorithmInterface {
private function loadKey(): Key {
if ($this->loadedKey === null) {
$this->loadedKey = new Key($this->key);
$this->loadedKey = InMemory::plainText($this->key);
}
return $this->loadedKey;

View File

@ -22,11 +22,11 @@ final class AlgorithmsManager {
* @param AlgorithmInterface[] $algorithms
*/
public function __construct(array $algorithms = []) {
array_map([$this, 'add'], $algorithms);
array_map($this->add(...), $algorithms);
}
public function add(AlgorithmInterface $algorithm): self {
$id = $algorithm->getSigner()->getAlgorithmId();
$id = $algorithm->getSigner()->algorithmId();
Assert::keyNotExists($this->algorithms, $id, 'passed algorithm is already exists');
$this->algorithms[$id] = $algorithm;

View File

@ -5,31 +5,40 @@ namespace api\components\Tokens;
use Carbon\Carbon;
use Exception;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Parser;
use InvalidArgumentException;
use Lcobucci\JWT\Encoding\ChainedFormatter;
use Lcobucci\JWT\Encoding\JoseEncoder;
use Lcobucci\JWT\Token;
use Lcobucci\JWT\Token\Builder;
use Lcobucci\JWT\Token\Parser;
use Lcobucci\JWT\Token\RegisteredClaims;
use Lcobucci\JWT\UnencryptedToken;
use Lcobucci\JWT\Validation\Constraint\SignedWith;
use Lcobucci\JWT\Validation\Validator;
use ParagonIE\ConstantTime\Base64UrlSafe;
use RangeException;
use SodiumException;
use Webmozart\Assert\Assert;
use yii\base\Component as BaseComponent;
class Component extends BaseComponent {
private const PREFERRED_ALGORITHM = 'ES256';
private const string PREFERRED_ALGORITHM = 'ES256';
/**
* @var string
*/
public $privateKeyPath;
public string $privateKeyPath;
/**
* @var string|null
*/
public $privateKeyPass;
public ?string $privateKeyPass = null;
/**
* @var string
*/
public $encryptionKey;
public string $encryptionKey;
private ?AlgorithmsManager $algorithmManager = null;
@ -39,19 +48,49 @@ class Component extends BaseComponent {
Assert::notEmpty($this->encryptionKey, 'encryptionKey must be set');
}
public function create(array $payloads = [], array $headers = []): Token {
/**
* @param array{
* sub?: string,
* jti?: string,
* iat?: \DateTimeImmutable,
* nbf?: \DateTimeImmutable,
* exp?: \DateTimeImmutable,
* } $payloads
* @param array $headers
*
* @throws \api\components\Tokens\AlgorithmIsNotDefinedException
*/
public function create(array $payloads = [], array $headers = []): UnencryptedToken {
$now = Carbon::now();
$builder = (new Builder())->issuedAt($now->getTimestamp());
$builder = (new Builder(new JoseEncoder(), ChainedFormatter::default()))->issuedAt($now->toDateTimeImmutable());
if (isset($payloads['sub'])) {
$builder = $builder->relatedTo($payloads['sub']);
}
if (isset($payloads['jti'])) {
$builder = $builder->identifiedBy($payloads['jti']);
}
if (isset($payloads['iat'])) {
$builder = $builder->issuedAt($payloads['iat']);
}
if (isset($payloads['nbf'])) {
$builder = $builder->canOnlyBeUsedAfter($payloads['nbf']);
}
if (isset($payloads['exp'])) {
$builder->expiresAt($payloads['exp']);
$builder = $builder->expiresAt($payloads['exp']);
}
foreach ($payloads as $claim => $value) {
$builder->withClaim($claim, $this->prepareValue($value));
if (!in_array($claim, RegisteredClaims::ALL, true)) { // Registered claims are handled by the if-chain above
$builder = $builder->withClaim($claim, $this->prepareValue($value));
}
}
foreach ($headers as $claim => $value) {
$builder->withHeader($claim, $this->prepareValue($value));
$builder = $builder->withHeader($claim, $this->prepareValue($value));
}
/** @noinspection PhpUnhandledExceptionInspection */
@ -64,17 +103,17 @@ class Component extends BaseComponent {
* @param string $jwt
*
* @return Token
* @throws \InvalidArgumentException
* @throws InvalidArgumentException
*/
public function parse(string $jwt): Token {
return (new Parser())->parse($jwt);
return (new Parser(new JoseEncoder()))->parse($jwt);
}
public function verify(Token $token): bool {
try {
$algorithm = $this->getAlgorithmManager()->get($token->getHeader('alg'));
return $token->verify($algorithm->getSigner(), $algorithm->getPublicKey());
} catch (Exception $e) {
$algorithm = $this->getAlgorithmManager()->get($token->headers()->get('alg'));
return (new Validator())->validate($token, new SignedWith($algorithm->getSigner(), $algorithm->getPublicKey()));
} catch (Exception) {
return false;
}
}
@ -92,8 +131,8 @@ class Component extends BaseComponent {
* @param string $encryptedValue
*
* @return string
* @throws \SodiumException
* @throws \RangeException
* @throws SodiumException
* @throws RangeException
*/
public function decryptValue(string $encryptedValue): string {
$decoded = Base64UrlSafe::decode($encryptedValue);
@ -109,7 +148,7 @@ class Component extends BaseComponent {
}
public function getPublicKey(): string {
return $this->getAlgorithmManager()->get(self::PREFERRED_ALGORITHM)->getPublicKey()->getContent();
return $this->getAlgorithmManager()->get(self::PREFERRED_ALGORITHM)->getPublicKey()->contents();
}
private function getAlgorithmManager(): AlgorithmsManager {
@ -122,9 +161,9 @@ class Component extends BaseComponent {
return $this->algorithmManager;
}
private function prepareValue($value) {
private function prepareValue(EncryptedValue|string $value): string {
if ($value instanceof EncryptedValue) {
return $this->encryptValue($value->getValue());
return $this->encryptValue($value->value);
}
return $value;

View File

@ -3,19 +3,9 @@ declare(strict_types=1);
namespace api\components\Tokens;
class EncryptedValue {
final readonly class EncryptedValue {
/**
* @var string
*/
private $value;
public function __construct(string $value) {
$this->value = $value;
}
public function getValue(): string {
return $this->value;
public function __construct(public string $value) {
}
}

View File

@ -3,19 +3,18 @@ declare(strict_types=1);
namespace api\components\Tokens;
use Lcobucci\JWT\Token;
use Lcobucci\JWT\UnencryptedToken;
use Yii;
final class TokenReader {
final readonly class TokenReader {
private Token $token;
public function __construct(Token $token) {
$this->token = $token;
public function __construct(
private UnencryptedToken $token,
) {
}
public function getAccountId(): ?int {
$sub = $this->token->getClaim('sub', false);
$sub = $this->token->claims()->get('sub', false);
if ($sub === false) {
return null;
}
@ -24,36 +23,36 @@ final class TokenReader {
return null;
}
return (int)mb_substr($sub, mb_strlen(TokensFactory::SUB_ACCOUNT_PREFIX));
return (int)mb_substr((string)$sub, mb_strlen(TokensFactory::SUB_ACCOUNT_PREFIX));
}
public function getClientId(): ?string {
return $this->token->getClaim('client_id', false) ?: null;
return $this->token->claims()->get('client_id', false) ?: null;
}
public function getScopes(): ?array {
$scopes = $this->token->getClaim('scope', false);
$scopes = $this->token->claims()->get('scope', false);
if ($scopes !== false) {
return explode(' ', $scopes);
return explode(' ', (string)$scopes);
}
// Handle legacy tokens, which used "ely-scopes" claim and was delimited with comma
$scopes = $this->token->getClaim('ely-scopes', false);
$scopes = $this->token->claims()->get('ely-scopes', false);
if ($scopes === false) {
return null;
}
return explode(',', $scopes);
return explode(',', (string)$scopes);
}
public function getMinecraftClientToken(): ?string {
$encodedClientToken = $this->token->getClaim('ely-client-token', false);
$encodedClientToken = $this->token->claims()->get('ely-client-token', false);
if ($encodedClientToken === false) {
return null;
}
/**
* It really might throw an exception but we have not seen any case of such exception yet
* It really might throw an exception, but we have not seen any case of such exception yet
* @noinspection PhpUnhandledExceptionInspection
*/
return Yii::$app->tokens->decryptValue($encodedClientToken);

View File

@ -10,6 +10,7 @@ use common\models\Account;
use common\models\AccountSession;
use DateTime;
use Lcobucci\JWT\Token;
use Lcobucci\JWT\UnencryptedToken;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\ScopeEntityInterface;
use Yii;
@ -17,54 +18,52 @@ use yii\base\Component;
class TokensFactory extends Component {
public const SUB_ACCOUNT_PREFIX = 'ely|';
public const string SUB_ACCOUNT_PREFIX = 'ely|';
public function createForWebAccount(Account $account, AccountSession $session = null): Token {
public function createForWebAccount(Account $account, AccountSession $session = null): UnencryptedToken {
$payloads = [
'sub' => $this->buildSub($account->id),
'exp' => Carbon::now()->addHour()->getTimestamp(),
'exp' => Carbon::now()->addHour()->toDateTimeImmutable(),
'scope' => $this->prepareScopes([R::ACCOUNTS_WEB_USER]),
];
if ($session === null) {
// If we don't remember a session, the token should live longer
// so that the session doesn't end while working with the account
$payloads['exp'] = Carbon::now()->addDays(7)->getTimestamp();
$payloads['exp'] = Carbon::now()->addDays(7)->toDateTimeImmutable();
} else {
$payloads['jti'] = $session->id;
$payloads['jti'] = (string)$session->id;
}
return Yii::$app->tokens->create($payloads);
}
public function createForOAuthClient(AccessTokenEntityInterface $accessToken): Token {
public function createForOAuthClient(AccessTokenEntityInterface $accessToken): UnencryptedToken {
$payloads = [
'client_id' => $accessToken->getClient()->getIdentifier(),
'scope' => $this->prepareScopes($accessToken->getScopes()),
];
if ($accessToken->getExpiryDateTime() > new DateTime()) {
$payloads['exp'] = $accessToken->getExpiryDateTime()->getTimestamp();
$payloads['exp'] = $accessToken->getExpiryDateTime();
}
if ($accessToken->getUserIdentifier() !== null) {
$payloads['sub'] = $this->buildSub($accessToken->getUserIdentifier());
$payloads['sub'] = $this->buildSub((int)$accessToken->getUserIdentifier());
}
return Yii::$app->tokens->create($payloads);
}
public function createForMinecraftAccount(Account $account, string $clientToken): Token {
public function createForMinecraftAccount(Account $account, string $clientToken): UnencryptedToken {
return Yii::$app->tokens->create([
'scope' => $this->prepareScopes([P::OBTAIN_OWN_ACCOUNT_INFO, P::MINECRAFT_SERVER_SESSION]),
'ely-client-token' => new EncryptedValue($clientToken),
'sub' => $this->buildSub($account->id),
'exp' => Carbon::now()->addDays(2)->getTimestamp(),
'exp' => Carbon::now()->addDays(2)->toDateTimeImmutable(),
]);
}
/**
* @param ScopeEntityInterface[]|string[] $scopes
*
* @return string
*/
private function prepareScopes(array $scopes): string {
return implode(' ', array_map(function($scope): string {

View File

@ -18,13 +18,13 @@ use yii\web\User as YiiUserComponent;
*/
class Component extends YiiUserComponent {
public const KEEP_MINECRAFT_SESSIONS = 1;
public const KEEP_SITE_SESSIONS = 2;
public const KEEP_CURRENT_SESSION = 4;
public const int KEEP_MINECRAFT_SESSIONS = 1;
public const int KEEP_SITE_SESSIONS = 2;
public const int KEEP_CURRENT_SESSION = 4;
public $enableSession = false;
public $loginUrl = null;
public $loginUrl;
/**
* We don't use the standard web authorization mechanism via cookies.
@ -57,12 +57,13 @@ class Component extends YiiUserComponent {
return null;
}
$sessionId = $identity->getToken()->getClaim('jti', false);
if ($sessionId === false) {
/** @var int|null $sessionId */
$sessionId = $identity->getToken()->claims()->get('jti');
if ($sessionId === null) {
return null;
}
return AccountSession::findOne($sessionId);
return AccountSession::findOne(['id' => (int)$sessionId]);
}
public function terminateSessions(Account $account, int $mode = 0): void {

View File

@ -5,57 +5,54 @@ namespace api\components\User;
use api\components\Tokens\TokenReader;
use Carbon\Carbon;
use Carbon\FactoryImmutable;
use common\models\Account;
use common\models\OauthClient;
use common\models\OauthSession;
use DateTimeImmutable;
use Exception;
use Lcobucci\JWT\Token;
use Lcobucci\JWT\ValidationData;
use Lcobucci\JWT\UnencryptedToken;
use Lcobucci\JWT\Validation\Constraint\LooseValidAt;
use Lcobucci\JWT\Validation\Validator;
use Yii;
use yii\base\NotSupportedException;
use yii\web\UnauthorizedHttpException;
class JwtIdentity implements IdentityInterface {
/**
* @var Token
*/
private $token;
private ?TokenReader $reader = null;
/**
* @var TokenReader|null
*/
private $reader;
private function __construct(Token $token) {
$this->token = $token;
private function __construct(
private readonly UnencryptedToken $token,
) {
}
public static function findIdentityByAccessToken($rawToken, $type = null): IdentityInterface {
public static function findIdentityByAccessToken($token, $type = null): IdentityInterface {
try {
$token = Yii::$app->tokens->parse($rawToken);
$parsedToken = Yii::$app->tokens->parse($token);
} catch (Exception $e) {
Yii::error($e);
throw new UnauthorizedHttpException('Incorrect token');
}
if (!Yii::$app->tokens->verify($token)) {
if (!Yii::$app->tokens->verify($parsedToken)) {
throw new UnauthorizedHttpException('Incorrect token');
}
$now = Carbon::now();
if ($token->isExpired($now)) {
if ($parsedToken->isExpired($now)) {
throw new UnauthorizedHttpException('Token expired');
}
if (!$token->validate(new ValidationData($now->getTimestamp()))) {
if (!(new Validator())->validate($parsedToken, new LooseValidAt(FactoryImmutable::getDefaultInstance()))) {
throw new UnauthorizedHttpException('Incorrect token');
}
$tokenReader = new TokenReader($token);
$tokenReader = new TokenReader($parsedToken);
$accountId = $tokenReader->getAccountId();
if ($accountId !== null) {
$iat = $token->getClaim('iat');
/** @var DateTimeImmutable $iat */
$iat = $parsedToken->claims()->get('iat');
if ($tokenReader->getMinecraftClientToken() !== null
&& self::isRevoked($accountId, OauthClient::UNAUTHORIZED_MINECRAFT_GAME_LAUNCHER, $iat)
) {
@ -69,10 +66,10 @@ class JwtIdentity implements IdentityInterface {
}
}
return new self($token);
return new self($parsedToken);
}
public function getToken(): Token {
public function getToken(): UnencryptedToken {
return $this->token;
}
@ -85,10 +82,10 @@ class JwtIdentity implements IdentityInterface {
}
public function getId(): string {
return (string)$this->token;
return $this->token->toString();
}
// @codeCoverageIgnoreStart
/** @codeCoverageIgnoreStart */
public function getAuthKey() {
throw new NotSupportedException('This method used for cookie auth, except we using Bearer auth');
}
@ -97,17 +94,19 @@ class JwtIdentity implements IdentityInterface {
throw new NotSupportedException('This method used for cookie auth, except we using Bearer auth');
}
/**
* @throws NotSupportedException
*/
public static function findIdentity($id) {
throw new NotSupportedException('This method used for cookie auth, except we using Bearer auth');
}
private static function isRevoked(int $accountId, string $clientId, int $iat): bool {
private static function isRevoked(int $accountId, string $clientId, DateTimeImmutable $iat): bool {
$session = OauthSession::findOne(['account_id' => $accountId, 'client_id' => $clientId]);
return $session !== null && $session->revoked_at !== null && $session->revoked_at > $iat;
return $session !== null && $session->revoked_at !== null && $session->revoked_at > $iat->getTimestamp();
}
// @codeCoverageIgnoreEnd
/** @codeCoverageIgnoreEnd */
private function getReader(): TokenReader {
if ($this->reader === null) {
$this->reader = new TokenReader($this->token);

View File

@ -10,33 +10,21 @@ use Yii;
use yii\base\NotSupportedException;
use yii\web\UnauthorizedHttpException;
class LegacyOAuth2Identity implements IdentityInterface {
readonly class LegacyOAuth2Identity implements IdentityInterface {
/**
* @var string
* @param string[] $scopes
*/
private $accessToken;
/**
* @var string
*/
private $sessionId;
/**
* @var string[]
*/
private $scopes;
private function __construct(string $accessToken, int $sessionId, array $scopes) {
$this->accessToken = $accessToken;
$this->sessionId = $sessionId;
$this->scopes = $scopes;
private function __construct(
private string $accessToken,
private int $sessionId,
private array $scopes,
) {
}
/**
* @inheritdoc
* @throws UnauthorizedHttpException
* @return IdentityInterface
*/
public static function findIdentityByAccessToken($token, $type = null): IdentityInterface {
$tokenParams = self::findRecordOnLegacyStorage($token);
@ -48,16 +36,11 @@ class LegacyOAuth2Identity implements IdentityInterface {
throw new UnauthorizedHttpException('Token expired');
}
return new static($token, $tokenParams['session_id'], $tokenParams['scopes']);
return new self($token, $tokenParams['session_id'], $tokenParams['scopes']);
}
public function getAccount(): ?Account {
$session = $this->getSession();
if ($session === null) {
return null;
}
return $session->account;
return $this->getSession()?->account;
}
/**
@ -71,7 +54,7 @@ class LegacyOAuth2Identity implements IdentityInterface {
return $this->accessToken;
}
// @codeCoverageIgnoreStart
/** @codeCoverageIgnoreStart */
public function getAuthKey() {
throw new NotSupportedException('This method used for cookie auth, except we using Bearer auth');
}
@ -84,8 +67,7 @@ class LegacyOAuth2Identity implements IdentityInterface {
throw new NotSupportedException('This method used for cookie auth, except we using Bearer auth');
}
// @codeCoverageIgnoreEnd
/** @codeCoverageIgnoreEnd */
private static function findRecordOnLegacyStorage(string $accessToken): ?array {
$record = Yii::$app->redis->get("oauth:access:tokens:{$accessToken}");
if ($record === null) {
@ -93,8 +75,8 @@ class LegacyOAuth2Identity implements IdentityInterface {
}
try {
$data = json_decode($record, true, 512, JSON_THROW_ON_ERROR);
} catch (Exception $e) {
$data = json_decode((string)$record, true, 512, JSON_THROW_ON_ERROR);
} catch (Exception) {
return null;
}

View File

@ -16,15 +16,12 @@ return [
],
'container' => [
'singletons' => [
api\components\ReCaptcha\Validator::class => function() {
return new class(new GuzzleHttp\Client()) extends api\components\ReCaptcha\Validator {
protected function validateValue($value) {
api\components\ReCaptcha\Validator::class => fn(): api\components\ReCaptcha\Validator => new class(new GuzzleHttp\Client()) extends api\components\ReCaptcha\Validator {
protected function validateValue($value): ?array {
return null;
}
};
},
common\components\SkinsSystemApi::class => function() {
return new class('http://chrly.ely.by') extends common\components\SkinsSystemApi {
common\components\SkinsSystemApi::class => fn(): common\components\SkinsSystemApi => new class('http://chrly.ely.by') extends common\components\SkinsSystemApi {
public function textures(string $username): ?array {
return [
'SKIN' => [
@ -103,7 +100,6 @@ return [
public function getSignatureVerificationKey(string $format = 'pem'): string {
return "-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANbUpVCZkMKpfvYZ08W3lumdAaYxLBnm\nUDlzHBQH3DpYef5WCO32TDU6feIJ58A0lAywgtZ4wwi2dGHOz/1hAvcCAwEAAQ==\n-----END PUBLIC KEY-----";
}
};
},
],
],

View File

@ -42,7 +42,7 @@ return [
'traceLevel' => YII_DEBUG ? 3 : 0,
'targets' => [
[
'class' => mito\sentry\Target::class,
'class' => nohnaimer\sentry\Target::class,
'levels' => ['error', 'warning'],
'except' => [
'legacy-authserver',

View File

@ -48,7 +48,7 @@ class AuthenticationController extends Controller {
];
}
public function actionLogin() {
public function actionLogin(): array {
$model = new LoginForm();
$model->load(Yii::$app->request->post());
if (($result = $model->login()) === null) {
@ -69,7 +69,7 @@ class AuthenticationController extends Controller {
], $result->formatAsOAuth2Response());
}
public function actionLogout() {
public function actionLogout(): array {
$form = new LogoutForm();
$form->logout();
@ -78,7 +78,7 @@ class AuthenticationController extends Controller {
];
}
public function actionForgotPassword() {
public function actionForgotPassword(): array {
$model = new ForgotPasswordForm();
$model->load(Yii::$app->request->post());
if ($model->forgotPassword() === false) {
@ -106,14 +106,14 @@ class AuthenticationController extends Controller {
],
];
if (strpos($model->login, '@') === false) {
if (!str_contains((string)$model->login, '@')) {
$response['data']['emailMask'] = StringHelper::getEmailMask($model->getAccount()->email);
}
return $response;
}
public function actionRecoverPassword() {
public function actionRecoverPassword(): array {
$model = new RecoverPasswordForm();
$model->load(Yii::$app->request->post());
if (($result = $model->recoverPassword()) === null) {
@ -128,7 +128,7 @@ class AuthenticationController extends Controller {
], $result->formatAsOAuth2Response());
}
public function actionRefreshToken() {
public function actionRefreshToken(): array {
$model = new RefreshTokenForm();
$model->load(Yii::$app->request->post());
if (($result = $model->renew()) === null) {

View File

@ -25,7 +25,7 @@ class Controller extends \yii\rest\Controller {
// XML and rate limiter is not necessary
unset(
$parentBehaviors['contentNegotiator']['formats']['application/xml'],
$parentBehaviors['rateLimiter']
$parentBehaviors['rateLimiter'],
);
return $parentBehaviors;

View File

@ -21,7 +21,7 @@ class FeedbackController extends Controller {
];
}
public function actionIndex() {
public function actionIndex(): array {
$model = new FeedbackForm();
$model->load(Yii::$app->request->post());
if (!$model->sendMessage()) {

View File

@ -27,7 +27,7 @@ class OptionsController extends Controller {
];
}
public function actionIndex() {
public function actionIndex(): array {
return [
'reCaptchaPublicKey' => Yii::$app->reCaptcha->public,
];

View File

@ -37,7 +37,7 @@ class SignupController extends Controller {
];
}
public function actionIndex() {
public function actionIndex(): array {
$model = new RegistrationForm();
$model->load(Yii::$app->request->post());
if (!$model->signup()) {
@ -52,7 +52,7 @@ class SignupController extends Controller {
];
}
public function actionRepeatMessage() {
public function actionRepeatMessage(): array {
$model = new RepeatAccountActivationForm();
$model->load(Yii::$app->request->post());
if (!$model->sendRepeatMessage()) {
@ -77,7 +77,7 @@ class SignupController extends Controller {
];
}
public function actionConfirm() {
public function actionConfirm(): array {
$model = new ConfirmEmailForm();
$model->load(Yii::$app->request->post());
if (!($result = $model->confirm())) {

View File

@ -48,7 +48,7 @@ final class LogMetricsToStatsd implements BootstrapInterface {
private function getPrefix(ActionEvent $event): ?string {
$action = $event->action;
switch (get_class($action)) {
switch ($action::class) {
case actions\AcceptRulesAction::class: return 'accounts.acceptRules';
case actions\ChangeEmailAction::class: return 'accounts.changeEmail';
case actions\ChangeLanguageAction::class: return 'accounts.switchLanguage';

View File

@ -28,6 +28,7 @@ final class MockDataResponse implements BootstrapInterface {
return;
}
/** @var \yii\web\Response $response */
$response = $event->action->controller->response;
$response->format = Response::FORMAT_JSON;
$response->data = $result;
@ -38,6 +39,7 @@ final class MockDataResponse implements BootstrapInterface {
private function getResponse(ActionEvent $event): ?array {
$action = $event->action;
/** @var \yii\web\Controller $controller */
$controller = $action->controller;
$request = $controller->request;
if ($controller instanceof SignupController && $action->id === 'index') {

View File

@ -12,13 +12,13 @@ use yii\base\InvalidConfigException;
class FeedbackForm extends ApiForm {
public $subject;
public mixed $subject = null;
public $email;
public mixed $email = null;
public $type;
public mixed $type = null;
public $message;
public mixed $message = null;
public function rules(): array {
return [
@ -31,27 +31,30 @@ class FeedbackForm extends ApiForm {
];
}
/**
* @throws InvalidConfigException
*/
public function sendMessage(): bool {
if (!$this->validate()) {
return false;
}
/** @var \yii\swiftmailer\Mailer $mailer */
/** @var \yii\symfonymailer\Mailer $mailer */
$mailer = Yii::$app->mailer;
$supportEmail = Yii::$app->params['supportEmail'];
if (!$supportEmail) {
throw new InvalidConfigException('Please specify supportEmail value in app params');
throw new InvalidConfigException('Please specify supportEmail value in the app params');
}
$account = $this->getAccount();
/** @var \yii\swiftmailer\Message $message */
/** @var \yii\symfonymailer\Message $message */
$message = $mailer->compose('@common/emails/views/feedback', [
'model' => $this,
'account' => $account,
]);
$message
->setTo($supportEmail)
->setFrom([$this->email => $account ? $account->username : $this->email])
->setFrom([$this->email => $account?->username ?? $this->email])
->setSubject($this->subject);
Assert::true($message->send(), 'Unable send feedback email.');

View File

@ -3,26 +3,18 @@ declare(strict_types=1);
namespace api\models\authentication;
use Lcobucci\JWT\Token;
use DateTimeImmutable;
use Lcobucci\JWT\UnencryptedToken;
class AuthenticationResult {
final readonly class AuthenticationResult {
/**
* @var Token
*/
private $token;
/**
* @var string|null
*/
private $refreshToken;
public function __construct(Token $token, string $refreshToken = null) {
$this->token = $token;
$this->refreshToken = $refreshToken;
public function __construct(
private UnencryptedToken $token,
private ?string $refreshToken = null,
) {
}
public function getToken(): Token {
public function getToken(): UnencryptedToken {
return $this->token;
}
@ -31,9 +23,11 @@ class AuthenticationResult {
}
public function formatAsOAuth2Response(): array {
/** @var DateTimeImmutable $expiresAt */
$expiresAt = $this->token->claims()->get('exp');
$response = [
'access_token' => (string)$this->token,
'expires_in' => $this->token->getClaim('exp') - time(),
'access_token' => $this->token->toString(),
'expires_in' => $expiresAt->getTimestamp() - (new DateTimeImmutable())->getTimestamp(),
];
$refreshToken = $this->refreshToken;

View File

@ -16,9 +16,9 @@ use yii\base\ErrorException;
class ForgotPasswordForm extends ApiForm {
public $captcha;
public mixed $captcha = null;
public $login;
public mixed $login = null;
public function rules(): array {
return [
@ -90,6 +90,7 @@ class ForgotPasswordForm extends ApiForm {
return null;
}
// @phpstan-ignore return.type
return $account->getEmailActivations()->withType(EmailActivation::TYPE_FORGOT_PASSWORD_KEY)->one();
}

View File

@ -13,39 +13,23 @@ use Yii;
class LoginForm extends ApiForm {
/**
* @var string
*/
public $login;
public mixed $login = null;
/**
* @var string
*/
public $password;
public mixed $password = null;
/**
* @var string|null
*/
public $totp;
public mixed $totp = null;
/**
* @var bool
*/
public $rememberMe = false;
public mixed $rememberMe = false;
public function rules(): array {
return [
['login', 'required', 'message' => E::LOGIN_REQUIRED],
['login', 'validateLogin'],
['password', 'required', 'when' => function(self $model): bool {
return !$model->hasErrors();
}, 'message' => E::PASSWORD_REQUIRED],
['password', 'required', 'when' => fn(self $model): bool => !$model->hasErrors(), 'message' => E::PASSWORD_REQUIRED],
['password', 'validatePassword'],
['totp', 'required', 'when' => function(self $model): bool {
return !$model->hasErrors() && $model->getAccount()->is_otp_enabled;
}, 'message' => E::TOTP_REQUIRED],
['totp', 'required', 'when' => fn(self $model): bool => !$model->hasErrors() && $model->getAccount()->is_otp_enabled, 'message' => E::TOTP_REQUIRED],
['totp', 'validateTotp'],
['login', 'validateActivity'],
@ -81,7 +65,6 @@ class LoginForm extends ApiForm {
}
$validator = new TotpValidator(['account' => $account]);
$validator->window = 1;
$validator->validateAttribute($this, $attribute);
}
@ -99,6 +82,7 @@ class LoginForm extends ApiForm {
}
}
/** @noinspection PhpIncompatibleReturnTypeInspection */
public function getAccount(): ?Account {
return Account::find()->andWhereLogin($this->login)->one();
}
@ -130,7 +114,7 @@ class LoginForm extends ApiForm {
$transaction->commit();
return new AuthenticationResult($token, $session ? $session->refresh_token : null);
return new AuthenticationResult($token, $session?->refresh_token);
}
}

View File

@ -54,7 +54,7 @@ class RegistrationForm extends ApiForm {
];
}
public function validatePasswordAndRePasswordMatch($attribute) {
public function validatePasswordAndRePasswordMatch($attribute): void {
if (!$this->hasErrors()) {
if ($this->password !== $this->rePassword) {
$this->addError($attribute, E::RE_PASSWORD_DOES_NOT_MATCH);
@ -64,7 +64,7 @@ class RegistrationForm extends ApiForm {
public function signup() {
if (!$this->validate() && !$this->canContinue($this->getFirstErrors())) {
return null;
return;
}
$transaction = Yii::$app->db->beginTransaction();

View File

@ -16,11 +16,9 @@ use Yii;
class RepeatAccountActivationForm extends ApiForm {
public $captcha;
public mixed $captcha = null;
public $email;
private $emailActivation;
public mixed $email = null;
public function rules(): array {
return [
@ -74,8 +72,6 @@ class RepeatAccountActivationForm extends ApiForm {
$activation->key = UserFriendlyRandomKey::make();
Assert::true($activation->save(), 'Unable save email-activation model.');
$this->emailActivation = $activation;
Yii::$app->queue->push(SendRegistrationEmail::createFromConfirmation($activation));
$transaction->commit();
@ -90,6 +86,7 @@ class RepeatAccountActivationForm extends ApiForm {
}
public function getActivation(): ?RegistrationConfirmation {
// @phpstan-ignore return.type
return $this->getAccount()
->getEmailActivations()
->withType(EmailActivation::TYPE_REGISTRATION_EMAIL_CONFIRMATION)

View File

@ -7,11 +7,11 @@ use common\models\Account;
class BaseAccountForm extends ApiForm {
private Account $account;
public function __construct(Account $account, array $config = []) {
public function __construct(
private readonly Account $account,
array $config = [],
) {
parent::__construct($config);
$this->account = $account;
}
public function getAccount(): Account {

View File

@ -8,8 +8,11 @@ use common\helpers\Error as E;
class EmailVerificationAction extends BaseAccountAction {
/**
* @param SendEmailVerificationForm|AccountActionForm $model
* @return array
* @param SendEmailVerificationForm $model
*
* @return array{
* canRepeatIn?: int,
* }
*/
public function getFailedResultData(AccountActionForm $model): array {
$emailError = $model->getFirstError('email');

View File

@ -29,9 +29,10 @@ class ChangePasswordForm extends AccountActionForm {
['newPassword', PasswordValidator::class],
['newRePassword', 'validatePasswordAndRePasswordMatch'],
['logoutAll', 'boolean'],
['password', PasswordRequiredValidator::class, 'account' => $this->getAccount(), 'when' => function() {
return !$this->hasErrors();
}],
['password', PasswordRequiredValidator::class,
'account' => $this->getAccount(),
'when' => fn(): bool => !$this->hasErrors(),
],
]);
}

View File

@ -16,9 +16,7 @@ class ChangeUsernameForm extends AccountActionForm {
public function rules(): array {
return [
['username', UsernameValidator::class, 'accountCallback' => function() {
return $this->getAccount()->id;
}],
['username', UsernameValidator::class, 'accountCallback' => fn() => $this->getAccount()->id],
['password', PasswordRequiredValidator::class, 'account' => $this->getAccount()],
];
}

View File

@ -40,7 +40,7 @@ final class DeleteAccountForm extends AccountActionForm {
Assert::true($account->save(), 'Cannot delete account');
// Schedule complete account erasing
Yii::$app->queue->delay($account->getDeleteAt()->diffInRealSeconds())->push(new DeleteAccount($account->id));
Yii::$app->queue->delay($account->getDeleteAt()->diffInUTCSeconds(null, true))->push(new DeleteAccount($account->id));
$transaction->commit();

View File

@ -18,7 +18,7 @@ class EnableTwoFactorAuthForm extends AccountActionForm {
return [
['account', 'validateOtpDisabled'],
['totp', 'required', 'message' => E::TOTP_REQUIRED],
['totp', TotpValidator::class, 'account' => $this->getAccount(), 'window' => 2],
['totp', TotpValidator::class, 'account' => $this->getAccount()],
['password', PasswordRequiredValidator::class, 'account' => $this->getAccount()],
];
}

View File

@ -77,14 +77,15 @@ class SendEmailVerificationForm extends AccountActionForm {
* Including checking for the confirmation of the new E-mail type, because when you go to this step,
* the activation of the previous step is removed.
*
* @return CurrentEmailConfirmation|\common\models\confirmations\NewEmailConfirmation
* @return \common\models\confirmations\CurrentEmailConfirmation|\common\models\confirmations\NewEmailConfirmation
*/
public function getEmailActivation(): ?EmailActivation {
// @phpstan-ignore return.type
return $this->getAccount()
->getEmailActivations()
->withType(
EmailActivation::TYPE_CURRENT_EMAIL_CONFIRMATION,
EmailActivation::TYPE_NEW_EMAIL_CONFIRMATION
EmailActivation::TYPE_NEW_EMAIL_CONFIRMATION,
)
->one();
}

View File

@ -17,6 +17,13 @@ use Webmozart\Assert\Assert;
class TwoFactorAuthInfo extends BaseAccountForm {
/**
* @return array{
* qr: string,
* uri: string,
* secret: string,
* }
*/
public function getCredentials(): array {
if (empty($this->getAccount()->otp_secret)) {
$this->setOtpSecret();

View File

@ -32,11 +32,11 @@ class Module extends BaseModule implements BootstrapInterface {
], false);
}
public static function info($message) {
public static function info($message): void {
Yii::info($message, 'legacy-authserver');
}
public static function error($message) {
public static function error($message): void {
Yii::info($message, 'legacy-authserver');
}

View File

@ -6,7 +6,7 @@ use api\controllers\Controller;
class IndexController extends Controller {
// TODO: симулировать для этого модуля обработчик 404 ошибок, как был в фалконе
public function notFoundAction() {
public function notFoundAction(): void {
/*return $this->response
->setStatusCode(404, 'Not Found')
->setContent('Page not found. Check our <a href="http://docs.ely.by">documentation site</a>.');*/

View File

@ -5,21 +5,14 @@ namespace api\modules\authserver\models;
use common\models\Account;
final class AuthenticateData {
final readonly class AuthenticateData {
private Account $account;
private string $accessToken;
private string $clientToken;
private bool $requestUser;
public function __construct(Account $account, string $accessToken, string $clientToken, bool $requestUser) {
$this->account = $account;
$this->accessToken = $accessToken;
$this->clientToken = $clientToken;
$this->requestUser = $requestUser;
public function __construct(
private Account $account,
private string $accessToken,
private string $clientToken,
private bool $requestUser,
) {
}
public function getResponseData(bool $includeAvailableProfiles = false): array {

View File

@ -17,6 +17,7 @@ use common\models\OauthSession;
use Ramsey\Uuid\Uuid;
use Webmozart\Assert\Assert;
use Yii;
use yii\db\Exception;
class AuthenticationForm extends ApiForm {
@ -50,8 +51,8 @@ class AuthenticationForm extends ApiForm {
/**
* @return AuthenticateData
* @throws \api\modules\authserver\exceptions\IllegalArgumentException
* @throws \api\modules\authserver\exceptions\ForbiddenOperationException
* @throws ForbiddenOperationException
* @throws Exception
*/
public function authenticate(): AuthenticateData {
// This validating method will throw an exception in case when validation will not pass successfully
@ -61,7 +62,7 @@ class AuthenticationForm extends ApiForm {
// The previous authorization server implementation used the nickname field instead of username,
// so we keep such behavior
$attribute = strpos($this->username, '@') === false ? 'nickname' : 'email';
$attribute = !str_contains($this->username, '@') ? 'nickname' : 'email';
$password = $this->password;
$totp = null;
@ -113,7 +114,7 @@ class AuthenticationForm extends ApiForm {
$account = $loginForm->getAccount();
$clientToken = $this->clientToken ?: Uuid::uuid4()->toString();
$token = Yii::$app->tokensFactory->createForMinecraftAccount($account, $clientToken);
$dataModel = new AuthenticateData($account, (string)$token, $clientToken, (bool)$this->requestUser);
$dataModel = new AuthenticateData($account, $token->toString(), $clientToken, (bool)$this->requestUser);
/** @var OauthSession|null $minecraftOauthSession */
$minecraftOauthSession = $account->getOauthSessions()
->andWhere(['client_id' => OauthClient::UNAUTHORIZED_MINECRAFT_GAME_LAUNCHER])

View File

@ -75,7 +75,7 @@ class RefreshTokenForm extends ApiForm {
$minecraftOauthSession->last_used_at = time();
Assert::true($minecraftOauthSession->save());
return new AuthenticateData($account, (string)$token, $this->clientToken, (bool)$this->requestUser);
return new AuthenticateData($account, $token->toString(), $this->clientToken, (bool)$this->requestUser);
}
}

View File

@ -47,7 +47,7 @@ class SignoutForm extends ApiForm {
// The previous authorization server implementation used the nickname field instead of username,
// so we keep such behavior
$attribute = strpos($this->username, '@') === false ? 'nickname' : 'email';
$attribute = !str_contains($this->username, '@') ? 'nickname' : 'email';
throw new ForbiddenOperationException("Invalid credentials. Invalid {$attribute} or password.");
}

View File

@ -13,8 +13,8 @@ use yii\validators\Validator;
class AccessTokenValidator extends Validator {
private const INVALID_TOKEN = 'Invalid token.';
private const TOKEN_EXPIRED = 'Token expired.';
private const string INVALID_TOKEN = 'Invalid token.';
private const string TOKEN_EXPIRED = 'Token expired.';
public bool $verifyExpiration = true;
@ -27,7 +27,7 @@ class AccessTokenValidator extends Validator {
protected function validateValue($value): ?array {
try {
$token = Yii::$app->tokens->parse($value);
} catch (Exception $e) {
} catch (Exception) {
throw new ForbiddenOperationException(self::INVALID_TOKEN);
}

View File

@ -10,7 +10,7 @@ class Module extends \yii\base\Module implements BootstrapInterface {
/**
* @param \yii\base\Application $app the application currently running
*/
public function bootstrap($app) {
public function bootstrap($app): void {
$app->getUrlManager()->addRules([
'/internal/<controller>/<accountId>/<action>' => "{$this->id}/<controller>/<action>",
], false);

View File

@ -20,11 +20,9 @@ class AccountsController extends Controller {
'actions' => ['info'],
'allow' => true,
'roles' => [P::OBTAIN_EXTENDED_ACCOUNT_INFO],
'roleParams' => function() {
return [
'roleParams' => fn(): array => [
'accountId' => 0,
];
},
],
],
],
],
@ -37,7 +35,7 @@ class AccountsController extends Controller {
];
}
public function actionInfo(int $id = null, string $username = null, string $uuid = null) {
public function actionInfo(int $id = null, string $username = null, string $uuid = null): array {
if ($id !== null) {
$account = Account::findOne($id);
} elseif ($username !== null) {

View File

@ -3,10 +3,10 @@ namespace api\modules\internal\helpers;
final class Error {
public const ACCOUNT_ALREADY_BANNED = 'error.account_already_banned';
public const ACCOUNT_NOT_BANNED = 'error.account_not_banned';
public const string ACCOUNT_ALREADY_BANNED = 'error.account_already_banned';
public const string ACCOUNT_NOT_BANNED = 'error.account_not_banned';
public const ACCOUNT_ALREADY_DELETED = 'error.account_already_deleted';
public const ACCOUNT_NOT_DELETED = 'error.account_not_deleted';
public const string ACCOUNT_ALREADY_DELETED = 'error.account_already_deleted';
public const string ACCOUNT_NOT_DELETED = 'error.account_not_deleted';
}

View File

@ -47,7 +47,7 @@ class ApiController extends Controller {
}
}
} else {
/** @var Account|null $record */
/** @var Account|null $account */
$account = Account::findOne(['username' => $username]);
}
@ -64,7 +64,7 @@ class ApiController extends Controller {
public function actionUsernamesByUuid(string $uuid) {
try {
$uuid = Uuid::fromString($uuid)->toString();
} catch (\InvalidArgumentException $e) {
} catch (\InvalidArgumentException) {
return $this->illegalArgumentResponse('Invalid uuid format.');
}

View File

@ -27,11 +27,9 @@ class AuthorizationController extends Controller {
'allow' => true,
'actions' => ['complete'],
'roles' => [P::COMPLETE_OAUTH_FLOW],
'roleParams' => function() {
return [
'roleParams' => fn(): array => [
'accountId' => Yii::$app->user->identity->getAccount()->id,
];
},
],
],
],
],

View File

@ -38,7 +38,7 @@ class ClientsController extends Controller {
'actions' => ['update', 'delete', 'reset'],
'allow' => true,
'permissions' => [P::MANAGE_OAUTH_CLIENTS],
'roleParams' => fn() => [
'roleParams' => fn(): array => [
'clientId' => Yii::$app->request->get('clientId'),
],
],
@ -46,7 +46,7 @@ class ClientsController extends Controller {
'actions' => ['get'],
'allow' => true,
'permissions' => [P::VIEW_OAUTH_CLIENTS],
'roleParams' => fn() => [
'roleParams' => fn(): array => [
'clientId' => Yii::$app->request->get('clientId'),
],
],
@ -54,7 +54,7 @@ class ClientsController extends Controller {
'actions' => ['get-per-account'],
'allow' => true,
'permissions' => [P::VIEW_OAUTH_CLIENTS],
'roleParams' => fn() => [
'roleParams' => fn(): array => [
'accountId' => Yii::$app->request->get('accountId'),
],
],
@ -62,7 +62,7 @@ class ClientsController extends Controller {
'actions' => ['get-authorized-clients', 'revoke-client'],
'allow' => true,
'permissions' => [P::MANAGE_OAUTH_SESSIONS],
'roleParams' => fn() => [
'roleParams' => fn(): array => [
'accountId' => Yii::$app->request->get('accountId'),
],
],

View File

@ -27,7 +27,7 @@ class IdentityController extends Controller {
$account = $identity->getAccount();
if ($account === null) {
Yii::$app->sentry->captureMessage('Unexpected lack of account', [
'identityType' => get_class($identity),
'identityType' => $identity::class,
'userId' => $identity->getId(),
'assignedPermissions' => $identity->getAssignedPermissions(),
], [

View File

@ -6,14 +6,12 @@ use yii\base\Exception;
class UnsupportedOauthClientType extends Exception implements OauthException {
/**
* @var string
*/
private $type;
public function __construct(string $type, int $code = 0, Throwable $previous = null) {
public function __construct(
private readonly string $type,
int $code = 0,
Throwable $previous = null,
) {
parent::__construct('Unsupported oauth client type', $code, $previous);
$this->type = $type;
}
public function getType(): string {

View File

@ -9,7 +9,7 @@ use common\models\Account;
class IdentityInfo extends BaseAccountForm {
private $model;
private readonly AccountInfo $model;
public function __construct(Account $account, array $config = []) {
parent::__construct($account, $config);

View File

@ -12,10 +12,7 @@ use yii\helpers\Inflector;
class OauthClientForm {
/**
* @var OauthClient
*/
private $client;
private readonly OauthClient $client;
public function __construct(OauthClient $client) {
if ($client->type === null) {

View File

@ -15,23 +15,20 @@ class OauthClientFormFactory {
* @throws UnsupportedOauthClientType
*/
public static function create(OauthClient $client): OauthClientTypeForm {
switch ($client->type) {
case OauthClient::TYPE_APPLICATION:
return new ApplicationType([
return match ($client->type) {
OauthClient::TYPE_APPLICATION => new ApplicationType([
'name' => $client->name,
'websiteUrl' => $client->website_url,
'description' => $client->description,
'redirectUri' => $client->redirect_uri,
]);
case OauthClient::TYPE_MINECRAFT_SERVER:
return new MinecraftServerType([
]),
OauthClient::TYPE_MINECRAFT_SERVER => new MinecraftServerType([
'name' => $client->name,
'websiteUrl' => $client->website_url,
'minecraftServerIp' => $client->minecraft_server_ip,
]);
}
throw new UnsupportedOauthClientType($client->type);
]),
default => throw new UnsupportedOauthClientType($client->type),
};
}
}

View File

@ -13,22 +13,19 @@ use GuzzleHttp\Psr7\Response;
use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\Entities\ScopeEntityInterface;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
use League\OAuth2\Server\RequestTypes\AuthorizationRequestInterface;
use Psr\Http\Message\ServerRequestInterface;
use Webmozart\Assert\Assert;
use Yii;
class OauthProcess {
private const INTERNAL_PERMISSIONS_TO_PUBLIC_SCOPES = [
private const array INTERNAL_PERMISSIONS_TO_PUBLIC_SCOPES = [
P::OBTAIN_OWN_ACCOUNT_INFO => 'account_info',
P::OBTAIN_ACCOUNT_EMAIL => 'account_email',
];
private AuthorizationServer $server;
public function __construct(AuthorizationServer $server) {
$this->server = $server;
public function __construct(private readonly AuthorizationServer $server) {
}
/**
@ -96,17 +93,12 @@ class OauthProcess {
$canBeAutoApproved = $this->canBeAutoApproved($account, $client, $authRequest);
$acceptParam = ((array)$request->getParsedBody())['accept'] ?? null;
if ($acceptParam === null && !$canBeAutoApproved) {
Yii::$app->statsd->inc('oauth.complete.approve_required');
throw $this->createAcceptRequiredException();
}
Yii::$app->statsd->inc('oauth.complete.approve_required');
if ($acceptParam === null && $canBeAutoApproved) {
$approved = true;
} else {
$approved = in_array($acceptParam, [1, '1', true, 'true'], true);
}
// At this point if the $acceptParam is an empty, then the application can be auto approved
$approved = $acceptParam === null || in_array($acceptParam, [1, '1', true, 'true'], true);
if ($approved) {
$this->storeOauthSession($account, $client, $authRequest);
}
@ -163,7 +155,7 @@ class OauthProcess {
Yii::$app->statsd->inc("oauth.issueToken_{$grantType}.attempt");
$shouldIssueRefreshToken = false;
$this->server->getEmitter()->addOneTimeListener(RequestedRefreshToken::class, function() use (&$shouldIssueRefreshToken) {
$this->server->getEmitter()->subscribeOnceTo(RequestedRefreshToken::class, function() use (&$shouldIssueRefreshToken): void {
$shouldIssueRefreshToken = true;
});
@ -207,14 +199,8 @@ class OauthProcess {
/**
* The method checks whether the current user can be automatically authorized for the specified client
* without requesting access to the necessary list of scopes
*
* @param Account $account
* @param OauthClient $client
* @param AuthorizationRequest $request
*
* @return bool
*/
private function canBeAutoApproved(Account $account, OauthClient $client, AuthorizationRequest $request): bool {
private function canBeAutoApproved(Account $account, OauthClient $client, AuthorizationRequestInterface $request): bool {
if ($client->is_trusted) {
return true;
}
@ -231,7 +217,7 @@ class OauthProcess {
return empty(array_diff($this->getScopesList($request), $session->getScopes()));
}
private function storeOauthSession(Account $account, OauthClient $client, AuthorizationRequest $request): void {
private function storeOauthSession(Account $account, OauthClient $client, AuthorizationRequestInterface $request): void {
$session = $this->findOauthSession($account, $client);
if ($session === null) {
$session = new OauthSession();
@ -301,7 +287,7 @@ class OauthProcess {
];
if ($e->hasRedirect()) {
$response['redirectUri'] = $e->getRedirectUri();
$response['redirectUri'] = $e->getRedirectUri() . http_build_query($e->getPayload());
}
if ($e->getHttpStatusCode() !== 200) {
@ -345,12 +331,11 @@ class OauthProcess {
return new OAuthServerException('Client must accept authentication request.', 0, 'accept_required', 401);
}
private function getScopesList(AuthorizationRequest $request): array {
return array_values(array_map(function(ScopeEntityInterface $scope): string {
return $scope->getIdentifier();
}, $request->getScopes()));
private function getScopesList(AuthorizationRequestInterface $request): array {
return array_values(array_map(fn(ScopeEntityInterface $scope): string => $scope->getIdentifier(), $request->getScopes()));
}
/** @noinspection PhpIncompatibleReturnTypeInspection */
private function findOauthSession(Account $account, OauthClient $client): ?OauthSession {
return $account->getOauthSessions()->andWhere(['client_id' => $client->id])->one();
}

View File

@ -9,11 +9,11 @@ class Module extends \yii\base\Module {
public $defaultRoute = 'session';
public static function info($message) {
public static function info($message): void {
Yii::info($message, 'session');
}
public static function error($message) {
public static function error($message): void {
Yii::info($message, 'session');
}

View File

@ -95,7 +95,7 @@ class SessionController extends Controller {
$hasJoinedForm = new HasJoinedForm($protocol);
try {
$hasJoinedForm->hasJoined();
} catch (ForbiddenOperationException $e) {
} catch (ForbiddenOperationException) {
return 'NO';
} catch (SessionServerException $e) {
Yii::$app->response->statusCode = $e->statusCode;
@ -116,7 +116,7 @@ class SessionController extends Controller {
public function actionProfile(string $uuid, string $unsigned = null): ?array {
try {
$uuid = Uuid::fromString($uuid)->toString();
} catch (\InvalidArgumentException $e) {
} catch (\InvalidArgumentException) {
throw new IllegalArgumentException('Invalid uuid format.');
}

View File

@ -17,7 +17,7 @@ class RateLimiter extends \yii\filters\RateLimiter {
private $server;
public function init() {
public function init(): void {
parent::init();
if ($this->authserverDomain === null) {
throw new InvalidConfigException('authserverDomain param is required');
@ -30,10 +30,10 @@ class RateLimiter extends \yii\filters\RateLimiter {
*/
public function beforeAction($action) {
$this->checkRateLimit(
null,
null, // @phpstan-ignore argument.type (at this moment we don't have any specific identity, so pass null (yea, it's hacky))
$this->request ?: Yii::$app->getRequest(),
$this->response ?: Yii::$app->getResponse(),
$action
$action,
);
return true;
@ -43,8 +43,8 @@ class RateLimiter extends \yii\filters\RateLimiter {
* @inheritdoc
* @throws TooManyRequestsHttpException
*/
public function checkRateLimit($user, $request, $response, $action) {
if (parse_url($request->getHostInfo(), PHP_URL_HOST) === $this->authserverDomain) {
public function checkRateLimit($user, $request, $response, $action): void {
if (parse_url((string)$request->getHostInfo(), PHP_URL_HOST) === $this->authserverDomain) {
return;
}
@ -66,11 +66,7 @@ class RateLimiter extends \yii\filters\RateLimiter {
}
}
/**
* @param Request $request
* @return OauthClient|null
*/
protected function getServer(Request $request) {
protected function getServer(Request $request): ?OauthClient {
$serverId = $request->get('server_id');
if ($serverId === null) {
$this->server = false;
@ -78,7 +74,6 @@ class RateLimiter extends \yii\filters\RateLimiter {
}
if ($this->server === null) {
/** @var OauthClient|null $server */
$this->server = OauthClient::findOne($serverId);
// TODO: убедится, что это сервер
if ($this->server === null) {
@ -93,7 +88,7 @@ class RateLimiter extends \yii\filters\RateLimiter {
return $this->server;
}
protected function buildKey($ip): string {
protected function buildKey(string $ip): string {
return 'sessionserver:ratelimit:' . $ip;
}

View File

@ -14,14 +14,11 @@ use yii\base\Model;
class HasJoinedForm extends Model {
/**
* @var HasJoinedInterface
*/
private $protocol;
public function __construct(HasJoinedInterface $protocol, array $config = []) {
public function __construct(
private readonly HasJoinedInterface $protocol,
array $config = [],
) {
parent::__construct($config);
$this->protocol = $protocol;
}
/**

View File

@ -9,7 +9,6 @@ use api\modules\session\models\protocols\JoinInterface;
use api\modules\session\Module as Session;
use api\modules\session\validators\RequiredValidator;
use api\rbac\Permissions as P;
use Closure;
use common\helpers\StringHelper;
use common\models\Account;
use Ramsey\Uuid\Uuid;
@ -20,35 +19,32 @@ use yii\web\UnauthorizedHttpException;
class JoinForm extends Model {
public $accessToken;
public mixed $accessToken = null;
public $selectedProfile;
public mixed $selectedProfile = null;
public $serverId;
public mixed $serverId = null;
/**
* @var Account|null
*/
private $account;
private ?Account $account = null;
/**
* @var JoinInterface
*/
private $protocol;
public function __construct(JoinInterface $protocol, array $config = []) {
public function __construct(
private readonly JoinInterface $protocol,
array $config = [],
) {
parent::__construct($config);
$this->protocol = $protocol;
$this->accessToken = $protocol->getAccessToken();
$this->selectedProfile = $protocol->getSelectedProfile();
$this->serverId = $protocol->getServerId();
$this->accessToken = $this->protocol->getAccessToken();
$this->selectedProfile = $this->protocol->getSelectedProfile();
$this->serverId = $this->protocol->getServerId();
}
public function rules(): array {
return [
[['accessToken', 'serverId'], RequiredValidator::class],
[['accessToken', 'selectedProfile'], Closure::fromCallable([$this, 'validateUuid'])],
[['accessToken'], Closure::fromCallable([$this, 'validateAccessToken'])],
[['accessToken', 'selectedProfile'], $this->validateUuid(...)],
[['accessToken'], $this->validateAccessToken(...)],
];
}
@ -147,7 +143,7 @@ class JoinForm extends Model {
throw new ForbiddenOperationException('Wrong selected_profile.');
}
if (!$isUuid && mb_strtolower($account->username) !== mb_strtolower($selectedProfile)) {
if (!$isUuid && mb_strtolower($account->username) !== mb_strtolower((string)$selectedProfile)) {
Session::error("User with access_token = '{$accessToken}' trying to join with identity = '{$selectedProfile}', but access_token issued to account with username = '{$account->username}'.");
Yii::$app->statsd->inc('sessionserver.join.fail_username_mismatch');

View File

@ -1,36 +1,35 @@
<?php
declare(strict_types=1);
namespace api\modules\session\models;
use common\models\Account;
use Yii;
class SessionModel {
final readonly class SessionModel {
private const KEY_TIME = 120; // 2 min
private const int KEY_TIME = 120; // 2 min
public $username;
public $serverId;
public function __construct(string $username, string $serverId) {
$this->username = $username;
$this->serverId = $serverId;
public function __construct(
public string $username,
public string $serverId,
) {
}
public static function find(string $username, string $serverId): ?self {
$key = static::buildKey($username, $serverId);
$key = self::buildKey($username, $serverId);
$result = Yii::$app->redis->get($key);
if (!$result) {
return null;
}
$data = json_decode($result, true);
$data = json_decode((string)$result, true);
return new static($data['username'], $data['serverId']);
return new self($data['username'], $data['serverId']);
}
public function save() {
$key = static::buildKey($this->username, $this->serverId);
public function save(): mixed {
$key = self::buildKey($this->username, $this->serverId);
$data = json_encode([
'username' => $this->username,
'serverId' => $this->serverId,
@ -39,15 +38,15 @@ class SessionModel {
return Yii::$app->redis->setex($key, self::KEY_TIME, $data);
}
public function delete() {
return Yii::$app->redis->del(static::buildKey($this->username, $this->serverId));
public function delete(): mixed {
return Yii::$app->redis->del(self::buildKey($this->username, $this->serverId));
}
public function getAccount(): ?Account {
return Account::findOne(['username' => $this->username]);
}
protected static function buildKey($username, $serverId): string {
protected static function buildKey(string $username, string $serverId): string {
return md5('minecraft:join-server:' . mb_strtolower($username) . ':' . $serverId);
}

View File

@ -3,9 +3,9 @@ namespace api\modules\session\models\protocols;
abstract class BaseHasJoined implements HasJoinedInterface {
private $username;
private readonly string $username;
private $serverId;
private readonly string $serverId;
public function __construct(string $username, string $serverId) {
$this->username = trim($username);
@ -21,11 +21,7 @@ abstract class BaseHasJoined implements HasJoinedInterface {
}
public function validate(): bool {
return !$this->isEmpty($this->username) && !$this->isEmpty($this->serverId);
}
private function isEmpty($value): bool {
return $value === null || $value === '';
return $this->username !== '' && $this->serverId !== '';
}
}

View File

@ -3,15 +3,15 @@ namespace api\modules\session\models\protocols;
class LegacyJoin extends BaseJoin {
private $user;
private readonly string $user;
private $sessionId;
private string $sessionId;
private $serverId;
private readonly string $serverId;
private $accessToken;
private $uuid;
private ?string $uuid = null;
public function __construct(string $user, string $sessionId, string $serverId) {
$this->user = trim($user);
@ -46,7 +46,7 @@ class LegacyJoin extends BaseJoin {
* Split by ':' to take into account authorization in modern launchers and login to an legacy version of the game.
* The sessionId is passed on as "token:{accessToken}:{uuid}", so it needs to be processed
*/
private function parseSessionId(string $sessionId) {
private function parseSessionId(string $sessionId): void {
$parts = explode(':', $sessionId);
if (count($parts) === 3) {
$this->accessToken = $parts[1];

View File

@ -3,11 +3,11 @@ namespace api\modules\session\models\protocols;
class ModernJoin extends BaseJoin {
private $accessToken;
private readonly string $accessToken;
private $selectedProfile;
private readonly string $selectedProfile;
private $serverId;
private readonly string $serverId;
public function __construct(string $accessToken, string $selectedProfile, string $serverId) {
$this->accessToken = trim($accessToken);

View File

@ -11,10 +11,9 @@ class RequiredValidator extends \yii\validators\RequiredValidator {
/**
* @param string $value
* @return null
* @throws \api\modules\session\exceptions\SessionServerException
*/
protected function validateValue($value) {
protected function validateValue($value): ?array {
if (parent::validateValue($value) !== null) {
throw new IllegalArgumentException();
}

View File

@ -16,16 +16,14 @@ class Manager extends PhpManager {
* In Yii2, the mechanism of recursive permissions checking requires that the array
* with permissions must be indexed by the keys of these permissions.
*
* @param string $accessToken
* @return string[]
* @return array<string, \yii\rbac\Assignment>
*/
public function getAssignments($accessToken): array {
public function getAssignments($userId): array {
$identity = Yii::$app->user->getIdentity();
if ($identity === null) {
return [];
}
/** @noinspection NullPointerExceptionInspection */
$rawPermissions = $identity->getAssignedPermissions();
$result = [];
foreach ($rawPermissions as $name) {

View File

@ -6,42 +6,42 @@ namespace api\rbac;
final class Permissions {
// Top level Controller permissions
public const OBTAIN_ACCOUNT_INFO = 'obtain_account_info';
public const CHANGE_ACCOUNT_LANGUAGE = 'change_account_language';
public const CHANGE_ACCOUNT_USERNAME = 'change_account_username';
public const CHANGE_ACCOUNT_PASSWORD = 'change_account_password';
public const CHANGE_ACCOUNT_EMAIL = 'change_account_email';
public const MANAGE_TWO_FACTOR_AUTH = 'manage_two_factor_auth';
public const DELETE_ACCOUNT = 'delete_account';
public const RESTORE_ACCOUNT = 'restore_account';
public const BLOCK_ACCOUNT = 'block_account';
public const COMPLETE_OAUTH_FLOW = 'complete_oauth_flow';
public const MANAGE_OAUTH_SESSIONS = 'manage_oauth_sessions';
public const CREATE_OAUTH_CLIENTS = 'create_oauth_clients';
public const VIEW_OAUTH_CLIENTS = 'view_oauth_clients';
public const MANAGE_OAUTH_CLIENTS = 'manage_oauth_clients';
public const string OBTAIN_ACCOUNT_INFO = 'obtain_account_info';
public const string CHANGE_ACCOUNT_LANGUAGE = 'change_account_language';
public const string CHANGE_ACCOUNT_USERNAME = 'change_account_username';
public const string CHANGE_ACCOUNT_PASSWORD = 'change_account_password';
public const string CHANGE_ACCOUNT_EMAIL = 'change_account_email';
public const string MANAGE_TWO_FACTOR_AUTH = 'manage_two_factor_auth';
public const string DELETE_ACCOUNT = 'delete_account';
public const string RESTORE_ACCOUNT = 'restore_account';
public const string BLOCK_ACCOUNT = 'block_account';
public const string COMPLETE_OAUTH_FLOW = 'complete_oauth_flow';
public const string MANAGE_OAUTH_SESSIONS = 'manage_oauth_sessions';
public const string CREATE_OAUTH_CLIENTS = 'create_oauth_clients';
public const string VIEW_OAUTH_CLIENTS = 'view_oauth_clients';
public const string MANAGE_OAUTH_CLIENTS = 'manage_oauth_clients';
// Personal level controller permissions
public const OBTAIN_OWN_ACCOUNT_INFO = 'obtain_own_account_info';
public const OBTAIN_OWN_EXTENDED_ACCOUNT_INFO = 'obtain_own_extended_account_info';
public const CHANGE_OWN_ACCOUNT_LANGUAGE = 'change_own_account_language';
public const ACCEPT_NEW_PROJECT_RULES = 'accept_new_project_rules';
public const CHANGE_OWN_ACCOUNT_USERNAME = 'change_own_account_username';
public const CHANGE_OWN_ACCOUNT_PASSWORD = 'change_own_account_password';
public const CHANGE_OWN_ACCOUNT_EMAIL = 'change_own_account_email';
public const MANAGE_OWN_TWO_FACTOR_AUTH = 'manage_own_two_factor_auth';
public const DELETE_OWN_ACCOUNT = 'delete_own_account';
public const RESTORE_OWN_ACCOUNT = 'restore_own_account';
public const MINECRAFT_SERVER_SESSION = 'minecraft_server_session';
public const MANAGE_OWN_OAUTH_SESSIONS = 'manage_own_oauth_sessions';
public const VIEW_OWN_OAUTH_CLIENTS = 'view_own_oauth_clients';
public const MANAGE_OWN_OAUTH_CLIENTS = 'manage_own_oauth_clients';
public const string OBTAIN_OWN_ACCOUNT_INFO = 'obtain_own_account_info';
public const string OBTAIN_OWN_EXTENDED_ACCOUNT_INFO = 'obtain_own_extended_account_info';
public const string CHANGE_OWN_ACCOUNT_LANGUAGE = 'change_own_account_language';
public const string ACCEPT_NEW_PROJECT_RULES = 'accept_new_project_rules';
public const string CHANGE_OWN_ACCOUNT_USERNAME = 'change_own_account_username';
public const string CHANGE_OWN_ACCOUNT_PASSWORD = 'change_own_account_password';
public const string CHANGE_OWN_ACCOUNT_EMAIL = 'change_own_account_email';
public const string MANAGE_OWN_TWO_FACTOR_AUTH = 'manage_own_two_factor_auth';
public const string DELETE_OWN_ACCOUNT = 'delete_own_account';
public const string RESTORE_OWN_ACCOUNT = 'restore_own_account';
public const string MINECRAFT_SERVER_SESSION = 'minecraft_server_session';
public const string MANAGE_OWN_OAUTH_SESSIONS = 'manage_own_oauth_sessions';
public const string VIEW_OWN_OAUTH_CLIENTS = 'view_own_oauth_clients';
public const string MANAGE_OWN_OAUTH_CLIENTS = 'manage_own_oauth_clients';
// Data permissions
public const OBTAIN_ACCOUNT_EMAIL = 'obtain_account_email';
public const OBTAIN_EXTENDED_ACCOUNT_INFO = 'obtain_account_extended_info';
public const string OBTAIN_ACCOUNT_EMAIL = 'obtain_account_email';
public const string OBTAIN_EXTENDED_ACCOUNT_INFO = 'obtain_account_extended_info';
// Service permissions
public const ESCAPE_IDENTITY_VERIFICATION = 'escape_identity_verification';
public const string ESCAPE_IDENTITY_VERIFICATION = 'escape_identity_verification';
}

View File

@ -5,6 +5,6 @@ namespace api\rbac;
final class Roles {
public const ACCOUNTS_WEB_USER = 'accounts_web_user';
public const string ACCOUNTS_WEB_USER = 'accounts_web_user';
}

View File

@ -36,19 +36,6 @@ class RequestParser implements RequestParserInterface {
$parser = Yii::createObject(JsonParser::class);
$parser->throwException = false;
$result = $parser->parse($rawBody, $contentType);
if (is_string($result)) {
Yii::$app->sentry->captureMessage('Received an empty $result from the parser', [
'inputText' => $rawBody,
'inputTextLength' => mb_strlen($rawBody),
'outputText' => $result,
'contentType' => $contentType,
], [
'level' => 'warning',
]);
return [];
}
if (!empty($result)) {
return $result;
}

View File

@ -3,11 +3,11 @@ namespace api\tests\_pages;
class AccountsRoute extends BasePage {
public function get(int $accountId) {
public function get(int $accountId): void {
$this->getActor()->sendGET("/api/v1/accounts/{$accountId}");
}
public function changePassword(int $accountId, $currentPassword = null, $newPassword = null, $newRePassword = null) {
public function changePassword(int $accountId, $currentPassword = null, $newPassword = null, $newRePassword = null): void {
$this->getActor()->sendPOST("/api/v1/accounts/{$accountId}/password", [
'password' => $currentPassword,
'newPassword' => $newPassword,
@ -15,65 +15,65 @@ class AccountsRoute extends BasePage {
]);
}
public function changeUsername(int $accountId, $currentPassword = null, $newUsername = null) {
public function changeUsername(int $accountId, $currentPassword = null, $newUsername = null): void {
$this->getActor()->sendPOST("/api/v1/accounts/{$accountId}/username", [
'password' => $currentPassword,
'username' => $newUsername,
]);
}
public function changeEmailInitialize(int $accountId, $password = '') {
public function changeEmailInitialize(int $accountId, $password = ''): void {
$this->getActor()->sendPOST("/api/v1/accounts/{$accountId}/email-verification", [
'password' => $password,
]);
}
public function changeEmailSubmitNewEmail(int $accountId, $key = null, $email = null) {
public function changeEmailSubmitNewEmail(int $accountId, $key = null, $email = null): void {
$this->getActor()->sendPOST("/api/v1/accounts/{$accountId}/new-email-verification", [
'key' => $key,
'email' => $email,
]);
}
public function changeEmail(int $accountId, $key = null) {
public function changeEmail(int $accountId, $key = null): void {
$this->getActor()->sendPOST("/api/v1/accounts/{$accountId}/email", [
'key' => $key,
]);
}
public function changeLanguage(int $accountId, $lang = null) {
public function changeLanguage(int $accountId, $lang = null): void {
$this->getActor()->sendPOST("/api/v1/accounts/{$accountId}/language", [
'lang' => $lang,
]);
}
public function acceptRules(int $accountId) {
public function acceptRules(int $accountId): void {
$this->getActor()->sendPOST("/api/v1/accounts/{$accountId}/rules");
}
public function getTwoFactorAuthCredentials(int $accountId) {
public function getTwoFactorAuthCredentials(int $accountId): void {
$this->getActor()->sendGET("/api/v1/accounts/{$accountId}/two-factor-auth");
}
public function enableTwoFactorAuth(int $accountId, $totp = null, $password = null) {
public function enableTwoFactorAuth(int $accountId, $totp = null, $password = null): void {
$this->getActor()->sendPOST("/api/v1/accounts/{$accountId}/two-factor-auth", [
'totp' => $totp,
'password' => $password,
]);
}
public function disableTwoFactorAuth(int $accountId, $totp = null, $password = null) {
public function disableTwoFactorAuth(int $accountId, $totp = null, $password = null): void {
$this->getActor()->sendDELETE("/api/v1/accounts/{$accountId}/two-factor-auth", [
'totp' => $totp,
'password' => $password,
]);
}
public function ban(int $accountId) {
public function ban(int $accountId): void {
$this->getActor()->sendPOST("/api/v1/accounts/{$accountId}/ban");
}
public function pardon(int $accountId) {
public function pardon(int $accountId): void {
$this->getActor()->sendDELETE("/api/v1/accounts/{$accountId}/ban");
}

View File

@ -6,10 +6,10 @@ class AuthenticationRoute extends BasePage {
/**
* @param string $login
* @param string $password
* @param string|bool|null $rememberMeOrToken
* @param bool|string|null $rememberMeOrToken
* @param bool $rememberMe
*/
public function login($login = '', $password = '', $rememberMeOrToken = null, $rememberMe = false) {
public function login(string $login = '', string $password = '', bool|string|null $rememberMeOrToken = null, bool $rememberMe = false): void {
$params = [
'login' => $login,
'password' => $password,
@ -24,14 +24,14 @@ class AuthenticationRoute extends BasePage {
$this->getActor()->sendPOST('/api/authentication/login', $params);
}
public function forgotPassword($login = null, $token = null) {
public function forgotPassword($login = null, $token = null): void {
$this->getActor()->sendPOST('/api/authentication/forgot-password', [
'login' => $login,
'totp' => $token,
]);
}
public function recoverPassword($key = null, $newPassword = null, $newRePassword = null) {
public function recoverPassword($key = null, $newPassword = null, $newRePassword = null): void {
$this->getActor()->sendPOST('/api/authentication/recover-password', [
'key' => $key,
'newPassword' => $newPassword,
@ -39,7 +39,7 @@ class AuthenticationRoute extends BasePage {
]);
}
public function refreshToken($refreshToken = null) {
public function refreshToken($refreshToken = null): void {
$this->getActor()->sendPOST('/api/authentication/refresh-token', [
'refresh_token' => $refreshToken,
]);

View File

@ -5,13 +5,9 @@ use api\tests\FunctionalTester;
class BasePage {
/**
* @var FunctionalTester
*/
private $actor;
public function __construct(FunctionalTester $I) {
$this->actor = $I;
public function __construct(
private readonly FunctionalTester $actor,
) {
}
public function getActor(): FunctionalTester {

View File

@ -3,7 +3,7 @@ namespace api\tests\_pages;
class IdentityInfoRoute extends BasePage {
public function info() {
public function info(): void {
$this->getActor()->sendGET('/api/account/v1/info');
}

View File

@ -3,7 +3,7 @@ namespace api\tests\_pages;
class InternalRoute extends BasePage {
public function info(string $param, string $value) {
public function info(string $param, string $value): void {
$this->getActor()->sendGET('/api/internal/accounts/info', [$param => $value]);
}

View File

@ -3,12 +3,12 @@ namespace api\tests\_pages;
class MojangApiRoute extends BasePage {
public function usernameToUuid($username, $at = null) {
public function usernameToUuid($username, $at = null): void {
$params = $at === null ? [] : ['at' => $at];
$this->getActor()->sendGET("/api/mojang/profiles/{$username}", $params);
}
public function usernamesByUuid($uuid) {
public function usernamesByUuid($uuid): void {
$this->getActor()->sendGET("/api/mojang/profiles/{$uuid}/names");
}

View File

@ -3,7 +3,7 @@ namespace api\tests\_pages;
class OptionsRoute extends BasePage {
public function get() {
public function get(): void {
$this->getActor()->sendGET('/api/options');
}

View File

@ -3,23 +3,23 @@ namespace api\tests\_pages;
class SessionServerRoute extends BasePage {
public function join($params) {
public function join($params): void {
$this->getActor()->sendPOST('/api/minecraft/session/join', $params);
}
public function joinLegacy(array $params) {
public function joinLegacy(array $params): void {
$this->getActor()->sendGET('/api/minecraft/session/legacy/join', $params);
}
public function hasJoined(array $params) {
public function hasJoined(array $params): void {
$this->getActor()->sendGET('/api/minecraft/session/hasJoined', $params);
}
public function hasJoinedLegacy(array $params) {
public function hasJoinedLegacy(array $params): void {
$this->getActor()->sendGET('/api/minecraft/session/legacy/hasJoined', $params);
}
public function profile(string $profileUuid, bool $signed = false) {
public function profile(string $profileUuid, bool $signed = false): void {
$url = "/api/minecraft/session/profile/{$profileUuid}";
if ($signed) {
$url .= '?unsigned=false';

View File

@ -3,15 +3,15 @@ namespace api\tests\_pages;
class SignupRoute extends BasePage {
public function register(array $registrationData) {
public function register(array $registrationData): void {
$this->getActor()->sendPOST('/api/signup', $registrationData);
}
public function sendRepeatMessage($email = '') {
public function sendRepeatMessage($email = ''): void {
$this->getActor()->sendPOST('/api/signup/repeat-message', ['email' => $email]);
}
public function confirm($key = '') {
public function confirm($key = ''): void {
$this->getActor()->sendPOST('/api/signup/confirm', [
'key' => $key,
]);

View File

@ -12,21 +12,21 @@ use Yii;
class FunctionalTester extends Actor {
use FunctionalTesterActions;
public function amAuthenticated(string $asUsername = 'admin') { // Do not declare type
/** @var Account $account */
public function amAuthenticated(string $asUsername = 'admin'): mixed {
/** @var Account|null $account */
$account = Account::findOne(['username' => $asUsername]);
if ($account === null) {
throw new InvalidArgumentException("Cannot find account with username \"{$asUsername}\"");
}
$token = Yii::$app->tokensFactory->createForWebAccount($account);
$this->amBearerAuthenticated((string)$token);
$this->amBearerAuthenticated($token->toString());
return $account->id;
}
public function notLoggedIn(): void {
$this->haveHttpHeader('Authorization', null);
$this->haveHttpHeader('Authorization', '');
Yii::$app->user->logout();
}

View File

@ -4,5 +4,4 @@ use common\config\ConfigLoader;
use yii\helpers\ArrayHelper;
return ArrayHelper::merge(ConfigLoader::load('api'), [
]);

View File

@ -4,5 +4,4 @@ use common\config\ConfigLoader;
use yii\helpers\ArrayHelper;
return ArrayHelper::merge(ConfigLoader::load('api'), [
]);

Some files were not shown because too many files have changed in this diff Show More