You've already forked php-auth
generated from siteworxpro/Php-Template
Password reset and email
Some checks failed
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Failing after 1m19s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m9s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Failing after 1m19s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 1m34s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Failing after 1m20s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Failing after 12s
Some checks failed
🧪✨ Tests Workflow / 🛡️ 🔒 Library Audit (push) Failing after 1m19s
🧪✨ Tests Workflow / 📝 ✨ Code Lint (push) Successful in 1m9s
🧪✨ Tests Workflow / 🛡️ 🔒 License Check (push) Failing after 1m19s
🧪✨ Tests Workflow / 🧪 ✨ Database Migrations (push) Successful in 1m34s
🧪✨ Tests Workflow / 🐙 🔍 Code Sniffer (push) Failing after 1m20s
🧪✨ Tests Workflow / 🧪 ✅ Unit Tests (push) Failing after 12s
This commit is contained in:
@@ -32,7 +32,9 @@
|
||||
"ext-sodium": "*",
|
||||
"league/climate": "^3.10",
|
||||
"hansott/psr7-cookies": "^4.0",
|
||||
"symfony/console": "^v7.4.3"
|
||||
"symfony/console": "^v7.4.3",
|
||||
"twig/twig": "^3.22",
|
||||
"phpmailer/phpmailer": "^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^12.4",
|
||||
|
||||
163
composer.lock
generated
163
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "7aee32dcb1c3e870610dda4d399383f0",
|
||||
"content-hash": "4c2ae64539e4f6a700912cdc7d863493",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
@@ -2442,6 +2442,88 @@
|
||||
},
|
||||
"time": "2020-10-15T08:29:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpmailer/phpmailer",
|
||||
"version": "v7.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPMailer/PHPMailer.git",
|
||||
"reference": "ebf1655bd5b99b3f97e1a3ec0a69e5f4cd7ea088"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/ebf1655bd5b99b3f97e1a3ec0a69e5f4cd7ea088",
|
||||
"reference": "ebf1655bd5b99b3f97e1a3ec0a69e5f4cd7ea088",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-ctype": "*",
|
||||
"ext-filter": "*",
|
||||
"ext-hash": "*",
|
||||
"php": ">=5.5.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
|
||||
"doctrine/annotations": "^1.2.6 || ^1.13.3",
|
||||
"php-parallel-lint/php-console-highlighter": "^1.0.0",
|
||||
"php-parallel-lint/php-parallel-lint": "^1.3.2",
|
||||
"phpcompatibility/php-compatibility": "^10.0.0@dev",
|
||||
"squizlabs/php_codesniffer": "^3.13.5",
|
||||
"yoast/phpunit-polyfills": "^1.0.4"
|
||||
},
|
||||
"suggest": {
|
||||
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
|
||||
"directorytree/imapengine": "For uploading sent messages via IMAP, see gmail example",
|
||||
"ext-imap": "Needed to support advanced email address parsing according to RFC822",
|
||||
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
|
||||
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
|
||||
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
|
||||
"hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
|
||||
"league/oauth2-google": "Needed for Google XOAUTH2 authentication",
|
||||
"psr/log": "For optional PSR-3 debug logging",
|
||||
"symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)",
|
||||
"thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PHPMailer\\PHPMailer\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"LGPL-2.1-only"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Marcus Bointon",
|
||||
"email": "phpmailer@synchromedia.co.uk"
|
||||
},
|
||||
{
|
||||
"name": "Jim Jagielski",
|
||||
"email": "jimjag@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Andy Prevost",
|
||||
"email": "codeworxtech@users.sourceforge.net"
|
||||
},
|
||||
{
|
||||
"name": "Brent R. Matzelle"
|
||||
}
|
||||
],
|
||||
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
|
||||
"support": {
|
||||
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
|
||||
"source": "https://github.com/PHPMailer/PHPMailer/tree/v7.0.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/Synchro",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-01-09T18:02:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpdoc-parser",
|
||||
"version": "2.3.0",
|
||||
@@ -5363,6 +5445,85 @@
|
||||
],
|
||||
"time": "2025-12-04T18:17:06+00:00"
|
||||
},
|
||||
{
|
||||
"name": "twig/twig",
|
||||
"version": "v3.22.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/twigphp/Twig.git",
|
||||
"reference": "946ddeafa3c9f4ce279d1f34051af041db0e16f2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/twigphp/Twig/zipball/946ddeafa3c9f4ce279d1f34051af041db0e16f2",
|
||||
"reference": "946ddeafa3c9f4ce279d1f34051af041db0e16f2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1.0",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/polyfill-ctype": "^1.8",
|
||||
"symfony/polyfill-mbstring": "^1.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^2.0",
|
||||
"psr/container": "^1.0|^2.0",
|
||||
"symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/Resources/core.php",
|
||||
"src/Resources/debug.php",
|
||||
"src/Resources/escaper.php",
|
||||
"src/Resources/string_loader.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Twig\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com",
|
||||
"homepage": "http://fabien.potencier.org",
|
||||
"role": "Lead Developer"
|
||||
},
|
||||
{
|
||||
"name": "Twig Team",
|
||||
"role": "Contributors"
|
||||
},
|
||||
{
|
||||
"name": "Armin Ronacher",
|
||||
"email": "armin.ronacher@active-4.com",
|
||||
"role": "Project Founder"
|
||||
}
|
||||
],
|
||||
"description": "Twig, the flexible, fast, and secure template language for PHP",
|
||||
"homepage": "https://twig.symfony.com",
|
||||
"keywords": [
|
||||
"templating"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/twigphp/Twig/issues",
|
||||
"source": "https://github.com/twigphp/Twig/tree/v3.22.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/twig/twig",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-12-14T11:28:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "voku/portable-ascii",
|
||||
"version": "2.0.3",
|
||||
|
||||
17
config.php
17
config.php
@@ -9,6 +9,7 @@ return [
|
||||
'dev_mode' => Env::get('DEV_MODE', false, 'bool'),
|
||||
'url' => Env::get('APP_URL', 'https://localhost'),
|
||||
'encryption_key' => Env::get('APP_ENCRYPTION_KEY', 'base64:change_me'),
|
||||
'default_support_email' => Env::get('DEFAULT_SUPPORT_EMAIL', 'no@email.com'),
|
||||
],
|
||||
|
||||
/**
|
||||
@@ -36,6 +37,22 @@ return [
|
||||
],
|
||||
],
|
||||
|
||||
'mailer' => [
|
||||
'driver' => Env::get('MAILER_DRIVER', 'log'), // Options: 'log', 'smtp'
|
||||
|
||||
'log' => [
|
||||
'log_file' => Env::get('MAILER_LOG_FILE', 'php://stdout'),
|
||||
],
|
||||
|
||||
'smtp' => [
|
||||
'host' => Env::get('SMTP_HOST', 'localhost'),
|
||||
'port' => Env::get('SMTP_PORT', 25, 'int'),
|
||||
'username' => Env::get('SMTP_USERNAME', ''),
|
||||
'password' => Env::get('SMTP_PASSWORD', ''),
|
||||
'encryption' => Env::get('SMTP_ENCRYPTION', 'none'), // Options: 'ssl', 'starttls', 'none'
|
||||
],
|
||||
],
|
||||
|
||||
'cors' => [
|
||||
'allowed_origins' => Env::get('CORS_ALLOWED_ORIGINS', 'localhost:3000'),
|
||||
'allow_credentials' => Env::get('CORS_ALLOW_CREDENTIALS', true, 'bool'),
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Async\Handlers;
|
||||
|
||||
use Siteworxpro\App\Attributes\Async\HandlesMessage;
|
||||
use Siteworxpro\App\Async\Messages\Message;
|
||||
use Siteworxpro\App\Async\Messages\SayHelloMessage;
|
||||
use Siteworxpro\App\Services\Facades\Logger;
|
||||
|
||||
#[HandlesMessage(SayHelloMessage::class)]
|
||||
class SayHelloHandler implements HandlerInterface
|
||||
{
|
||||
public function __invoke(Message | SayHelloMessage $message): void
|
||||
{
|
||||
$name = $message->getPayload()['name'] ?? 'Guest';
|
||||
|
||||
Logger::info(sprintf("Hello, %s!", $name));
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ namespace Siteworxpro\App\Async\Messages;
|
||||
use Siteworxpro\App\Async\Queues\DefaultQueue;
|
||||
use Siteworxpro\App\Async\Queues\Queue;
|
||||
use Siteworxpro\App\Helpers\Ulid;
|
||||
use Siteworxpro\App\Services\Facades\Broker;
|
||||
|
||||
abstract class Message implements \Serializable
|
||||
{
|
||||
@@ -22,10 +23,6 @@ abstract class Message implements \Serializable
|
||||
|
||||
protected const string DEFAULT_QUEUE = DefaultQueue::class;
|
||||
|
||||
abstract public static function dispatch(...$args): void;
|
||||
|
||||
abstract public static function dispatchLater(int $delay, ...$args): void;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->uniqueId = Ulid::generate();
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Async\Messages;
|
||||
|
||||
use Siteworxpro\App\Services\Facades\Broker;
|
||||
|
||||
class SayHelloMessage extends Message
|
||||
{
|
||||
public static function dispatch(...$args): void
|
||||
{
|
||||
$name = $args[0] ?? 'World';
|
||||
$message = new self($name);
|
||||
Broker::publish(
|
||||
$message->getQueue(),
|
||||
$message
|
||||
);
|
||||
}
|
||||
|
||||
public static function dispatchLater(int $delay, ...$args): void
|
||||
{
|
||||
$name = $args[0] ?? 'World';
|
||||
$message = new self($name);
|
||||
Broker::publishLater(
|
||||
$message->getQueue(),
|
||||
$message,
|
||||
$delay
|
||||
);
|
||||
}
|
||||
|
||||
private function __construct(
|
||||
private readonly string $name
|
||||
) {
|
||||
parent::__construct();
|
||||
|
||||
$this->payload = [
|
||||
'name' => $this->name,
|
||||
];
|
||||
}
|
||||
}
|
||||
22
src/Async/Messages/SendUserReset.php
Normal file
22
src/Async/Messages/SendUserReset.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Async\Messages;
|
||||
|
||||
use Siteworxpro\App\Models\User;
|
||||
use Siteworxpro\App\OAuth\Entities\Client;
|
||||
|
||||
class SendUserReset extends Message
|
||||
{
|
||||
public function __construct(User $user, Client $client, string $token)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->payload = [
|
||||
'user' => $user,
|
||||
'client' => $client,
|
||||
'token' => $token,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,12 @@ namespace Siteworxpro\App\Cli;
|
||||
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\ClientCapabilities;
|
||||
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\Cli\Commands\User\ResetPassword;
|
||||
use Siteworxpro\App\Helpers\Version;
|
||||
use Siteworxpro\App\Kernel;
|
||||
use Symfony\Component\Console\Application;
|
||||
@@ -38,6 +40,8 @@ class App
|
||||
$this->app->addCommand(new Start());
|
||||
$this->app->addCommand(new GenerateKey());
|
||||
$this->app->addCommand(new AddScope());
|
||||
$this->app->addCommand(new ResetPassword());
|
||||
$this->app->addCommand(new ClientCapabilities());
|
||||
}
|
||||
|
||||
public function run(): int
|
||||
|
||||
@@ -4,8 +4,16 @@ declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Cli\Commands;
|
||||
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Siteworxpro\App\Cli\ClimateOutput;
|
||||
use Siteworxpro\App\Models\User;
|
||||
use Siteworxpro\App\OAuth\Entities\Client;
|
||||
use Symfony\Component\Console\Helper\HelperSet;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\Question as QuestionInput;
|
||||
|
||||
abstract class Command extends \Symfony\Component\Console\Command\Command implements CommandInterface
|
||||
{
|
||||
@@ -18,4 +26,65 @@ abstract class Command extends \Symfony\Component\Console\Command\Command implem
|
||||
$this->helper = new QuestionHelper();
|
||||
$this->setHelperSet(new HelperSet([$this->helper]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ClimateOutput|OutputInterface $output
|
||||
* @param ArgvInput|InputInterface $input
|
||||
* @return Client | null
|
||||
*/
|
||||
protected function askForClient(ClimateOutput|OutputInterface $output, ArgvInput|InputInterface $input): ?Client
|
||||
{
|
||||
/** @var Collection<Client> $clients */
|
||||
$clients = Client::whereJsonContains('grant_types', 'authorization_code')
|
||||
->get(['id', 'name']);
|
||||
|
||||
if ($clients->isEmpty()) {
|
||||
$output->error('No OAuth clients available.');
|
||||
return null;
|
||||
}
|
||||
|
||||
$output->info('Available OAuth Clients:');
|
||||
foreach ($clients as $client) {
|
||||
$output->out("[$client->id] $client->name");
|
||||
}
|
||||
|
||||
$question = new QuestionInput('Enter the client ID to associate the new user with: ');
|
||||
$question->setAutocompleterValues($clients->pluck('id')->toArray());
|
||||
|
||||
/** @var QuestionHelper $helper */
|
||||
$helper = $this->getHelper('question');
|
||||
$id = $helper->ask($input, $output, $question);
|
||||
|
||||
return Client::find($id);
|
||||
}
|
||||
|
||||
protected function askForUser(
|
||||
Client $client,
|
||||
ClimateOutput|OutputInterface $output,
|
||||
ArgvInput|InputInterface $input
|
||||
): ?User {
|
||||
$users = $client->users;
|
||||
|
||||
if ($users->isEmpty()) {
|
||||
$output->error('No users found for the specified client.');
|
||||
return null;
|
||||
}
|
||||
|
||||
$output->info('Available Users for Client ' . $client->name . ':');
|
||||
|
||||
/** @var User $user */
|
||||
foreach ($users as $user) {
|
||||
$output->out("[$user->id] $user->email");
|
||||
}
|
||||
|
||||
$question = new QuestionInput('Enter the user ID: ');
|
||||
$question->setAutocompleterValues($users->pluck('id')->toArray());
|
||||
|
||||
/** @var QuestionHelper $helper */
|
||||
$helper = $this->getHelper('question');
|
||||
|
||||
$id = $helper->ask($input, $output, $question);
|
||||
|
||||
return User::find($id);
|
||||
}
|
||||
}
|
||||
|
||||
65
src/Cli/Commands/OAuth/ClientCapabilities.php
Normal file
65
src/Cli/Commands/OAuth/ClientCapabilities.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Cli\Commands\OAuth;
|
||||
|
||||
use Siteworxpro\App\Cli\ClimateOutput;
|
||||
use Siteworxpro\App\Cli\Commands\Command;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
|
||||
#[AsCommand(name: 'oauth:client:edit', description: 'Edit an OAuth client.')]
|
||||
class ClientCapabilities extends Command
|
||||
{
|
||||
public function __invoke(ArgvInput|InputInterface $input, ClimateOutput|OutputInterface $output): int
|
||||
{
|
||||
$client = $this->askForClient($output, $input);
|
||||
|
||||
if ($client === null) {
|
||||
$output->error('Client not found.');
|
||||
return \Symfony\Component\Console\Command\Command::FAILURE;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
$output->info('Choose a capability to modify:');
|
||||
$capabilities = $client->capabilities->toArray();
|
||||
|
||||
$output->info('[0] Exit');
|
||||
$output->info('[1] Username Password: ' . ($capabilities['userPass'] ? 'Enabled' : 'Disabled'));
|
||||
$output->info('[2] Magic Link: ' . ($capabilities['magicLink'] ? 'Enabled' : 'Disabled'));
|
||||
$output->info('[3] Social Logins: ' . ($capabilities['socials'] ? 'Enabled' : 'Disabled'));
|
||||
|
||||
$question = new Question('What do you want to edit: ', '');
|
||||
$selection = $this->helper->ask($input, $output, $question);
|
||||
|
||||
if ($selection === '0') {
|
||||
break;
|
||||
}
|
||||
|
||||
switch ($selection) {
|
||||
case '1':
|
||||
$capabilities['userPass'] = !$capabilities['userPass'];
|
||||
break;
|
||||
case '2':
|
||||
$capabilities['magicLink'] = !$capabilities['magicLink'];
|
||||
break;
|
||||
case '3':
|
||||
$output->info('Social Logins cannot be modified via CLI at this time.');
|
||||
break;
|
||||
default:
|
||||
$output->error('Invalid selection. Please try again.');
|
||||
continue 2;
|
||||
}
|
||||
|
||||
|
||||
$client->capabilities = $capabilities;
|
||||
$client->save();
|
||||
}
|
||||
|
||||
return \Symfony\Component\Console\Command\Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Cli\Commands\User;
|
||||
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Siteworxpro\App\Cli\Commands\Command;
|
||||
use Siteworxpro\App\CommandBus\Commands\CreateUser;
|
||||
use Siteworxpro\App\Models\ClientUser;
|
||||
use Siteworxpro\App\Models\User;
|
||||
use Siteworxpro\App\OAuth\Entities\Client;
|
||||
use Siteworxpro\App\Services\Facades\CommandBus;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command as SCommand;
|
||||
@@ -21,23 +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']);
|
||||
|
||||
$output->info('Available OAuth Clients:');
|
||||
foreach ($clients as $client) {
|
||||
$output->out("[$client->id] $client->name");
|
||||
}
|
||||
|
||||
$question = new QuestionInput('Enter the client ID to associate the new user with: ');
|
||||
$question->setAutocompleterValues($clients->pluck('id')->toArray());
|
||||
|
||||
/** @var QuestionHelper $helper */
|
||||
$helper = $this->getHelper('question');
|
||||
$id = $helper->ask($input, $output, $question);
|
||||
|
||||
$client = Client::find($id);
|
||||
$client = $this->askForClient($output, $input);
|
||||
if (!$client) {
|
||||
$output->error('Client not found.');
|
||||
return SCommand::FAILURE;
|
||||
@@ -53,6 +35,9 @@ class Add extends Command
|
||||
return $input;
|
||||
});
|
||||
|
||||
/** @var QuestionHelper $helper */
|
||||
$helper = $this->getHelper('question');
|
||||
|
||||
$email = $helper->ask($input, $output, $emailQuestion);
|
||||
|
||||
/** @var User| null $user */
|
||||
|
||||
77
src/Cli/Commands/User/ResetPassword.php
Normal file
77
src/Cli/Commands/User/ResetPassword.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Cli\Commands\User;
|
||||
|
||||
use Siteworxpro\App\Cli\ClimateOutput;
|
||||
use Siteworxpro\App\Cli\Commands\Command;
|
||||
use Siteworxpro\App\CommandBus\Commands\SendPasswordReset;
|
||||
use Siteworxpro\App\Services\Facades\CommandBus;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\Question as QuestionInput;
|
||||
|
||||
#[AsCommand('user:reset-password', 'Reset a user\'s password')]
|
||||
class ResetPassword extends Command
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addOption(
|
||||
'send-email',
|
||||
's',
|
||||
InputOption::VALUE_NONE,
|
||||
description: 'Send password reset email to the user'
|
||||
);
|
||||
}
|
||||
|
||||
public function __invoke(ArgvInput|InputInterface $input, ClimateOutput|OutputInterface $output): int
|
||||
{
|
||||
$client = $this->askForClient($output, $input);
|
||||
|
||||
if (!$client) {
|
||||
$output->error('Client not found.');
|
||||
return \Symfony\Component\Console\Command\Command::FAILURE;
|
||||
}
|
||||
|
||||
$user = $this->askForUser($client, $output, $input);
|
||||
if (!$user) {
|
||||
$output->error('User not found for the specified client.');
|
||||
return \Symfony\Component\Console\Command\Command::FAILURE;
|
||||
}
|
||||
|
||||
if ($input->getOption('send-email')) {
|
||||
CommandBus::handle(new SendPasswordReset($user, $client));
|
||||
$output->info('Password reset email sent to the user.');
|
||||
|
||||
return \Symfony\Component\Console\Command\Command::SUCCESS;
|
||||
}
|
||||
|
||||
/** @var QuestionHelper $helper */
|
||||
$helper = $this->getHelper('question');
|
||||
$question = new QuestionInput('Enter the new password: ');
|
||||
$question->setValidator(function ($input): string {
|
||||
if (empty($input) || strlen($input) < 8) {
|
||||
throw new \InvalidArgumentException('Password must be at least 8 characters long.');
|
||||
}
|
||||
|
||||
return $input;
|
||||
});
|
||||
|
||||
$question->setHidden(true);
|
||||
$question->setHiddenFallback(false);
|
||||
|
||||
$newPassword = $helper->ask($input, $output, $question);
|
||||
|
||||
$user->password = $newPassword;
|
||||
$user->save();
|
||||
|
||||
$output->info('Password has been reset successfully.');
|
||||
|
||||
return \Symfony\Component\Console\Command\Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
30
src/CommandBus/Commands/SendPasswordReset.php
Normal file
30
src/CommandBus/Commands/SendPasswordReset.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\CommandBus\Commands;
|
||||
|
||||
use Siteworxpro\App\Models\User;
|
||||
use Siteworxpro\App\OAuth\Entities\Client;
|
||||
|
||||
readonly class SendPasswordReset extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private User $user,
|
||||
private Client $client
|
||||
) {
|
||||
}
|
||||
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Client
|
||||
*/
|
||||
public function getClient(): Client
|
||||
{
|
||||
return $this->client;
|
||||
}
|
||||
}
|
||||
55
src/CommandBus/Handlers/SendPasswordResetHandler.php
Normal file
55
src/CommandBus/Handlers/SendPasswordResetHandler.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\CommandBus\Handlers;
|
||||
|
||||
use Random\RandomException;
|
||||
use Siteworxpro\App\Attributes\CommandBus\HandlesCommand;
|
||||
use Siteworxpro\App\CommandBus\Commands\Command;
|
||||
use Siteworxpro\App\CommandBus\Commands\SendPasswordReset;
|
||||
use Siteworxpro\App\CommandBus\Exceptions\CommandHandlerException;
|
||||
use Siteworxpro\App\Helpers\Rand;
|
||||
use Siteworxpro\App\Mailer\Message;
|
||||
use Siteworxpro\App\Models\User;
|
||||
use Siteworxpro\App\Services\Facades\Config;
|
||||
use Siteworxpro\App\Services\Facades\Mailer;
|
||||
use Siteworxpro\App\Services\Facades\Redis;
|
||||
use Siteworxpro\App\Services\Facades\Twig;
|
||||
|
||||
#[HandlesCommand(SendPasswordReset::class)]
|
||||
class SendPasswordResetHandler extends CommandHandler
|
||||
{
|
||||
/**
|
||||
* @throws RandomException
|
||||
*/
|
||||
public function __invoke(Command|SendPasswordReset $command): User
|
||||
{
|
||||
if (!$command instanceof SendPasswordReset) {
|
||||
throw new CommandHandlerException('Invalid command type provided to handler.');
|
||||
}
|
||||
|
||||
$token = Rand::string(64);
|
||||
|
||||
$content = Twig::render('password-reset.twig', [
|
||||
'user' => $command->getUser(),
|
||||
'resetLink' => Config::get('app.url') . '/reset-password?token=' . $token,
|
||||
'client' => $command->getClient()
|
||||
]);
|
||||
|
||||
$message = new Message(
|
||||
$command->getUser()->email,
|
||||
$command->getClient()->capabilities->toArray()['support_email'] ?? Config::get('app.default_support_email'),
|
||||
'Password Reset Request',
|
||||
$content
|
||||
);
|
||||
|
||||
if (Mailer::send($message) === false) {
|
||||
throw new CommandHandlerException('Failed to send password reset email.');
|
||||
}
|
||||
|
||||
Redis::set('password_reset:' . $command->getUser()->id, $token, 3600);
|
||||
|
||||
return $command->getUser();
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ use Siteworxpro\App\Services\ServiceProviders\DispatcherServiceProvider;
|
||||
use Siteworxpro\App\Services\ServiceProviders\EncryptionServiceProvider;
|
||||
use Siteworxpro\App\Services\ServiceProviders\LoggerServiceProvider;
|
||||
use Siteworxpro\App\Services\ServiceProviders\RedisServiceProvider;
|
||||
use Siteworxpro\App\Services\ServiceProviders\TwigProvider;
|
||||
|
||||
/**
|
||||
* Class Kernel
|
||||
@@ -38,6 +39,7 @@ class Kernel
|
||||
BrokerServiceProvider::class,
|
||||
CommandBusProvider::class,
|
||||
EncryptionServiceProvider::class,
|
||||
TwigProvider::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,5 +8,6 @@ use Siteworxpro\App\Mailer\Message;
|
||||
|
||||
interface DriverInterface
|
||||
{
|
||||
public function setFrom(string $address, string $name): void;
|
||||
public function send(Message $message): bool;
|
||||
}
|
||||
|
||||
@@ -9,22 +9,30 @@ use Siteworxpro\App\Services\Facades\Logger;
|
||||
|
||||
class Log implements DriverInterface
|
||||
{
|
||||
private string $fromAddress = '';
|
||||
|
||||
public function send(Message $message): bool
|
||||
{
|
||||
$logMessage = `
|
||||
$logMessage = <<<'MSG'
|
||||
===============================================
|
||||
From: {$this->fromAddress}
|
||||
To: {$message->getTo()}
|
||||
Subject: {$message->getSubject()}
|
||||
Body:
|
||||
{$this->formatBodyForLog($message->getBody())}
|
||||
===============================================
|
||||
`;
|
||||
MSG;
|
||||
|
||||
Logger::info($logMessage);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function setFrom(string $address, string $name): void
|
||||
{
|
||||
$this->fromAddress = sprintf('%s <%s>', $name, $address);
|
||||
}
|
||||
|
||||
private function formatBodyForLog(string $body): string
|
||||
{
|
||||
$body = str_replace('<br>', "\n", $body);
|
||||
|
||||
53
src/Mailer/Drivers/Smtp.php
Normal file
53
src/Mailer/Drivers/Smtp.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Mailer\Drivers;
|
||||
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use Siteworxpro\App\Mailer\Message;
|
||||
use Siteworxpro\App\Services\Facades\Config;
|
||||
|
||||
class Smtp implements DriverInterface
|
||||
{
|
||||
private PHPMailer $mailer;
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$config = Config::get('mailer.smtp', []);
|
||||
|
||||
if (empty($config['host']) || empty($config['port'])) {
|
||||
throw new \InvalidArgumentException(
|
||||
'SMTP host and port must be specified in the configuration mailer.smtp.'
|
||||
);
|
||||
}
|
||||
|
||||
if ($config['encryption'] && !in_array($config['encryption'], ['ssl', 'starttls', 'none'], true)) {
|
||||
throw new \InvalidArgumentException(
|
||||
'Invalid SMTP encryption type specified. Valid options are: ssl, starttls, or none.'
|
||||
);
|
||||
}
|
||||
|
||||
$this->mailer = new PHPMailer(true);
|
||||
$this->mailer->isSMTP();
|
||||
$this->mailer->Host = $config['host'];
|
||||
$this->mailer->Port = $config['port'];
|
||||
$this->mailer->SMTPAuth = !empty($config['username']) && !empty($config['password']);
|
||||
$this->mailer->Username = $config['username'] ?? '';
|
||||
$this->mailer->Password = $config['password'] ?? '';
|
||||
}
|
||||
|
||||
public function send(Message $message): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function setFrom(string $address, string $name): void
|
||||
{
|
||||
$this->mailer->setFrom($address, $name, false);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ readonly class Message
|
||||
{
|
||||
public function __construct(
|
||||
private string $to,
|
||||
private string $from,
|
||||
private string $subject,
|
||||
private string $body
|
||||
) {
|
||||
@@ -21,6 +22,14 @@ readonly class Message
|
||||
return $this->to;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFrom(): string
|
||||
{
|
||||
return $this->from;
|
||||
}
|
||||
|
||||
public function getSubject(): string
|
||||
{
|
||||
return $this->subject;
|
||||
|
||||
@@ -14,6 +14,7 @@ class Sendmail
|
||||
{
|
||||
$this->driver = match (Config::get('mailer.driver')) {
|
||||
'log' => new Drivers\Log(),
|
||||
'smtp' => new Drivers\Smtp(),
|
||||
default => throw new \InvalidArgumentException('Invalid mailer driver specified.'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends _base.twig %}
|
||||
{% extends '_base.twig' %}
|
||||
|
||||
{% block subject %}
|
||||
Password Reset Request
|
||||
|
||||
@@ -41,6 +41,7 @@ use Siteworxpro\App\OAuth\ScopeRepository;
|
||||
* @property ClientCapabilities $capabilities
|
||||
* @property-read Collection<ClientRedirectUri> $clientRedirectUris
|
||||
* @property-read Scope[]|Collection $scopes
|
||||
* @property-read Collection<User> $users
|
||||
*/
|
||||
class Client extends Model implements ClientEntityInterface
|
||||
{
|
||||
@@ -169,8 +170,12 @@ class Client extends Model implements ClientEntityInterface
|
||||
/**
|
||||
* @throws \JsonException
|
||||
*/
|
||||
public function setCapabilitiesAttribute(ClientCapabilities $capabilities): void
|
||||
public function setCapabilitiesAttribute(ClientCapabilities | array $capabilities): void
|
||||
{
|
||||
if (is_array($capabilities)) {
|
||||
$capabilities = new ClientCapabilities($capabilities);
|
||||
}
|
||||
|
||||
$this->attributes['capabilities'] = $capabilities->toJson();
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Siteworxpro\App\OAuth\Entities;
|
||||
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use JetBrains\PhpStorm\ArrayShape;
|
||||
|
||||
class ClientCapabilities implements Arrayable
|
||||
{
|
||||
@@ -13,6 +14,8 @@ class ClientCapabilities implements Arrayable
|
||||
private bool $passkey = false;
|
||||
private array $socials = [];
|
||||
|
||||
private string $support_email = '';
|
||||
|
||||
private array $branding = [
|
||||
'primaryColor' => '#000000',
|
||||
'secondaryColor' => '#FFFFFF',
|
||||
@@ -40,6 +43,10 @@ class ClientCapabilities implements Arrayable
|
||||
if (isset($capabilities['branding']) && is_array($capabilities['branding'])) {
|
||||
$this->branding = array_merge($this->branding, $capabilities['branding']);
|
||||
}
|
||||
|
||||
if (isset($capabilities['support_email'])) {
|
||||
$this->support_email = (string)$capabilities['support_email'];
|
||||
}
|
||||
}
|
||||
|
||||
public static function fromJson(string $data): self
|
||||
@@ -53,6 +60,14 @@ class ClientCapabilities implements Arrayable
|
||||
}
|
||||
}
|
||||
|
||||
#[ArrayShape([
|
||||
'userPass' => "bool",
|
||||
'magicLink' => "bool",
|
||||
'passkey' => "bool",
|
||||
'socials' => "array",
|
||||
'branding' => "array",
|
||||
'support_email' => "string"
|
||||
])]
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
@@ -61,6 +76,7 @@ class ClientCapabilities implements Arrayable
|
||||
'passkey' => $this->passkey,
|
||||
'socials' => $this->socials,
|
||||
'branding' => $this->branding,
|
||||
'support_email' => $this->support_email,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
33
src/Services/Facades/Mailer.php
Normal file
33
src/Services/Facades/Mailer.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Services\Facades;
|
||||
|
||||
use Siteworxpro\App\Mailer\Message;
|
||||
use Siteworxpro\App\Mailer\Sendmail;
|
||||
use Siteworxpro\App\Services\Facade;
|
||||
|
||||
/**
|
||||
* @method static bool send(Message $message)
|
||||
*/
|
||||
class Mailer extends Facade
|
||||
{
|
||||
public static function getFacadeRoot(): Sendmail
|
||||
{
|
||||
if (static::resolveFacadeInstance(self::getFacadeAccessor())) {
|
||||
static::resolveFacadeInstance(self::getFacadeAccessor());
|
||||
}
|
||||
|
||||
$sendMail = new Sendmail();
|
||||
|
||||
static::getFacadeContainer()->bind(self::getFacadeAccessor(), fn() => $sendMail);
|
||||
|
||||
return $sendMail;
|
||||
}
|
||||
|
||||
protected static function getFacadeAccessor(): string
|
||||
{
|
||||
return Sendmail::class;
|
||||
}
|
||||
}
|
||||
24
src/Services/Facades/Twig.php
Normal file
24
src/Services/Facades/Twig.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Services\Facades;
|
||||
|
||||
use Siteworxpro\App\Services\Facade;
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
* Class Twig
|
||||
*
|
||||
* Facade for accessing the Twig templating engine.
|
||||
*
|
||||
* @package Siteworxpro\App\Services\Facades
|
||||
* @method static render(string $template, array $context = []): string
|
||||
*/
|
||||
class Twig extends Facade
|
||||
{
|
||||
protected static function getFacadeAccessor(): string
|
||||
{
|
||||
return Environment::class;
|
||||
}
|
||||
}
|
||||
28
src/Services/ServiceProviders/TwigProvider.php
Normal file
28
src/Services/ServiceProviders/TwigProvider.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Siteworxpro\App\Services\ServiceProviders;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Twig\Cache\NullCache;
|
||||
use Twig\Environment;
|
||||
|
||||
class TwigProvider extends ServiceProvider
|
||||
{
|
||||
public function provides(): array
|
||||
{
|
||||
return [ Environment::class ];
|
||||
}
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(Environment::class, function () {
|
||||
$loader = new \Twig\Loader\FilesystemLoader(__DIR__ . '/../../Mailer/Templates');
|
||||
|
||||
return new Environment($loader, [
|
||||
'cache' => new NullCache()
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user