diff --git a/composer.json b/composer.json index 9806aa0..82e0dcf 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index 238f701..005d3c0 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/config.php b/config.php index 67c6e31..264ad9d 100644 --- a/config.php +++ b/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'), diff --git a/src/Async/Handlers/SayHelloHandler.php b/src/Async/Handlers/SayHelloHandler.php deleted file mode 100644 index 5c30514..0000000 --- a/src/Async/Handlers/SayHelloHandler.php +++ /dev/null @@ -1,21 +0,0 @@ -getPayload()['name'] ?? 'Guest'; - - Logger::info(sprintf("Hello, %s!", $name)); - } -} diff --git a/src/Async/Messages/Message.php b/src/Async/Messages/Message.php index c099a5c..f0335e5 100644 --- a/src/Async/Messages/Message.php +++ b/src/Async/Messages/Message.php @@ -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(); diff --git a/src/Async/Messages/SayHelloMessage.php b/src/Async/Messages/SayHelloMessage.php deleted file mode 100644 index 3bed371..0000000 --- a/src/Async/Messages/SayHelloMessage.php +++ /dev/null @@ -1,41 +0,0 @@ -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, - ]; - } -} diff --git a/src/Async/Messages/SendUserReset.php b/src/Async/Messages/SendUserReset.php new file mode 100644 index 0000000..07da763 --- /dev/null +++ b/src/Async/Messages/SendUserReset.php @@ -0,0 +1,22 @@ +payload = [ + 'user' => $user, + 'client' => $client, + 'token' => $token, + ]; + } +} diff --git a/src/Cli/App.php b/src/Cli/App.php index 4acc449..ad47397 100644 --- a/src/Cli/App.php +++ b/src/Cli/App.php @@ -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 diff --git a/src/Cli/Commands/Command.php b/src/Cli/Commands/Command.php index f06b6b7..c46a4bc 100644 --- a/src/Cli/Commands/Command.php +++ b/src/Cli/Commands/Command.php @@ -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 $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); + } } diff --git a/src/Cli/Commands/OAuth/ClientCapabilities.php b/src/Cli/Commands/OAuth/ClientCapabilities.php new file mode 100644 index 0000000..f3dbbf5 --- /dev/null +++ b/src/Cli/Commands/OAuth/ClientCapabilities.php @@ -0,0 +1,65 @@ +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; + } +} diff --git a/src/Cli/Commands/User/Add.php b/src/Cli/Commands/User/Add.php index dad8466..8c66e3e 100644 --- a/src/Cli/Commands/User/Add.php +++ b/src/Cli/Commands/User/Add.php @@ -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 $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 */ diff --git a/src/Cli/Commands/User/ResetPassword.php b/src/Cli/Commands/User/ResetPassword.php new file mode 100644 index 0000000..2cfaf50 --- /dev/null +++ b/src/Cli/Commands/User/ResetPassword.php @@ -0,0 +1,77 @@ +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; + } +} diff --git a/src/CommandBus/Commands/SendPasswordReset.php b/src/CommandBus/Commands/SendPasswordReset.php new file mode 100644 index 0000000..5684a5f --- /dev/null +++ b/src/CommandBus/Commands/SendPasswordReset.php @@ -0,0 +1,30 @@ +user; + } + + /** + * @return Client + */ + public function getClient(): Client + { + return $this->client; + } +} diff --git a/src/CommandBus/Handlers/SendPasswordResetHandler.php b/src/CommandBus/Handlers/SendPasswordResetHandler.php new file mode 100644 index 0000000..9732417 --- /dev/null +++ b/src/CommandBus/Handlers/SendPasswordResetHandler.php @@ -0,0 +1,55 @@ + $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(); + } +} diff --git a/src/Kernel.php b/src/Kernel.php index 3b12841..b92df62 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -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, ]; /** diff --git a/src/Mailer/Drivers/DriverInterface.php b/src/Mailer/Drivers/DriverInterface.php index eff1903..93abafe 100644 --- a/src/Mailer/Drivers/DriverInterface.php +++ b/src/Mailer/Drivers/DriverInterface.php @@ -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; } diff --git a/src/Mailer/Drivers/Log.php b/src/Mailer/Drivers/Log.php index 46f41c5..c869b9f 100644 --- a/src/Mailer/Drivers/Log.php +++ b/src/Mailer/Drivers/Log.php @@ -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('
', "\n", $body); diff --git a/src/Mailer/Drivers/Smtp.php b/src/Mailer/Drivers/Smtp.php new file mode 100644 index 0000000..f17e577 --- /dev/null +++ b/src/Mailer/Drivers/Smtp.php @@ -0,0 +1,53 @@ +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); + } +} diff --git a/src/Mailer/Message.php b/src/Mailer/Message.php index 254c6dc..470fd7b 100644 --- a/src/Mailer/Message.php +++ b/src/Mailer/Message.php @@ -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; diff --git a/src/Mailer/Sendmail.php b/src/Mailer/Sendmail.php index a7f5c69..0dea19a 100644 --- a/src/Mailer/Sendmail.php +++ b/src/Mailer/Sendmail.php @@ -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.'), }; } diff --git a/src/Mailer/Templates/password-reset.twig b/src/Mailer/Templates/password-reset.twig index be36d71..14dd0fe 100644 --- a/src/Mailer/Templates/password-reset.twig +++ b/src/Mailer/Templates/password-reset.twig @@ -1,4 +1,4 @@ -{% extends _base.twig %} +{% extends '_base.twig' %} {% block subject %} Password Reset Request diff --git a/src/OAuth/Entities/Client.php b/src/OAuth/Entities/Client.php index addc3ec..30f9ddb 100644 --- a/src/OAuth/Entities/Client.php +++ b/src/OAuth/Entities/Client.php @@ -41,6 +41,7 @@ use Siteworxpro\App\OAuth\ScopeRepository; * @property ClientCapabilities $capabilities * @property-read Collection $clientRedirectUris * @property-read Scope[]|Collection $scopes + * @property-read Collection $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(); } diff --git a/src/OAuth/Entities/ClientCapabilities.php b/src/OAuth/Entities/ClientCapabilities.php index 3b8f57b..4ddd4a9 100644 --- a/src/OAuth/Entities/ClientCapabilities.php +++ b/src/OAuth/Entities/ClientCapabilities.php @@ -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, ]; } diff --git a/src/Services/Facades/Mailer.php b/src/Services/Facades/Mailer.php new file mode 100644 index 0000000..3b07521 --- /dev/null +++ b/src/Services/Facades/Mailer.php @@ -0,0 +1,33 @@ +bind(self::getFacadeAccessor(), fn() => $sendMail); + + return $sendMail; + } + + protected static function getFacadeAccessor(): string + { + return Sendmail::class; + } +} diff --git a/src/Services/Facades/Twig.php b/src/Services/Facades/Twig.php new file mode 100644 index 0000000..7fa781a --- /dev/null +++ b/src/Services/Facades/Twig.php @@ -0,0 +1,24 @@ +app->singleton(Environment::class, function () { + $loader = new \Twig\Loader\FilesystemLoader(__DIR__ . '/../../Mailer/Templates'); + + return new Environment($loader, [ + 'cache' => new NullCache() + ]); + }); + } +}