You've already forked php-auth
generated from siteworxpro/Php-Template
more updates
Some checks failed
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m25s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m35s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 2m45s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m36s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Failing after 2m25s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Failing after 1m10s
Some checks failed
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 2m25s
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Successful in 2m35s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Failing after 2m45s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Successful in 2m36s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Failing after 2m25s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Failing after 1m10s
This commit is contained in:
@@ -20,7 +20,6 @@
|
||||
"predis/predis": "^v3.2.0",
|
||||
"siteworxpro/http-status": "0.0.2",
|
||||
"lcobucci/jwt": "^5.6",
|
||||
"adhocore/cli": "^1.9",
|
||||
"robinvdvleuten/ulid": "^5.0",
|
||||
"monolog/monolog": "^3.9",
|
||||
"react/promise": "^3",
|
||||
@@ -32,13 +31,14 @@
|
||||
"league/oauth2-server": "^9.3",
|
||||
"ext-sodium": "*",
|
||||
"league/climate": "^3.10",
|
||||
"hansott/psr7-cookies": "^4.0"
|
||||
"hansott/psr7-cookies": "^4.0",
|
||||
"symfony/console": "^v7.4.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^12.4",
|
||||
"mockery/mockery": "^1.6",
|
||||
"squizlabs/php_codesniffer": "^4.0",
|
||||
"lendable/composer-license-checker": "^1.2",
|
||||
"lendable/composer-license-checker": "^1.3.0",
|
||||
"phpstan/phpstan": "^2.1.31",
|
||||
"kwn/php-rdkafka-stubs": "^2.2"
|
||||
},
|
||||
|
||||
1391
composer.lock
generated
1391
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -3,4 +3,5 @@ drop table if exists client_scopes;
|
||||
drop table if exists scopes;
|
||||
drop table if exists client_redirect_uris;
|
||||
drop table if exists clients;
|
||||
drop table if exists users;
|
||||
drop table if exists users;
|
||||
drop table if exists user_scopes;
|
||||
@@ -38,7 +38,9 @@ create table scopes
|
||||
name varchar not null
|
||||
constraint scopes_name_key
|
||||
unique,
|
||||
description varchar
|
||||
description varchar not null,
|
||||
created_at timestamp default now(),
|
||||
updated_at timestamp default now()
|
||||
);
|
||||
|
||||
create table client_scopes
|
||||
@@ -89,3 +91,20 @@ create table client_users
|
||||
constraint client_users_client_id_user_id_key
|
||||
unique (client_id, user_id)
|
||||
);
|
||||
|
||||
create table user_scopes
|
||||
(
|
||||
id VARCHAR(26) not null
|
||||
constraint user_scopes_pk
|
||||
primary key,
|
||||
user_id VARCHAR(26) not null
|
||||
constraint user_scopes_user_id_fk
|
||||
references users
|
||||
on delete cascade,
|
||||
scope_id VARCHAR(26) not null
|
||||
constraint user_scopes_scope_id_fk
|
||||
references scopes
|
||||
on delete cascade,
|
||||
constraint user_scopes_user_id_scope_id_key
|
||||
unique (user_id, scope_id)
|
||||
);
|
||||
15
src/Api.php
15
src/Api.php
@@ -78,22 +78,17 @@ class Api
|
||||
$this->router = new Router();
|
||||
$this->router->get('/healthz', HealthcheckController::class . '::get');
|
||||
|
||||
$this->router->group('/.well-known', function (RouteGroup $router) {
|
||||
$router->get('/swagger.yaml', OpenApiController::class . '::get');
|
||||
$router->get('/swagger.json', OpenApiController::class . '::get');
|
||||
});
|
||||
|
||||
$this->router->group('/client', function (RouteGroup $group) {
|
||||
$group->get('/capabilities', CapabilitiesController::class . '::get');
|
||||
});
|
||||
|
||||
// Authorize URL
|
||||
$this->router->get('/authorize', AuthorizeController::class . '::get');
|
||||
$this->router->post('/authorize', AuthorizeController::class . '::post');
|
||||
|
||||
$this->router->group('/client', function (RouteGroup $group) {
|
||||
$group->get('/capabilities', CapabilitiesController::class . '::get');
|
||||
$group->post('/access_token', AccessTokenController::class . '::post');
|
||||
});
|
||||
|
||||
$this->router->group('/client/{client_id}', function (RouteGroup $group) {
|
||||
$group->get('/.well-known/openid-configuration', OpenIdController::class . '::get');
|
||||
$group->post('/access_token', AccessTokenController::class . '::post');
|
||||
});
|
||||
|
||||
$this->router->middleware(new CorsMiddleware());
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace Siteworxpro\App\Async;
|
||||
use Siteworxpro\App\Attributes\Async\HandlesMessage;
|
||||
use Siteworxpro\App\Async\Messages\Message;
|
||||
use Siteworxpro\App\Async\Queues\Queue;
|
||||
use Siteworxpro\App\Cli\ClimateOutput;
|
||||
use Siteworxpro\App\Services\Facades\Broker;
|
||||
use Siteworxpro\App\Services\Facades\Logger;
|
||||
|
||||
|
||||
@@ -4,14 +4,17 @@ declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Cli;
|
||||
|
||||
use Ahc\Cli\Application;
|
||||
use Siteworxpro\App\Cli\Commands\Crypt\GenerateKey;
|
||||
use Siteworxpro\App\Cli\Commands\OAuth\AddRedirectUri;
|
||||
use Siteworxpro\App\Cli\Commands\OAuth\AddScope;
|
||||
use Siteworxpro\App\Cli\Commands\OAuth\CreateClient;
|
||||
use Siteworxpro\App\Cli\Commands\OAuth\ListClients;
|
||||
use Siteworxpro\App\Cli\Commands\Queue\Start;
|
||||
use Siteworxpro\App\Cli\Commands\User\Add;
|
||||
use Siteworxpro\App\Helpers\Version;
|
||||
use Siteworxpro\App\Kernel;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
|
||||
class App
|
||||
{
|
||||
@@ -23,27 +26,32 @@ class App
|
||||
public function __construct()
|
||||
{
|
||||
Kernel::boot();
|
||||
$this->app = new Application('Php-Auth', Version::VERSION);
|
||||
|
||||
$this->app->add(new CreateClient());
|
||||
$this->app->add(new AddRedirectUri());
|
||||
$this->app->add(new Add());
|
||||
$this->app->add(new Start());
|
||||
$this->app->add(new GenerateKey());
|
||||
$this->app = new Application('Siteworxpro Auth', Version::VERSION);
|
||||
|
||||
$this->app->setCatchErrors();
|
||||
|
||||
$this->app->addCommand(new CreateClient());
|
||||
$this->app->addCommand(new ListClients());
|
||||
$this->app->addCommand(new AddRedirectUri());
|
||||
$this->app->addCommand(new Add());
|
||||
$this->app->addCommand(new Start());
|
||||
$this->app->addCommand(new GenerateKey());
|
||||
$this->app->addCommand(new AddScope());
|
||||
}
|
||||
|
||||
public function run(): int
|
||||
{
|
||||
$this->app->logo(
|
||||
<<<EOF
|
||||
▄▄▄▄▄ ▄ ▄ ▄▄▄▄▄ ▄▄▄▄▄▄▄ ▀▀█ ▄
|
||||
█ ▀█ █ █ █ ▀█ █ ▄▄▄ ▄▄▄▄▄ ▄▄▄▄ █ ▄▄▄ ▄▄█▄▄ ▄▄▄
|
||||
█▄▄▄█▀ █▄▄▄▄█ █▄▄▄█▀ █ █▀ █ █ █ █ █▀ ▀█ █ ▀ █ █ █▀ █
|
||||
█ █ █ █ ▀▀▀ █ █▀▀▀▀ █ █ █ █ █ █ ▄▀▀▀█ █ █▀▀▀▀
|
||||
█ █ █ █ █ ▀█▄▄▀ █ █ █ ██▄█▀ ▀▄▄ ▀▄▄▀█ ▀▄▄ ▀█▄▄▀
|
||||
█
|
||||
EOF
|
||||
);
|
||||
return $this->app->handle($_SERVER['argv']);
|
||||
|
||||
$output = new ClimateOutput();
|
||||
|
||||
try {
|
||||
return $this->app->run(new ArgvInput(), $output);
|
||||
} catch (\Exception $e) {
|
||||
$output->error($e->getMessage());
|
||||
$output->error($e->getTraceAsString());
|
||||
|
||||
return $e->getCode() ?: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
213
src/Cli/ClimateOutput.php
Normal file
213
src/Cli/ClimateOutput.php
Normal file
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Cli;
|
||||
|
||||
use League\CLImate\CLImate;
|
||||
use League\CLImate\TerminalObject\Dynamic\Spinner;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatterInterface;
|
||||
use Symfony\Component\Console\Output\ConsoleOutputInterface;
|
||||
use Symfony\Component\Console\Output\ConsoleSectionOutput;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* @method CLImate black(string $str = null)
|
||||
* @method CLImate red(string $str = null)
|
||||
* @method CLImate green(string $str = null)
|
||||
* @method CLImate yellow(string $str = null)
|
||||
* @method CLImate blue(string $str = null)
|
||||
* @method CLImate magenta(string $str = null)
|
||||
* @method CLImate cyan(string $str = null)
|
||||
* @method CLImate lightGray(string $str = null)
|
||||
* @method CLImate darkGray(string $str = null)
|
||||
* @method CLImate lightRed(string $str = null)
|
||||
* @method CLImate lightGreen(string $str = null)
|
||||
* @method CLImate lightYellow(string $str = null)
|
||||
* @method CLImate lightBlue(string $str = null)
|
||||
* @method CLImate lightMagenta(string $str = null)
|
||||
* @method CLImate lightCyan(string $str = null)
|
||||
* @method CLImate white(string $str = null)
|
||||
*
|
||||
* @method CLImate backgroundBlack(string $str = null)
|
||||
* @method CLImate backgroundRed(string $str = null)
|
||||
* @method CLImate backgroundGreen(string $str = null)
|
||||
* @method CLImate backgroundYellow(string $str = null)
|
||||
* @method CLImate backgroundBlue(string $str = null)
|
||||
* @method CLImate backgroundMagenta(string $str = null)
|
||||
* @method CLImate backgroundCyan(string $str = null)
|
||||
* @method CLImate backgroundLightGray(string $str = null)
|
||||
* @method CLImate backgroundDarkGray(string $str = null)
|
||||
* @method CLImate backgroundLightRed(string $str = null)
|
||||
* @method CLImate backgroundLightGreen(string $str = null)
|
||||
* @method CLImate backgroundLightYellow(string $str = null)
|
||||
* @method CLImate backgroundLightBlue(string $str = null)
|
||||
* @method CLImate backgroundLightMagenta(string $str = null)
|
||||
* @method CLImate backgroundLightCyan(string $str = null)
|
||||
* @method CLImate backgroundWhite(string $str = null)
|
||||
*
|
||||
* @method CLImate bold(string $str = null)
|
||||
* @method CLImate dim(string $str = null)
|
||||
* @method CLImate underline(string $str = null)
|
||||
* @method CLImate blink(string $str = null)
|
||||
* @method CLImate invert(string $str = null)
|
||||
* @method CLImate hidden(string $str = null)
|
||||
*
|
||||
* @method CLImate info(string $str = null)
|
||||
* @method CLImate comment(string $str = null)
|
||||
* @method CLImate whisper(string $str = null)
|
||||
* @method CLImate shout(string $str = null)
|
||||
* @method CLImate error(string $str = null)
|
||||
*
|
||||
* @method mixed out(string $str)
|
||||
* @method mixed inline(string $str)
|
||||
* @method mixed table(array $data)
|
||||
* @method mixed json(mixed $var)
|
||||
* @method mixed br($count = 1)
|
||||
* @method mixed tab($count = 1)
|
||||
* @method mixed draw(string $art)
|
||||
* @method mixed border(string $char = null, integer $length = null)
|
||||
* @method mixed dump(mixed $var)
|
||||
* @method mixed flank(string $output, string $char = null, integer $length = null)
|
||||
* @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()
|
||||
*
|
||||
* @method CLImate addArt(string $dir)
|
||||
*/
|
||||
class ClimateOutput extends ConsoleSectionOutput implements ConsoleOutputInterface
|
||||
{
|
||||
private CLImate $CLImate;
|
||||
|
||||
private OutputFormatterInterface|OutputFormatter $formatter;
|
||||
|
||||
private int $verbosity = self::VERBOSITY_NORMAL;
|
||||
|
||||
private bool $decorated = true;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->CLImate = new CLImate();
|
||||
$this->formatter = new OutputFormatter($this->isDecorated());
|
||||
|
||||
// create stream output
|
||||
$output = fopen('php://stdout', 'w');
|
||||
stream_set_blocking($output, true);
|
||||
|
||||
$sections = [];
|
||||
|
||||
parent::__construct(
|
||||
$output,
|
||||
$sections,
|
||||
$this->getVerbosity(),
|
||||
$this->isDecorated(),
|
||||
$this->getFormatter()
|
||||
);
|
||||
}
|
||||
|
||||
public function write(iterable|string $messages, bool $newline = false, int $options = 0): void
|
||||
{
|
||||
if (is_string($messages)) {
|
||||
$messages = [$messages];
|
||||
}
|
||||
|
||||
foreach ($messages as $message) {
|
||||
$message = $this->formatter->format($message);
|
||||
if (!$newline) {
|
||||
$this->CLImate->inline($message);
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->CLImate->out($message);
|
||||
}
|
||||
}
|
||||
|
||||
protected function doWrite(string $message, bool $newline): void
|
||||
{
|
||||
if ($newline) {
|
||||
$this->CLImate->out($message);
|
||||
} else {
|
||||
$this->CLImate->inline($message);
|
||||
}
|
||||
}
|
||||
|
||||
public function setVerbosity(int $level): void
|
||||
{
|
||||
$this->verbosity = $level;
|
||||
}
|
||||
|
||||
public function getVerbosity(): int
|
||||
{
|
||||
return $this->verbosity;
|
||||
}
|
||||
|
||||
public function isQuiet(): bool
|
||||
{
|
||||
return $this->verbosity === self::VERBOSITY_QUIET;
|
||||
}
|
||||
|
||||
public function isVerbose(): bool
|
||||
{
|
||||
return $this->verbosity >= self::VERBOSITY_VERBOSE;
|
||||
}
|
||||
|
||||
public function isVeryVerbose(): bool
|
||||
{
|
||||
return $this->verbosity >= self::VERBOSITY_VERY_VERBOSE;
|
||||
}
|
||||
|
||||
public function isDebug(): bool
|
||||
{
|
||||
return $this->verbosity >= self::VERBOSITY_DEBUG;
|
||||
}
|
||||
|
||||
public function setDecorated(bool $decorated): void
|
||||
{
|
||||
$this->decorated = $decorated;
|
||||
}
|
||||
|
||||
public function isDecorated(): bool
|
||||
{
|
||||
return $this->decorated;
|
||||
}
|
||||
|
||||
public function setFormatter(OutputFormatterInterface $formatter): void
|
||||
{
|
||||
$this->formatter = $formatter;
|
||||
}
|
||||
|
||||
public function getFormatter(): OutputFormatterInterface
|
||||
{
|
||||
return $this->formatter;
|
||||
}
|
||||
|
||||
public function __call(string $name, array $arguments)
|
||||
{
|
||||
return $this->CLImate->$name(...$arguments);
|
||||
}
|
||||
|
||||
public function getErrorOutput(): OutputInterface
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setErrorOutput(OutputInterface $error): void
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
|
||||
public function section(): ConsoleSectionOutput
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -4,17 +4,18 @@ declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Cli\Commands;
|
||||
|
||||
use Ahc\Cli\Application as App;
|
||||
use League\CLImate\CLImate;
|
||||
use Symfony\Component\Console\Helper\HelperSet;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
|
||||
abstract class Command extends \Ahc\Cli\Input\Command implements CommandInterface
|
||||
abstract class Command extends \Symfony\Component\Console\Command\Command implements CommandInterface
|
||||
{
|
||||
protected Climate $climate;
|
||||
protected QuestionHelper $helper;
|
||||
|
||||
public function __construct(string $_name, string $_desc = '', bool $_allowUnknown = false, ?App $_app = null)
|
||||
public function __construct(?string $name = null, ?callable $code = null)
|
||||
{
|
||||
parent::__construct($_name, $_desc, $_allowUnknown, $_app);
|
||||
parent::__construct($name, $code);
|
||||
|
||||
$this->climate = new CLImate();
|
||||
$this->helper = new QuestionHelper();
|
||||
$this->setHelperSet(new HelperSet([$this->helper]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Cli\Commands;
|
||||
|
||||
use Siteworxpro\App\Cli\ClimateOutput;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
interface CommandInterface
|
||||
{
|
||||
/**
|
||||
@@ -11,5 +16,5 @@ interface CommandInterface
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function execute(): int;
|
||||
public function __invoke(InputInterface | ArgvInput $input, OutputInterface | ClimateOutput $output): int;
|
||||
}
|
||||
|
||||
@@ -5,25 +5,23 @@ declare(strict_types=1);
|
||||
namespace Siteworxpro\App\Cli\Commands\Crypt;
|
||||
|
||||
use Random\RandomException;
|
||||
use Siteworxpro\App\Cli\ClimateOutput;
|
||||
use Siteworxpro\App\Cli\Commands\Command;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
|
||||
#[AsCommand('crypt:generate-key', 'Generate a new encryption key for the application')]
|
||||
class GenerateKey extends Command
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('crypt:generate-key', 'Generate a new encryption key for the application');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RandomException
|
||||
*/
|
||||
public function execute(): int
|
||||
public function __invoke($input, $output): int
|
||||
{
|
||||
$key = base64_encode(random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES));
|
||||
|
||||
$this->climate->info('Generated Encryption Key:');
|
||||
$this->climate->out('base64:' . $key);
|
||||
$output->info('Generated Encryption Key:');
|
||||
$output->out('base64:' . $key);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -5,51 +5,52 @@ declare(strict_types=1);
|
||||
namespace Siteworxpro\App\Cli\Commands\OAuth;
|
||||
|
||||
use League\CLImate\TerminalObject\Dynamic\Input;
|
||||
use Siteworxpro\App\Cli\ClimateOutput;
|
||||
use Siteworxpro\App\Models\ClientRedirectUri;
|
||||
use Siteworxpro\App\OAuth\Entities\Client;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
|
||||
#[AsCommand(name: 'oauth:redirect-uri:create', description: 'Add a redirect URI to an existing OAuth client.')]
|
||||
class AddRedirectUri extends \Siteworxpro\App\Cli\Commands\Command
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('oauth:redirect-uri:add', 'Add a redirect URI to an existing OAuth client.');
|
||||
}
|
||||
|
||||
public function execute(): int
|
||||
public function __invoke($input, $output): int
|
||||
{
|
||||
$clients = Client::all('id', 'name');
|
||||
|
||||
/** @var Input $input */
|
||||
$input = $this->climate->input(
|
||||
'Select the OAuth client to add a redirect URI to' . PHP_EOL .
|
||||
$clients->map(fn(Client $client) => "[$client->id $client->name]")->implode(PHP_EOL) .
|
||||
PHP_EOL .
|
||||
'Enter the client ID: '
|
||||
);
|
||||
$input->accept(
|
||||
$clients->pluck('id')->toArray()
|
||||
);
|
||||
$clients->map(function (Client $client) use ($output) {
|
||||
$output->info("[$client->id] $client->name");
|
||||
});
|
||||
|
||||
$id = $input->prompt();
|
||||
$question = new Question('Enter the client ID: ');
|
||||
$question->setAutocompleterValues($clients->pluck('id')->toArray());
|
||||
|
||||
$client = Client::find($id);
|
||||
$clientId = $this->helper->ask($input, $output, $question);
|
||||
$client = Client::find($clientId);
|
||||
if (!$client) {
|
||||
$this->climate->error('Client not found.');
|
||||
return 1;
|
||||
$output->error('Client not found. Not a valid client ID.');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
/** @var Input $uriInput */
|
||||
$uriInput = $this->climate->input('Enter the redirect URI to add: ');
|
||||
$uriInput->accept(function (string $value) {
|
||||
return filter_var($value, FILTER_VALIDATE_URL) !== false;
|
||||
}, 'Please enter a valid URL.');
|
||||
$question = new Question('Enter the redirect URI to add: ');
|
||||
$question->setValidator(function ($input): string {
|
||||
if (filter_var($input, FILTER_VALIDATE_URL) === false) {
|
||||
throw new \InvalidArgumentException('Please enter a valid URL.');
|
||||
}
|
||||
|
||||
$redirectUri = $uriInput->prompt();
|
||||
return $input;
|
||||
});
|
||||
|
||||
$redirectUri = $this->helper->ask($input, $output, $question);
|
||||
|
||||
$redirectUris = $client->clientRedirectUris;
|
||||
if (in_array($redirectUri, $redirectUris->toArray(), true)) {
|
||||
$this->climate->error('The redirect URI already exists for this client.');
|
||||
return 1;
|
||||
$output->error('The redirect URI already exists for this client.');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$clientRedirectUri = new ClientRedirectUri();
|
||||
@@ -57,6 +58,8 @@ class AddRedirectUri extends \Siteworxpro\App\Cli\Commands\Command
|
||||
$clientRedirectUri->redirect_uri = $redirectUri;
|
||||
$clientRedirectUri->save();
|
||||
|
||||
return 0;
|
||||
$output->green('Redirect URI added successfully.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
46
src/Cli/Commands/OAuth/AddScope.php
Normal file
46
src/Cli/Commands/OAuth/AddScope.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Cli\Commands\OAuth;
|
||||
|
||||
use Siteworxpro\App\Cli\Commands\Command;
|
||||
use Siteworxpro\App\OAuth\Entities\Scope;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command as SympCommand;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
#[AsCommand('oauth:scope:add', 'Add a new OAuth scope')]
|
||||
class AddScope extends Command
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addArgument('name', InputArgument::REQUIRED, 'The name of the scope')
|
||||
->addArgument('description', InputArgument::OPTIONAL, 'The description of the scope');
|
||||
}
|
||||
|
||||
public function __invoke(ArgvInput | InputInterface $input, $output): int
|
||||
{
|
||||
$name = $input->getArgument('name');
|
||||
$description = $input->getArgument('description') ?? '';
|
||||
|
||||
$scope = Scope::where('name', $name)->first();
|
||||
if ($scope) {
|
||||
$output->error("Scope with name '$name' already exists.");
|
||||
|
||||
return SympCommand::FAILURE;
|
||||
}
|
||||
|
||||
$scope = new Scope();
|
||||
$scope->name = $name;
|
||||
$scope->description = $description;
|
||||
$scope->save();
|
||||
|
||||
$output->green("Scope '$name' added successfully.");
|
||||
|
||||
return SympCommand::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -4,45 +4,54 @@ declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Cli\Commands\OAuth;
|
||||
|
||||
use Ahc\Cli\IO\Interactor;
|
||||
use Siteworxpro\App\Cli\ClimateOutput;
|
||||
use Siteworxpro\App\CommandBus\Commands\CreateClient as CreateClientCommand;
|
||||
use Siteworxpro\App\CommandBus\Exceptions\CommandHandlerException;
|
||||
use Siteworxpro\App\OAuth\Entities\Client;
|
||||
use Siteworxpro\App\Services\Facades\CommandBus;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
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\Question;
|
||||
|
||||
#[AsCommand(name: 'oauth:client:create', description: 'Create a new OAuth client.')]
|
||||
class CreateClient extends \Siteworxpro\App\Cli\Commands\Command
|
||||
{
|
||||
public function __construct()
|
||||
public function __invoke($input, $output): int
|
||||
{
|
||||
parent::__construct('oauth:client:create', 'Create a new OAuth client.');
|
||||
}
|
||||
$question = new Question('Enter client name: ');
|
||||
$clientName = $this->helper->ask($input, $output, $question);
|
||||
|
||||
public function execute(): int
|
||||
{
|
||||
$interactor = new Interactor();
|
||||
$clientName = $interactor->prompt('Enter client name');
|
||||
$clientDescription = $interactor->prompt('Enter client description (optional)', '');
|
||||
$clientGrantsString = $interactor->prompt(
|
||||
'Enter client grants (comma separated, e.g. "authorization_code,refresh_token")',
|
||||
false
|
||||
);
|
||||
$question = new Question('Enter client description (optional): ', '');
|
||||
$clientDescription = $this->helper->ask($input, $output, $question);
|
||||
|
||||
$grants = explode(',', $clientGrantsString);
|
||||
$question = new ChoiceQuestion('Enter client grants', [
|
||||
'authorization_code',
|
||||
'client_credentials',
|
||||
'refresh_token',
|
||||
'password',
|
||||
], 0);
|
||||
$question->setMultiselect(true);
|
||||
|
||||
$grants = $this->helper->ask($input, $output, $question);
|
||||
|
||||
$command = new CreateClientCommand($clientName, $grants, $clientDescription);
|
||||
try {
|
||||
/** @var Client $client */
|
||||
$client = CommandBus::handle($command);
|
||||
|
||||
$this->climate->green('OAuth client created successfully');
|
||||
$this->climate->info('Client ID: ' . $client->client_id);
|
||||
$this->climate->info('Client Secret: ' . $client->client_secret)->br(2);
|
||||
$output->green('OAuth client created successfully');
|
||||
$output->info('Client ID: ' . $client->client_id);
|
||||
$output->info('Client Secret: ' . $client->client_secret)->br(2);
|
||||
} catch (CommandHandlerException $exception) {
|
||||
$this->climate->error($exception->getMessage());
|
||||
$output->error($exception->getMessage());
|
||||
|
||||
return 1;
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
return 0;
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
75
src/Cli/Commands/OAuth/ListClients.php
Normal file
75
src/Cli/Commands/OAuth/ListClients.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Cli\Commands\OAuth;
|
||||
|
||||
use Siteworxpro\App\Cli\ClimateOutput;
|
||||
use Siteworxpro\App\Cli\Commands\Command;
|
||||
use Siteworxpro\App\OAuth\Entities\Client;
|
||||
use Siteworxpro\App\Services\Facades\Config;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
#[AsCommand('oauth:client:list', 'List all OAuth clients')]
|
||||
class ListClients extends Command
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addArgument('client-id', null, 'Filter by client ID');
|
||||
}
|
||||
|
||||
public function __invoke(ArgvInput|InputInterface $input, ClimateOutput|OutputInterface $output): int
|
||||
{
|
||||
if ($input->getArgument('client-id')) {
|
||||
$client = Client::find($input->getArgument('client-id'));
|
||||
|
||||
if (!$client) {
|
||||
$output->red('No OAuth client found with the specified client ID.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$output->json([
|
||||
'ID' => $client->id,
|
||||
'Name' => $client->name,
|
||||
'Client ID' => $client->client_id,
|
||||
'Client Secret' => $client->client_secret,
|
||||
'Grant Types' => implode(', ', $client->grant_types->toArray()),
|
||||
'Access Token Url' => Config::get('app.url') . '/client/access_token',
|
||||
'OAuth Config Url' => Config::get('app.url') .
|
||||
'/client/' . $client->id . '/.well-known/openid-configuration',
|
||||
'Scopes' => $client->scopes->toArray()
|
||||
]);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$clients = Client::all('id', 'name', 'client_id', 'client_secret', 'grant_types');
|
||||
|
||||
if ($clients->isEmpty()) {
|
||||
$output->yellow('No OAuth clients found.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$outputArray = [];
|
||||
|
||||
$clients->map(function (Client $client) use (&$outputArray, $input) {
|
||||
$outputValues = [
|
||||
'ID' => $client->id,
|
||||
'Name' => $client->name,
|
||||
'Client ID' => $client->client_id,
|
||||
'Grant Types' => implode(', ', $client->grant_types->toArray()),
|
||||
];
|
||||
|
||||
$outputArray[] = $outputValues;
|
||||
});
|
||||
|
||||
$output->table($outputArray);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -4,29 +4,41 @@ declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Cli\Commands\Queue;
|
||||
|
||||
use Ahc\Cli\Input\Command;
|
||||
use Siteworxpro\App\Async\Consumer;
|
||||
use Siteworxpro\App\Async\Messages\SayHelloMessage;
|
||||
use Siteworxpro\App\Cli\Commands\Command;
|
||||
use Siteworxpro\App\Cli\Commands\CommandInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
|
||||
#[AsCommand('queue:start', 'Start the queue consumer')]
|
||||
class Start extends Command implements CommandInterface
|
||||
{
|
||||
public function __construct()
|
||||
protected function configure(): void
|
||||
{
|
||||
parent::__construct('queue:start', 'Start the queue consumer to process messages.');
|
||||
$this->argument('[queues]', 'The name of the queue to consume from. ex. "first_queue,second_queue"');
|
||||
$this->addArgument(
|
||||
'queues',
|
||||
InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
|
||||
'Space separated list of queues to consume from. If omitted the default queue is used.',
|
||||
['default']
|
||||
);
|
||||
}
|
||||
|
||||
public function execute(): int
|
||||
public function __invoke($input, $output): int
|
||||
{
|
||||
$queues = [];
|
||||
if ($this->values()['queues'] !== null) {
|
||||
$queues = explode(',', $this->values()['queues']);
|
||||
$queues = $input->getArgument('queues');
|
||||
|
||||
if ($output->isVerbose()) {
|
||||
$output->lightCyan('Consuming from queues: ' . implode(', ', $queues))->br();
|
||||
}
|
||||
|
||||
$consumer = new Consumer($queues);
|
||||
|
||||
if ($output->isVerbose()) {
|
||||
$output->lightCyan('Queue consumer started. Waiting for messages...')->br();
|
||||
}
|
||||
|
||||
$consumer->start();
|
||||
|
||||
return 0;
|
||||
return \Symfony\Component\Console\Command\Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,60 +8,70 @@ use Siteworxpro\App\Cli\Commands\Command;
|
||||
use Siteworxpro\App\Models\ClientUser;
|
||||
use Siteworxpro\App\Models\User;
|
||||
use Siteworxpro\App\OAuth\Entities\Client;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command as SCommand;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Question\Question as QuestionInput;
|
||||
|
||||
#[AsCommand('user:add', 'Add a new user associated with an OAuth client')]
|
||||
class Add extends Command
|
||||
{
|
||||
public function __construct()
|
||||
public function __invoke($input, $output): int
|
||||
{
|
||||
parent::__construct('user:add', 'Add a new user to the system');
|
||||
}
|
||||
$clients = Client::whereJsonContains('grant_types', 'authorization_code')
|
||||
->get(['id', 'name']);
|
||||
|
||||
public function execute(): int
|
||||
{
|
||||
$clients = Client::all(['id', 'name']);
|
||||
|
||||
$this->climate->info('Available OAuth Clients:');
|
||||
$output->info('Available OAuth Clients:');
|
||||
foreach ($clients as $client) {
|
||||
$this->climate->out("[$client->id] $client->name");
|
||||
$output->out("[$client->id] $client->name");
|
||||
}
|
||||
|
||||
$input = $this->climate->input('Enter the client ID to associate the new user with: ');
|
||||
$input->accept(
|
||||
$clients->pluck('id')->toArray()
|
||||
);
|
||||
$question = new QuestionInput('Enter the client ID to associate the new user with: ');
|
||||
$question->setAutocompleterValues($clients->pluck('id')->toArray());
|
||||
|
||||
$id = $input->prompt();
|
||||
/** @var QuestionHelper $helper */
|
||||
$helper = $this->getHelper('question');
|
||||
$id = $helper->ask($input, $output, $question);
|
||||
|
||||
$client = Client::find($id);
|
||||
if (!$client) {
|
||||
$this->climate->error('Client not found.');
|
||||
return 1;
|
||||
$output->error('Client not found.');
|
||||
return SCommand::FAILURE;
|
||||
}
|
||||
|
||||
$this->climate->info("Adding a new user for client: $client->name");
|
||||
$output->info("Adding a new user for client: $client->name");
|
||||
$emailQuestion = new QuestionInput('Enter the user\'s email: ');
|
||||
$emailQuestion->setValidator(function ($input): string {
|
||||
if (empty($input) || !filter_var($input, FILTER_VALIDATE_EMAIL)) {
|
||||
throw new \InvalidArgumentException('Invalid email address.');
|
||||
}
|
||||
|
||||
$input = $this->climate->input('Enter the user\'s email:');
|
||||
$email = $input->prompt();
|
||||
return $input;
|
||||
});
|
||||
|
||||
$email = $helper->ask($input, $output, $emailQuestion);
|
||||
|
||||
$user = User::where('email', $email)->first();
|
||||
if ($user) {
|
||||
$this->climate->error('A user with this email already exists. Associating the user with the client.');
|
||||
$output->yellow('A user with this email already exists. Associating the user with the client.');
|
||||
$clientUser = new ClientUser();
|
||||
$clientUser->client_id = $client->id;
|
||||
$clientUser->user_id = $user->id;
|
||||
$clientUser->save();
|
||||
|
||||
return 0;
|
||||
return SCommand::SUCCESS;
|
||||
}
|
||||
|
||||
$passwordInput = $this->climate->password('Enter the user\'s password: 🔐');
|
||||
$password = $passwordInput->prompt();
|
||||
$passwordQuestion = new QuestionInput('Enter the user\'s password: ');
|
||||
$passwordQuestion->setHidden(true);
|
||||
$passwordQuestion->setHiddenFallback(false);
|
||||
$password = $helper->ask($input, $output, $passwordQuestion);
|
||||
|
||||
$firstNameInput = $this->climate->input('Enter the user\'s first name: ');
|
||||
$firstName = $firstNameInput->prompt();
|
||||
$firstNameQuestion = new QuestionInput('Enter the user\'s first name: ');
|
||||
$firstName = $helper->ask($input, $output, $firstNameQuestion);
|
||||
|
||||
$lastNameInput = $this->climate->input('Enter the user\'s last name: ');
|
||||
$lastName = $lastNameInput->prompt();
|
||||
$lastNameQuestion = new QuestionInput('Enter the user\'s last name: ');
|
||||
$lastName = $helper->ask($input, $output, $lastNameQuestion);
|
||||
|
||||
$user = new User();
|
||||
$user->email = $email;
|
||||
@@ -75,6 +85,8 @@ class Add extends Command
|
||||
$clientUser->user_id = $user->id;
|
||||
$clientUser->save();
|
||||
|
||||
return 0;
|
||||
$output->green('User added and associated with the client successfully.');
|
||||
|
||||
return SCommand::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ use Nyholm\Psr7\ServerRequest;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Siteworxpro\App\Http\JsonResponseFactory;
|
||||
use Siteworxpro\App\Http\Responses\GenericResponse;
|
||||
use Siteworxpro\App\OAuth\Entities\AuthorizationCode;
|
||||
use Siteworxpro\App\OAuth\Entities\Client;
|
||||
use Siteworxpro\HttpStatus\CodesEnum;
|
||||
|
||||
@@ -23,13 +22,11 @@ final class AccessTokenController extends Controller
|
||||
* @throws BadFormatException
|
||||
* @throws EnvironmentIsBrokenException
|
||||
* @throws \JsonException
|
||||
* @throws OAuthServerException
|
||||
*/
|
||||
public function post(ServerRequest $request): ResponseInterface
|
||||
{
|
||||
try {
|
||||
$grantType = $request->getParsedBody()['grant_type'] ?? null;
|
||||
$client = Client::find($request->getAttribute('client_id'));
|
||||
$client = Client::byClientId($request->getParsedBody()['client_id'] ?? '');
|
||||
if ($client === null) {
|
||||
return JsonResponseFactory::createJsonResponse(
|
||||
new GenericResponse('Invalid client'),
|
||||
@@ -37,30 +34,14 @@ final class AccessTokenController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
switch ($grantType) {
|
||||
case 'authorization_code':
|
||||
return $client
|
||||
->getAuthorizationServer()
|
||||
->respondToAccessTokenRequest($request, JsonResponseFactory::createJsonResponse([]));
|
||||
|
||||
case 'refresh_token':
|
||||
break;
|
||||
default:
|
||||
return JsonResponseFactory::createJsonResponse(
|
||||
new GenericResponse('Unsupported grant type'),
|
||||
CodesEnum::BAD,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
$response = $this->authorizationServer->respondToAccessTokenRequest(
|
||||
$request,
|
||||
new Response(),
|
||||
);
|
||||
|
||||
return $response;
|
||||
return $client
|
||||
->getAuthorizationServer()
|
||||
->respondToAccessTokenRequest($request, JsonResponseFactory::createJsonResponse([]));
|
||||
} catch (OAuthServerException $e) {
|
||||
return $e->generateHttpResponse(new Response());
|
||||
return JsonResponseFactory::createJsonResponse(
|
||||
$e->getPayload(),
|
||||
CodesEnum::fromCode($e->getHttpStatusCode()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ 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)
|
||||
*/
|
||||
abstract class Model extends ORM
|
||||
{
|
||||
|
||||
@@ -6,10 +6,12 @@ namespace Siteworxpro\App\OAuth\Entities;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use DateTimeImmutable;
|
||||
use Lcobucci\JWT\Token;
|
||||
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
|
||||
use League\OAuth2\Server\Entities\ClientEntityInterface;
|
||||
use League\OAuth2\Server\Entities\ScopeEntityInterface;
|
||||
use League\OAuth2\Server\Entities\Traits\AccessTokenTrait;
|
||||
use Siteworxpro\App\Services\Facades\Config;
|
||||
|
||||
class AccessToken extends RedisModel implements AccessTokenEntityInterface
|
||||
{
|
||||
@@ -27,6 +29,26 @@ class AccessToken extends RedisModel implements AccessTokenEntityInterface
|
||||
return $this->client;
|
||||
}
|
||||
|
||||
private function convertToJWT(): Token
|
||||
{
|
||||
$this->initJwtConfiguration();
|
||||
|
||||
$builder = $this->jwtConfiguration->builder()
|
||||
->permittedFor($this->getClient()->getIdentifier())
|
||||
->identifiedBy($this->getIdentifier())
|
||||
->issuedAt(new DateTimeImmutable())
|
||||
->issuedBy(Config::get('app.url') . '/' . $this->getClient()->getIdentifier())
|
||||
->canOnlyBeUsedAfter(new DateTimeImmutable())
|
||||
->expiresAt($this->getExpiryDateTime())
|
||||
->withClaim('scopes', $this->getScopes());
|
||||
|
||||
if ($this->getUserIdentifier() !== null) {
|
||||
$builder = $builder->relatedTo($this->getSubjectIdentifier());
|
||||
}
|
||||
|
||||
return $builder->getToken($this->jwtConfiguration->signer(), $this->jwtConfiguration->signingKey());
|
||||
}
|
||||
|
||||
public function getExpiryDateTime(): DateTimeImmutable
|
||||
{
|
||||
return $this->expiryDateTime->toDateTimeImmutable();
|
||||
@@ -52,7 +74,7 @@ class AccessToken extends RedisModel implements AccessTokenEntityInterface
|
||||
$this->expiryDateTime = Carbon::instance($dateTime);
|
||||
}
|
||||
|
||||
public function setUserIdentifier(string $identifier): void
|
||||
public function setUserIdentifier(?string $identifier): void
|
||||
{
|
||||
$this->userIdentifier = $identifier;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user