Add scope management functionality for clients and enhance client creation process
Some checks failed
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 1m5s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 1m25s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 1m24s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m14s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Failing after 1m20s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Successful in -19s

This commit is contained in:
2026-01-30 12:43:26 -05:00
parent 5ec683890e
commit b4c892c104
8 changed files with 103 additions and 7 deletions

View File

@@ -6,6 +6,8 @@ namespace Siteworxpro\App\Cli\Commands\OAuth;
use Siteworxpro\App\Cli\ClimateOutput; use Siteworxpro\App\Cli\ClimateOutput;
use Siteworxpro\App\Cli\Commands\Command; use Siteworxpro\App\Cli\Commands\Command;
use Siteworxpro\App\OAuth\Entities\Client;
use Siteworxpro\App\OAuth\Entities\Scope;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question; use Symfony\Component\Console\Question\Question;
@@ -30,6 +32,8 @@ class ClientCapabilities extends Command
$output->info('[1] Username Password: ' . ($capabilities['userPass'] ? 'Enabled' : 'Disabled')); $output->info('[1] Username Password: ' . ($capabilities['userPass'] ? 'Enabled' : 'Disabled'));
$output->info('[2] Magic Link: ' . ($capabilities['magicLink'] ? 'Enabled' : 'Disabled')); $output->info('[2] Magic Link: ' . ($capabilities['magicLink'] ? 'Enabled' : 'Disabled'));
$output->info('[3] Social Logins: ' . ($capabilities['socials'] ? 'Enabled' : 'Disabled')); $output->info('[3] Social Logins: ' . ($capabilities['socials'] ? 'Enabled' : 'Disabled'));
$output->info('[4] External Client (require pkce): ' . (!$client->confidential ? 'Enabled' : 'Disabled'));
$output->info('[5] Manage Scopes');
$question = new Question('What do you want to edit: ', ''); $question = new Question('What do you want to edit: ', '');
$selection = $this->helper->ask($input, $output, $question); $selection = $this->helper->ask($input, $output, $question);
@@ -48,6 +52,12 @@ class ClientCapabilities extends Command
case '3': case '3':
$output->info('Social Logins cannot be modified via CLI at this time.'); $output->info('Social Logins cannot be modified via CLI at this time.');
break; break;
case '4':
$client->confidential = !$client->confidential;
break;
case '5':
$this->manageClientScopes($input, $output, $client);
break;
default: default:
$output->error('Invalid selection. Please try again.'); $output->error('Invalid selection. Please try again.');
continue 2; continue 2;
@@ -60,4 +70,48 @@ class ClientCapabilities extends Command
return \Symfony\Component\Console\Command\Command::SUCCESS; return \Symfony\Component\Console\Command\Command::SUCCESS;
} }
private function manageClientScopes($input, ClimateOutput|OutputInterface $output, Client $client): void
{
$allScopes = Scope::all();
$output->info('These are scope that are available for this client.');
$output->info('Type "exit" to finish managing scopes.');
while (true) {
$clientScopes = $client->scopes()->get();
$output->info('Available Scopes:');
/** @var Scope $scope */
foreach ($allScopes as $scope) {
$status = $clientScopes->contains($scope) ? 'Enabled' : 'Disabled';
$output->info("$scope->id - $scope->name $status");
}
$question = new Question('Enter scope ID to toggle (or type "exit" to finish): ', '');
$question->setAutocompleterValues($allScopes->pluck('id')->toArray());
$scopeId = $this->helper->ask($input, $output, $question);
if (strtolower($scopeId) === 'exit') {
break;
}
$scope = $allScopes->where('id', $scopeId)->first();
if ($scope === null) {
$output->error('Scope not found. Please try again.');
continue;
}
if ($clientScopes->contains($scope)) {
$client->disableScope($scope);
$output->info("Scope '$scope->name' disabled for client.");
} else {
$client->enableScope($scope);
$output->info("Scope '$scope->name' enabled for client.");
}
}
}
} }

View File

