2026-08-18 17:37:44 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
namespace Presentation\Console\Commands;
|
|
|
|
|
|
|
|
|
|
use Application\Security\Exceptions\ClientNotFoundException;
|
|
|
|
|
use Application\Security\Models\Entities\Client;
|
|
|
|
|
use Application\Security\Models\ValueObjects\ClientIdentifier;
|
|
|
|
|
use Application\Security\Ports\Repositories\ClientRepository;
|
2026-08-18 17:45:42 +02:00
|
|
|
use Application\Security\Usecases\FindClient\FindClient;
|
|
|
|
|
use Application\Security\Usecases\FindClient\Input as FindClientInput;
|
2026-08-18 17:37:44 +02:00
|
|
|
use Application\Security\Usecases\RegenerateClientSecret\Input;
|
|
|
|
|
use Application\Security\Usecases\RegenerateClientSecret\RegenerateClientSecret;
|
|
|
|
|
use Illuminate\Console\Command;
|
2026-08-18 17:45:42 +02:00
|
|
|
use Presentation\Console\Presenters\Clients\FindClientPresenter;
|
|
|
|
|
use Presentation\Console\Presenters\Clients\FindClientView;
|
|
|
|
|
use Presentation\Console\Presenters\Clients\FoundClientItem;
|
2026-08-18 17:37:44 +02:00
|
|
|
use Presentation\Console\Presenters\Clients\RegenerateClientSecretPresenter;
|
|
|
|
|
use Presentation\Console\Presenters\Clients\RegenerateClientSecretView;
|
|
|
|
|
use Shared\DDD\Usecase\Builder\UsecaseBuilder;
|
|
|
|
|
use Shared\DDD\Usecase\Builder\UsecaseExecutor;
|
|
|
|
|
use Shared\Pagination;
|
|
|
|
|
use Symfony\Component\Console\Completion\CompletionInput;
|
|
|
|
|
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
|
|
|
|
use Symfony\Component\Console\Completion\Suggestion;
|
|
|
|
|
|
|
|
|
|
final class RegenerateClientSecretCommand extends Command
|
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* @var string
|
|
|
|
|
*/
|
|
|
|
|
protected $signature = 'client:regenerate-secret
|
|
|
|
|
{identifier? : The identifier of the client}';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @var string
|
|
|
|
|
*/
|
|
|
|
|
protected $description = 'Regenerate the secret of an API client';
|
|
|
|
|
|
|
|
|
|
public function __construct(
|
|
|
|
|
private readonly ClientRepository $clientRepository,
|
|
|
|
|
) {
|
|
|
|
|
parent::__construct();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[\Override]
|
|
|
|
|
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
|
|
|
|
|
{
|
|
|
|
|
parent::complete($input, $suggestions);
|
|
|
|
|
|
|
|
|
|
if ($input->mustSuggestArgumentValuesFor('identifier')) {
|
|
|
|
|
$clients = $this->clientRepository->findAll(Pagination::ofOffsetAndLimit(0, 100));
|
|
|
|
|
|
|
|
|
|
/** @var list<Suggestion> $items */
|
|
|
|
|
$items = array_map(
|
|
|
|
|
static fn (Client $client): Suggestion => new Suggestion(
|
|
|
|
|
value: $client->identifier->value,
|
|
|
|
|
description: sprintf('%s (%s)', $client->name->value, $client->isActive ? 'Active' : 'Inactive'),
|
|
|
|
|
),
|
|
|
|
|
$clients->getItems(),
|
|
|
|
|
);
|
|
|
|
|
$suggestions->suggestValues($items);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function handle(
|
|
|
|
|
RegenerateClientSecret $usecase,
|
2026-08-18 17:45:42 +02:00
|
|
|
FindClient $findClient,
|
2026-08-18 17:37:44 +02:00
|
|
|
UsecaseExecutor $executor,
|
|
|
|
|
): int {
|
2026-08-18 17:45:42 +02:00
|
|
|
$identifierString = $this->resolveIdentifier($findClient, $executor);
|
2026-08-18 17:37:44 +02:00
|
|
|
|
|
|
|
|
if ($identifierString === null) {
|
|
|
|
|
return self::FAILURE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2026-08-18 17:45:42 +02:00
|
|
|
$clientIdentifier = $this->parseClientIdentifier($identifierString, $findClient, $executor);
|
2026-08-18 17:37:44 +02:00
|
|
|
$client = $this->clientRepository->findByIdentifier($clientIdentifier);
|
|
|
|
|
|
|
|
|
|
if ($client === null) {
|
|
|
|
|
$this->error(sprintf("Client '%s' not found.", $clientIdentifier->value));
|
|
|
|
|
|
|
|
|
|
return self::FAILURE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (! $this->confirmRegeneration($client)) {
|
|
|
|
|
$this->warn('Secret regeneration cancelled.');
|
|
|
|
|
|
|
|
|
|
return self::SUCCESS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$input = new Input($client->id);
|
|
|
|
|
|
|
|
|
|
/** @var RegenerateClientSecretView $view */
|
|
|
|
|
$view = UsecaseBuilder::using($executor)
|
|
|
|
|
->do($usecase)
|
|
|
|
|
->from($input)
|
|
|
|
|
->with(new RegenerateClientSecretPresenter())
|
|
|
|
|
->run();
|
|
|
|
|
|
|
|
|
|
$this->displayResult($client, $view);
|
|
|
|
|
|
|
|
|
|
return self::SUCCESS;
|
|
|
|
|
} catch (ClientNotFoundException | \InvalidArgumentException $e) {
|
|
|
|
|
$this->error($e->getMessage());
|
|
|
|
|
|
|
|
|
|
return self::FAILURE;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-18 17:45:42 +02:00
|
|
|
private function resolveIdentifier(FindClient $findClient, UsecaseExecutor $executor): ?string
|
2026-08-18 17:37:44 +02:00
|
|
|
{
|
|
|
|
|
$raw = $this->argument('identifier');
|
|
|
|
|
|
|
|
|
|
if ($raw !== null) {
|
|
|
|
|
return (string) $raw;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-18 17:45:42 +02:00
|
|
|
/** @var string|null $answer */
|
|
|
|
|
$answer = $this->anticipate(
|
|
|
|
|
'Search client (by name, ID, identifier, or slug):',
|
|
|
|
|
function (string $input) use ($findClient, $executor): array {
|
|
|
|
|
$trimmed = trim($input);
|
|
|
|
|
|
|
|
|
|
if ($trimmed === '') {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
/** @var FindClientView $view */
|
|
|
|
|
$view = UsecaseBuilder::using($executor)
|
|
|
|
|
->do($findClient)
|
|
|
|
|
->from(new FindClientInput($trimmed))
|
|
|
|
|
->with(new FindClientPresenter())
|
|
|
|
|
->run();
|
|
|
|
|
|
|
|
|
|
return array_map(
|
|
|
|
|
static fn (FoundClientItem $item): string => $item->identifier,
|
|
|
|
|
$view->clients,
|
|
|
|
|
);
|
|
|
|
|
} catch (\Throwable) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
);
|
2026-08-18 17:37:44 +02:00
|
|
|
|
2026-08-18 17:45:42 +02:00
|
|
|
if ($answer === null || trim($answer) === '') {
|
|
|
|
|
$this->error('No client specified.');
|
2026-08-18 17:37:44 +02:00
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-18 17:45:42 +02:00
|
|
|
return trim($answer);
|
2026-08-18 17:37:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @throws \InvalidArgumentException
|
|
|
|
|
*/
|
2026-08-18 17:45:42 +02:00
|
|
|
private function parseClientIdentifier(
|
|
|
|
|
string $value,
|
|
|
|
|
FindClient $findClient,
|
|
|
|
|
UsecaseExecutor $executor,
|
|
|
|
|
): ClientIdentifier {
|
2026-08-18 17:37:44 +02:00
|
|
|
try {
|
|
|
|
|
return new ClientIdentifier($value);
|
|
|
|
|
} catch (\InvalidArgumentException) {
|
2026-08-18 17:45:42 +02:00
|
|
|
try {
|
|
|
|
|
/** @var FindClientView $view */
|
|
|
|
|
$view = UsecaseBuilder::using($executor)
|
|
|
|
|
->do($findClient)
|
|
|
|
|
->from(new FindClientInput($value))
|
|
|
|
|
->with(new FindClientPresenter())
|
|
|
|
|
->run();
|
|
|
|
|
|
|
|
|
|
if (count($view->clients) === 1) {
|
|
|
|
|
return new ClientIdentifier($view->clients[0]->identifier);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (count($view->clients) > 1) {
|
|
|
|
|
foreach ($view->clients as $found) {
|
|
|
|
|
if (
|
|
|
|
|
strcasecmp($found->name, $value) === 0 || strcasecmp($found->id, $value) === 0
|
|
|
|
|
) {
|
|
|
|
|
return new ClientIdentifier($found->identifier);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (\Throwable) {
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-18 17:37:44 +02:00
|
|
|
throw new \InvalidArgumentException("Invalid client identifier '{$value}'. Expected a valid client identifier.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function confirmRegeneration(Client $client): bool
|
|
|
|
|
{
|
|
|
|
|
$this->newLine();
|
|
|
|
|
$this->warn(sprintf("Regenerating the secret will immediately invalidate the existing secret for client '%s'.", $client->name->value));
|
|
|
|
|
|
|
|
|
|
return $this->confirm('Do you want to proceed?', false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function displayResult(Client $client, RegenerateClientSecretView $view): void
|
|
|
|
|
{
|
|
|
|
|
$this->newLine();
|
|
|
|
|
$this->info($view->message);
|
|
|
|
|
$this->newLine();
|
|
|
|
|
|
|
|
|
|
$this->table(
|
|
|
|
|
['Field', 'Value'],
|
|
|
|
|
[
|
|
|
|
|
['ID', $client->id->value],
|
|
|
|
|
['Name', $client->name->value],
|
|
|
|
|
['Identifier', $view->identifier],
|
|
|
|
|
['New Secret', $view->secret],
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->newLine();
|
|
|
|
|
$this->warn('WARNING: Store the new secret securely, it cannot be retrieved later.');
|
|
|
|
|
}
|
|
|
|
|
}
|