Basics of auth
Some checks failed
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 54s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 1m4s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m14s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m10s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Successful in 1m19s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Failing after 1m12s

This commit is contained in:
2026-01-02 15:01:26 -05:00
parent b5b6caa400
commit fc6e493355
21 changed files with 51 additions and 189 deletions

View File

@@ -13,8 +13,6 @@ use Siteworxpro\App\Controllers\AccessTokenController;
use Siteworxpro\App\Controllers\AuthorizeController;
use Siteworxpro\App\Controllers\CapabilitiesController;
use Siteworxpro\App\Controllers\HealthcheckController;
use Siteworxpro\App\Controllers\IndexController;
use Siteworxpro\App\Controllers\OpenApiController;
use Siteworxpro\App\Controllers\OpenIdController;
use Siteworxpro\App\Http\JsonResponseFactory;
use Siteworxpro\App\Http\Middleware\CorsMiddleware;

View File

@@ -73,12 +73,6 @@ use Symfony\Component\Console\Output\OutputInterface;
* @method mixed progress(integer $total = null)
* @method Spinner spinner(string $label = null, string ...$characters = null)
* @method mixed padding(integer $length = 0, string $char = '.')
* @method mixed input(string $prompt, Util\Reader\ReaderInterface $reader = null)
* @method mixed confirm(string $prompt, Util\Reader\ReaderInterface $reader = null)
* @method mixed password(string $prompt, Util\Reader\ReaderInterface $reader = null)
* @method mixed checkboxes(string $prompt, array $options, Util\Reader\ReaderInterface $reader = null)
* @method mixed radio(string $prompt, array $options, Util\Reader\ReaderInterface $reader = null)
* @method mixed animation(string $art, TerminalObject\Helper\Sleeper $sleeper = null)
* @method mixed columns(array $data, $column_count = null)
* @method mixed clear()
* @method CLImate clearLine()
@@ -146,6 +140,9 @@ class ClimateOutput extends ConsoleSectionOutput implements ConsoleOutputInterfa
$this->verbosity = $level;
}
/**
* @return int
*/
public function getVerbosity(): int
{
return $this->verbosity;

View File

@@ -21,7 +21,12 @@ class ListClients extends Command
$this->addArgument('client-id', null, 'Filter by client ID');
}
public function __invoke(ArgvInput|InputInterface $input, ClimateOutput|OutputInterface $output): int
/**
* @param ArgvInput|InputInterface $input
* @param ClimateOutput $output
* @return int
*/
public function __invoke(ArgvInput|InputInterface $input, $output): int
{
if ($input->getArgument('client-id')) {
$client = Client::find($input->getArgument('client-id'));
@@ -57,7 +62,7 @@ class ListClients extends Command
$outputArray = [];
$clients->map(function (Client $client) use (&$outputArray, $input) {
$clients->map(function (Client $client) use (&$outputArray) {
$outputValues = [
'ID' => $client->id,
'Name' => $client->name,

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Siteworxpro\App\Cli\Commands\User;
use Illuminate\Database\Eloquent\Collection;
use Siteworxpro\App\Cli\Commands\Command;
use Siteworxpro\App\Models\ClientUser;
use Siteworxpro\App\Models\User;
@@ -18,6 +19,7 @@ class Add extends Command
{
public function __invoke($input, $output): int
{
/** @var Collection<Client> $clients */
$clients = Client::whereJsonContains('grant_types', 'authorization_code')
->get(['id', 'name']);
@@ -51,6 +53,7 @@ class Add extends Command
$email = $helper->ask($input, $output, $emailQuestion);
/** @var User| null $user */
$user = User::where('email', $email)->first();
if ($user) {
$output->yellow('A user with this email already exists. Associating the user with the client.');

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Siteworxpro\App\CommandBus\Handlers\OAuth;
use Illuminate\Support\Collection;
use Siteworxpro\App\Attributes\CommandBus\HandlesCommand;
use Siteworxpro\App\CommandBus\Commands\Command;
use Siteworxpro\App\CommandBus\Exceptions\CommandHandlerException;
@@ -22,7 +23,7 @@ class CreateClient extends CommandHandler
$client = new Client();
$client->name = $command->getClientName();
$client->description = $command->getClientDescription();
$client->grant_types = $command->getClientGrants();
$client->grant_types = new Collection($command->getClientGrants()); // @phpstan-ignore-line assign.propertyType
$client->save();

View File

@@ -103,7 +103,7 @@ final class AuthorizeController extends Controller
if (
isset($request->getCookieParams()['s']) &&
Redis::exists('session:' . $request->getCookieParams()['s'] ?? '')
Redis::exists('session:' . $request->getCookieParams()['s'])
) {
$s = $request->getCookieParams()['s'];
} else {

View File

@@ -1,66 +0,0 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Controllers;
use Nyholm\Psr7\ServerRequest;
use Psr\Http\Message\ResponseInterface;
use Siteworxpro\App\Attributes\Guards;
use Siteworxpro\App\CommandBus\Commands\ExampleCommand;
use Siteworxpro\App\Docs\TokenSecurity;
use Siteworxpro\App\Docs\UnauthorizedResponse;
use Siteworxpro\App\Http\JsonResponseFactory;
use OpenApi\Attributes as OA;
use Siteworxpro\App\Http\Responses\GenericResponse;
use Siteworxpro\App\Services\Facades\CommandBus;
/**
* Class IndexController
*
* This class handles the index route of the application.
*/
final class IndexController extends Controller
{
/**
* Handles the GET request for the index route.
*
* @throws \JsonException
*/
#[Guards\Jwt]
#[Guards\Scope(['get.index', 'status.check'])]
#[Guards\RequireAllScopes]
#[OA\Get(path: '/', security: [new TokenSecurity()], tags: ['Examples'])]
#[OA\Response(
response: '200',
description: 'An Example Response',
content: new OA\JsonContent(ref: '#/components/schemas/GenericResponse')
)]
#[UnauthorizedResponse]
public function get(ServerRequest $request): ResponseInterface
{
$command = new ExampleCommand($request->getQueryParams()['name'] ?? 'Guest');
$greeting = CommandBus::handle($command);
return JsonResponseFactory::createJsonResponse(new GenericResponse('Server is running. ' . $greeting));
}
/**
* Handles the POST request for the index route.
*
* @throws \JsonException
*/
#[Guards\Jwt]
#[Guards\Scope(['post.index'])]
#[OA\Post(path: '/', security: [new TokenSecurity()], tags: ['Examples'])]
#[OA\Response(
response: '200',
description: 'An Example Response',
content: new OA\JsonContent(ref: '#/components/schemas/GenericResponse')
)]
#[UnauthorizedResponse]
public function post(ServerRequest $request): ResponseInterface
{
return JsonResponseFactory::createJsonResponse(new GenericResponse('POST request received'));
}
}

View File

@@ -7,18 +7,15 @@ namespace Siteworxpro\App\GrpcHandlers;
use GRPC\Greeter\GreeterInterface;
use GRPC\Greeter\HelloReply;
use GRPC\Greeter\HelloRequest;
use Siteworxpro\App\CommandBus\Commands\ExampleCommand;
use Siteworxpro\App\Services\Facades\CommandBus;
use Spiral\RoadRunner\GRPC;
class GreeterHandler implements GreeterInterface
{
public function SayHello(GRPC\ContextInterface $ctx, HelloRequest $in): HelloReply // phpcs:ignore
{
$command = new ExampleCommand($in->getName());
$reply = new HelloReply();
$reply->setMessage(CommandBus::handle($command));
$reply->setMessage('Hello ' . $in->getName());
return $reply;
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Siteworxpro\App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model as ORM;
use Siteworxpro\App\Helpers\Ulid;
@@ -12,8 +13,8 @@ use Siteworxpro\App\Helpers\Ulid;
*
* @package Siteworxpro\App\Models
* @method static static|null find(string $id, array $columns = ['*'])
* @method static where(string $column, string $operator = null, string $value = null, string $boolean = 'and')
* @method static whereJsonContains(string $column, mixed $value, string $boolean = 'and', bool $not = false)
* @method static Builder where(string $column, string $operator = null, string $value = null, string $boolean = 'and')
* @method static Builder whereJsonContains(string $column, mixed $value, string $boolean = 'and', bool $not = false)
*/
abstract class Model extends ORM
{

View File

@@ -29,16 +29,15 @@ class AccessTokenRepository implements AccessTokenRepositoryInterface
public function persistNewAccessToken(AccessTokenEntityInterface | AccessToken $accessTokenEntity): void
{
$accessTokenEntity->save();
if ($accessTokenEntity instanceof AccessToken) {
$accessTokenEntity->save();
}
}
public function revokeAccessToken(string $tokenId): void
{
$accessToken = AccessToken::find($tokenId);
if ($accessToken) {
$accessToken->delete();
}
$accessToken?->delete();
}
public function isAccessTokenRevoked(string $tokenId): bool

View File

@@ -18,12 +18,11 @@ class AuthCodeRepository implements AuthCodeRepositoryInterface
public function persistNewAuthCode(AuthCodeEntityInterface | AuthorizationCode $authCodeEntity): void
{
$authCodeEntity->save();
if ($authCodeEntity instanceof AuthorizationCode) {
$authCodeEntity->save();
}
}
/**
* @throws InvalidArgumentException
*/
public function revokeAuthCode(string $codeId): void
{
$authCode = AuthorizationCode::find($codeId);

View File

@@ -17,11 +17,11 @@ class AccessToken extends RedisModel implements AccessTokenEntityInterface
{
use AccessTokenTrait;
private Client |null $client = null;
private ClientEntityInterface | Client |null $client = null;
private string | null $userIdentifier = null;
/** @var ScopeEntityInterface|Scope[] */
/** @var ScopeEntityInterface[]|Scope[] */
private array $scopes = [];
public function getClient(): ClientEntityInterface
@@ -29,7 +29,10 @@ class AccessToken extends RedisModel implements AccessTokenEntityInterface
return $this->client;
}
private function convertToJWT(): Token
/**
* @return Token
*/
private function convertToJWT(): Token // @phpstan-ignore method.unused
{
$this->initJwtConfiguration();

View File

@@ -15,7 +15,7 @@ class AuthorizationCode extends RedisModel implements AuthCodeEntityInterface
{
use AuthCodeTrait;
private Client | null $client = null;
private ClientEntityInterface | Client | null $client = null;
private string | null $userIdentifier = null;

View File

@@ -35,7 +35,7 @@ use Siteworxpro\App\OAuth\ScopeRepository;
* @property string $description
* @property string $private_key
* @property string $encryption_key
* @property Collection<string> $grant_types
* @property Collection $grant_types
* @property bool $confidential
*
* @property-read ClientCapabilities $capabilities
@@ -66,7 +66,10 @@ class Client extends Model implements ClientEntityInterface
public static function byClientId(string $clientId): ?Client
{
return self::where('client_id', $clientId)->first();
/** @var Client|null $client */
$client = self::where('client_id', $clientId)->first();
return $client;
}
/**

View File

@@ -11,7 +11,7 @@ use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
class RefreshToken extends RedisModel implements RefreshTokenEntityInterface
{
private AccessToken | null $accessToken = null;
private AccessTokenEntityInterface | AccessToken | null $accessToken = null;
protected static function getRedisPrefix(): string
{

View File

@@ -18,12 +18,11 @@ class RefreshTokenRepository implements RefreshTokenRepositoryInterface
public function persistNewRefreshToken(RefreshTokenEntityInterface | RefreshToken $refreshTokenEntity): void
{
$refreshTokenEntity->save();
if ($refreshTokenEntity instanceof RefreshToken) {
$refreshTokenEntity->save();
}
}
/**
* @throws InvalidArgumentException
*/
public function revokeRefreshToken(string $tokenId): void
{
$token = RefreshToken::find($tokenId);

View File

@@ -13,7 +13,10 @@ class ScopeRepository implements ScopeRepositoryInterface
{
public function getScopeEntityByIdentifier(string $identifier): ?ScopeEntityInterface
{
return Scope::where('name', $identifier)->first();
/** @var Scope $scope */
$scope = Scope::where('name', $identifier)->first();
return $scope;
}
public function finalizeScopes(

View File

@@ -6,16 +6,16 @@ namespace Siteworxpro\Tests\CommandBus;
use League\Tactician\Exception\CanNotInvokeHandlerException;
use Siteworxpro\App\CommandBus\AttributeLocator;
use Siteworxpro\App\CommandBus\Commands\ExampleCommand;
use Siteworxpro\App\CommandBus\Handlers\ExampleHandler;
use Siteworxpro\Tests\Unit;
class AttributeLocatorTest extends Unit
{
private const array HANDLERS = [
ExampleCommand::class => ExampleHandler::class,
];
/**
* @return void
*/
public function testResolvesFiles(): void
{
$attributeLocator = new AttributeLocator();

View File

@@ -1,32 +0,0 @@
<?php
namespace Siteworxpro\Tests\CommandBus\Handlers;
use Siteworxpro\App\CommandBus\Commands\Command;
use Siteworxpro\App\CommandBus\Commands\ExampleCommand;
use Siteworxpro\App\CommandBus\Handlers\ExampleHandler;
use Siteworxpro\Tests\Unit;
class ExampleHandlerTest extends Unit
{
public function testExampleCommand(): void
{
$command = new ExampleCommand('test payload');
$this->assertEquals('test payload', $command->getName());
$handler = new ExampleHandler();
$result = $handler($command);
$this->assertEquals('Hello, test payload!', $result);
}
public function testThrowsException(): void
{
$class = new readonly class extends Command
{
};
$this->expectException(\TypeError::class);
$handler = new ExampleHandler();
$handler($class);
}
}

View File

@@ -1,48 +0,0 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\Tests\Controllers;
use League\Tactician\CommandBus;
use Siteworxpro\App\Controllers\IndexController;
class IndexControllerTest extends AbstractController
{
/**
* @throws \JsonException|\ReflectionException
*/
public function testGet(): void
{
$this->assertTrue(true);
$this->getContainer()->bind(CommandBus::class, function () {
return \Mockery::mock(CommandBus::class)
->shouldReceive('handle')
->andReturn('Hello World')
->getMock();
});
$controller = new IndexController();
$response = $controller->get($this->getMockRequest());
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('{"message":"Server is running. Hello World"}', (string)$response->getBody());
}
/**
* @throws \JsonException
*/
public function testPost(): void
{
$this->assertTrue(true);
$controller = new IndexController();
$response = $controller->post($this->getMockRequest());
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('{"message":"POST request received"}', (string)$response->getBody());
}
}

View File

@@ -14,6 +14,6 @@ class UlidTest extends Unit
{
$ulid = Ulid::generate();
$this->assertIsString($ulid);
$this->assertEquals(16, strlen($ulid));
$this->assertEquals(26, strlen($ulid));
}
}