@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Siteworxpro\App\Cli\Commands\OAuth; namespace Siteworxpro\App\Cli\Commands\OAuth;
use Siteworxpro\App\Cli\ClimateOutput;
use Siteworxpro\App\CommandBus\Commands\CreateClient as CreateClientCommand; use Siteworxpro\App\CommandBus\Commands\CreateClient as CreateClientCommand;
use Siteworxpro\App\CommandBus\Exceptions\CommandHandlerException; use Siteworxpro\App\CommandBus\Exceptions\CommandHandlerException;
use Siteworxpro\App\Models\Enums\ClientGrant as ClientGrantAlias; use Siteworxpro\App\Models\Enums\ClientGrant as ClientGrantAlias;
@@ -12,9 +11,6 @@ use Siteworxpro\App\OAuth\Entities\Client;
use Siteworxpro\App\Services\Facades\CommandBus; use Siteworxpro\App\Services\Facades\CommandBus;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\Question; use Symfony\Component\Console\Question\Question;
@@ -31,8 +27,8 @@ class CreateClient extends \Siteworxpro\App\Cli\Commands\Command
$question = new ChoiceQuestion('Enter client grants', [ $question = new ChoiceQuestion('Enter client grants', [
'authorization_code', 'authorization_code',
'client_credentials',
'refresh_token', 'refresh_token',
'client_credentials',
'password', 'password',
], 0); ], 0);
$question->setMultiselect(true); $question->setMultiselect(true);
@@ -44,6 +40,18 @@ class CreateClient extends \Siteworxpro\App\Cli\Commands\Command
$grantsEnum[] = ClientGrantAlias::from($grant); $grantsEnum[] = ClientGrantAlias::from($grant);
} }
$question = $this->helper->ask(
$input,
$output,
new \Symfony\Component\Console\Question\ConfirmationQuestion(
'External Client (Require PKCE)? (y/N): ',
false,
'/^(y|yes)/i'
)
);
$command = new CreateClientCommand($clientName, $grantsEnum, $clientDescription); $command = new CreateClientCommand($clientName, $grantsEnum, $clientDescription);
try { try {
/** @var Client $client */ /** @var Client $client */

View File

@@ -46,7 +46,7 @@ class ListClients extends Command
'Access Token Url' => Config::get('app.url') . '/client/access_token', 'Access Token Url' => Config::get('app.url') . '/client/access_token',
'OAuth Config Url' => Config::get('app.url') . 'OAuth Config Url' => Config::get('app.url') .
'/client/' . $client->id . '/.well-known/openid-configuration', '/client/' . $client->id . '/.well-known/openid-configuration',
'Scopes' => $client->scopes->toArray(), 'Scopes' => $client->scopes->pluck('name')->toArray(),
'Capabilities' => $client->capabilities->toArray(), 'Capabilities' => $client->capabilities->toArray(),
]); ]);

View File

@@ -14,7 +14,8 @@ readonly class CreateClient extends Command
public function __construct( public function __construct(
private string $clientName, private string $clientName,
private array $clientGrants = [], private array $clientGrants = [],
private string $clientDescription = '' private string $clientDescription = '',
private bool $isExternal = false
) { ) {
foreach ($this->clientGrants as $grant) { foreach ($this->clientGrants as $grant) {
if ($grant instanceof ClientGrant === false) { if ($grant instanceof ClientGrant === false) {
@@ -32,6 +33,14 @@ readonly class CreateClient extends Command
} }
} }
/**
* @return bool
*/
public function isExternal(): bool
{
return $this->isExternal;
}
/** /**
* @return string * @return string
*/ */

View File

@@ -26,6 +26,7 @@ class CreateClient extends CommandHandler
$client->description = $command->getClientDescription(); $client->description = $command->getClientDescription();
$client->grant_types = new Collection($command->getClientGrants()); // @phpstan-ignore-line assign.propertyType $client->grant_types = new Collection($command->getClientGrants()); // @phpstan-ignore-line assign.propertyType
$client->capabilities = new ClientCapabilities(); $client->capabilities = new ClientCapabilities();
$client->confidential = !$command->isExternal();
$client->save(); $client->save();

View File

@@ -14,4 +14,5 @@ namespace Siteworxpro\App\Models;
*/ */
class ClientScope extends Model class ClientScope extends Model
{ {
public $timestamps = false;
} }

View File

@@ -241,4 +241,23 @@ class Client extends Model implements ClientEntityInterface
return $user->verifyPassword($password) ? $user : null; return $user->verifyPassword($password) ? $user : null;
} }
public function disableScope(Scope $scope): void
{
/** @var ClientScope | null $clientScope */
$clientScope = ClientScope::where('client_id', $this->id)
->where('scope_id', $scope->id)
->first();
$clientScope?->delete();
}
public function enableScope(Scope $scope): void
{
$clientScope = new ClientScope();
$clientScope->client_id = $this->id;
$clientScope->scope_id = $scope->id;
$clientScope->save();
}
} }

View File

@@ -20,6 +20,10 @@ class Scope extends Model implements ScopeEntityInterface
{ {
use ScopeTrait; use ScopeTrait;
protected $casts = [
'id' => 'string',
];
public function getIdentifier(): string public function getIdentifier(): string
{ {
return $this->name; return $this->name;