Add all files

This commit is contained in:
root
2025-11-30 11:57:22 +01:00
parent 2ee51cac4f
commit 044271db3d
1476 changed files with 142544 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Console\Commands;
use App\Models\Setting;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
class CheckForUpdates extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:check-for-updates';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Execute the console command.
*/
public function handle()
{
if (config('app.version') == 'development') {
$this->info('You are using the development version. No update check available.');
return;
}
if (config('app.version') == 'beta') {
$version = Http::get('https://api.paymenter.org/version?beta')->json();
Setting::updateOrCreate(
['key' => 'latest_commit'],
['value' => $version['beta']]
);
if (config('app.commit') != $version['beta']) {
$this->info('A new version is available: ' . $version['beta']);
// Save as a variable to use in the UI
$this->info('Latest version saved to database.');
} else {
$this->info('You are using the latest version: ' . config('app.commit'));
}
} else {
$version = Http::get('https://api.paymenter.org/version')->json();
if (config('app.version') != $version['latest']) {
$this->info('A new version is available: ' . $version['latest']);
// Save as a variable to use in the UI
$this->info('Latest version saved to database.');
} else {
$this->info('You are using the latest version: ' . config('app.version'));
}
}
Setting::updateOrCreate(
['key' => 'latest_version'],
['value' => $version['latest']]
);
$this->info('Update check completed.');
}
}

View File

@@ -0,0 +1,160 @@
<?php
namespace App\Console\Commands;
use App\Jobs\Server\SuspendJob;
use App\Jobs\Server\TerminateJob;
use App\Models\EmailLog;
use App\Models\Service;
use App\Models\ServiceUpgrade;
use App\Models\Ticket;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class CronJob extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:cron-job';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Run automated tasks';
/**
* Execute the console command.
*/
public function handle()
{
// Send invoices if due date is x days away
$sendedInvoices = 0;
Service::where('status', 'active')->where('expires_at', '<', now()->addDays((int) config('settings.cronjob_invoice', 7)))->get()->each(function ($service) use (&$sendedInvoices) {
// Does the service have already a pending invoice?
if ($service->invoices()->where('status', 'pending')->exists() || $service->cancellation()->exists()) {
return;
}
DB::beginTransaction();
try {
// Calculate if we should edit the price because of the coupon
if ($service->coupon) {
// Calculate what iteration of the coupon we are in
$iteration = $service->invoices()->count() + 1;
if ($iteration == $service->coupon->recurring) {
// Calculate the price
$price = $service->plan->prices()->where('currency_code', $service->currency_code)->first()->price;
$service->price = $price;
$service->save();
}
}
// Create invoice
$invoice = $service->invoices()->make([
'user_id' => $service->user_id,
'status' => 'pending',
'due_at' => $service->expires_at,
'currency_code' => $service->currency_code,
]);
$invoice->save();
// Create invoice items
$invoice->items()->create([
'reference_id' => $service->id,
'reference_type' => Service::class,
'price' => $service->price,
'quantity' => $service->quantity,
'description' => $service->description,
]);
} catch (\Exception $e) {
DB::rollBack();
$this->error('Error creating invoice for service ' . $service->id . ': ' . $e->getMessage());
return;
}
DB::commit();
$sendedInvoices++;
});
$this->info('Sending invoices if due date is ' . config('settings.cronjob_invoice', 7) . ' days away: ' . $sendedInvoices . ' invoices');
// Cancel services if first invoice is not paid after x days
$ordersCancelled = 0;
Service::where('status', 'pending')->whereDoesntHave('invoices', function ($query) {
$query->where('status', 'paid');
})->where('created_at', '<', now()->subDays((int) config('settings.cronjob_order_cancel', 7)))->get()->each(function ($service) use (&$ordersCancelled) {
$service->invoices()->where('status', 'pending')->update(['status' => 'cancelled']);
$service->update(['status' => 'cancelled']);
$ordersCancelled++;
});
$this->info('Cancelling services if first invoice is not paid after ' . config('settings.cronjob_order_cancel', 7) . ' days: ' . $ordersCancelled . ' orders');
$updatedUpgradeInvoices = 0;
ServiceUpgrade::where('status', 'pending')->get()->each(function ($upgrade) use (&$updatedUpgradeInvoices) {
if ($upgrade->service->expires_at < now()) {
$upgrade->update(['status' => 'cancelled']);
$upgrade->invoice->update(['status' => 'cancelled']);
$updatedUpgradeInvoices++;
return;
}
$upgrade->invoice->items()->update([
'price' => $upgrade->calculatePrice()->price,
]);
$updatedUpgradeInvoices++;
});
// Suspend orders if due date is overdue for x days
$ordersSuspended = 0;
Service::where('status', 'active')->where('expires_at', '<', now()->subDays((int) config('settings.cronjob_order_suspend', 2)))->each(function ($service) use (&$ordersSuspended) {
SuspendJob::dispatch($service);
$service->update(['status' => 'suspended']);
$ordersSuspended++;
});
$this->info('Suspending orders if due date is overdue for ' . config('settings.cronjob_order_suspend', 2) . ' days: ' . $ordersSuspended . ' orders');
// Terminate orders if due date is overdue for x days
$ordersTerminated = 0;
Service::where('status', 'suspended')->where('expires_at', '<', now()->subDays((int) config('settings.cronjob_order_terminate', 14)))->each(function ($service) use (&$ordersTerminated) {
TerminateJob::dispatch($service);
$service->update(['status' => 'cancelled']);
// Cancel outstanding invoices
$service->invoices()->where('status', 'pending')->update(['status' => 'cancelled']);
$ordersTerminated++;
});
$this->info('Terminating orders if due date is overdue for ' . config('settings.cronjob_order_terminate', 14) . ' days: ' . $ordersTerminated . ' orders');
// Close tickets if no response for x days
$ticketClosed = 0;
Ticket::where('status', 'replied')->each(function ($ticket) use (&$ticketClosed) {
$lastMessage = $ticket->messages()->latest('created_at')->first();
if ($lastMessage && $lastMessage->created_at < now()->subDays((int) config('settings.cronjob_close_ticket', 7))) {
$ticket->update(['status' => 'closed']);
$ticketClosed++;
}
});
$this->info('Closing tickets if no response for ' . config('settings.cronjob_close_ticket', 7) . ' days: ' . $ticketClosed . ' tickets');
// Delete email logs older then x
$this->info('Deleting email logs older then ' . config('settings.cronjob_delete_email_logs', 90) . ' days: ' . EmailLog::where('created_at', '<', now()->subDays((int) config('settings.cronjob_delete_email_logs', 30)))->count());
EmailLog::where('created_at', '<', now()->subDays((int) config('settings.cronjob_delete_email_logs', 90)))->delete();
// Check for updates
$this->info('Checking for updates...');
$this->call(CheckForUpdates::class);
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace App\Console\Commands\Extension;
use App\Models\Extension;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\PromptsForMissingInput;
class Create extends Command implements PromptsForMissingInput
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:extension:create {name} {type} {--force}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new extension';
public function handle()
{
$name = $this->argument('name');
$type = $this->argument('type');
if (!in_array($type, ['server', 'gateway', 'other'])) {
$this->error('Invalid extension type. Valid types are: server, gateway, event');
return;
}
// Read stub file contents
$stub = file_get_contents(__DIR__ . '/stubs/' . $type . '.stub');
// Replace placeholders with actual values
$stub = str_replace('{{ class }}', ucfirst($name), $stub);
$stub = str_replace('{{ namespace }}', 'Paymenter\\Extensions\\' . ucfirst($type) . 's' . '\\' . ucfirst($name), $stub);
// Create the extension file
$path = base_path('extensions/' . ucfirst($type) . 's/' . ucfirst($name) . '/' . ucfirst($name) . '.php');
if (file_exists(dirname($path)) && !$this->option('force')) {
$this->error("Extension already exists\nRerun the command with a different name or use --force to overwrite");
return;
}
if (!file_exists(dirname($path))) {
mkdir(dirname($path), 0755, true);
}
file_put_contents($path, $stub);
// Return path
$this->info("Extension created at: {$path}");
}
/**
* Prompt for missing input arguments using the returned questions.
*
* @return array
*/
protected function promptForMissingArgumentsUsing()
{
return [
'name' => 'What is the name of the extension?',
'type' => function () {
$type = $this->choice('What type of extension?', ['server', 'gateway', 'other']);
return $type;
},
];
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Console\Commands\Extension;
use App\Models\Extension;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\PromptsForMissingInput;
use function Laravel\Prompts\select;
class Disable extends Command implements PromptsForMissingInput
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:extension:disable {name}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Disable a extension if it causes issues';
public function handle()
{
$name = $this->argument('name');
$extension = Extension::where('extension', $name)->first();
if (!$extension) {
$this->error('Extension not found');
return;
}
$extension->update(['enabled' => false]);
$this->info('Extension disabled');
}
/**
* Prompt for missing input arguments using the returned questions.
*
* @return array
*/
protected function promptForMissingArgumentsUsing()
{
return [
'name' => fn () => select(
label: 'Which extension do you want to disable?',
options: Extension::all()->pluck('extension', 'extension')->toArray(),
),
];
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace {{ namespace }};
use App\Classes\Extension\Gateway;
use App\Models\Invoice;
class {{ class }} extends Gateway
{
/**
* Get all the configuration for the extension
*
* @param array $values
* @return array
*/
public function getConfig($values = [])
{
return [];
}
/**
* Return a view or a url to redirect to
*
* @param Invoice $invoice
* @param float $total
* @return string
*/
public function pay(Invoice $invoice, $total)
{
return '';
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace {{ namespace }};
use App\Classes\Extension\Extension;
class {{ class }} extends Extension
{
/**
* Get all the configuration for the extension
*
* @param array $values
* @return array
*/
public function getConfig($values = [])
{
return [];
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace {{ namespace }};
use App\Classes\Extension\Server;
use App\Models\Service;
class {{ class }} extends Server
{
/**
* Get all the configuration for the extension
*
* @param array $values
* @return array
*/
public function getConfig($values = []): array
{
return [];
}
/**
* Get product config
*
* @param array $values
* @return array
*/
public function getProductConfig($values = []): array
{
return [];
}
/**
* Check if currenct configuration is valid
*
* @return bool|string
*/
public function testConfig(): bool|string
{
return true;
}
/**
* Create a server for the service.
*
* @param Service $service The service that is being created.
* @param array $settings The settings that are provided on the product from `getProductConfig`.
* @param array $properties Custom properties created by the admin and values coming from `getCheckoutConfig`.
*/
public function createServer(Service $service, $settings, $properties)
{
return false;
}
/**
* Suspend a server
*
* @param Service $service
* @param array $settings (product settings)
* @param array $properties (checkout options)
* @return bool
*/
public function suspendServer(Service $service, $settings, $properties)
{
return false;
}
/**
* Unsuspend a server
*
* @param Service $service
* @param array $settings (product settings)
* @param array $properties (checkout options)
* @return bool
*/
public function unsuspendServer(Service $service, $settings, $properties)
{
return false;
}
/**
* Terminate a server
*
* @param Service $service
* @param array $settings (product settings)
* @param array $properties (checkout options)
* @return bool
*/
public function terminateServer(Service $service, $settings, $properties)
{
return false;
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Console\Commands;
use App\Models\Setting;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\PromptsForMissingInput;
use function Laravel\Prompts\text;
class Init extends Command implements PromptsForMissingInput
{
protected $signature = 'app:init {name} {url}';
protected $description = 'Show the application initialization steps';
public function handle()
{
$this->info('Thanks for installing Paymenter!');
// Validate the URL
if (!str_starts_with($this->argument('url'), 'http')) {
$this->error('The URL must start with http or https.');
return;
}
Setting::updateOrCreate(['key' => 'company_name'], ['value' => $this->argument('name')]);
Setting::updateOrCreate(['key' => 'app_url'], ['value' => rtrim($this->argument('url'), '/')]);
$this->info("Now you're all set up!\nVisit Paymenter at " . $this->argument('url'));
}
protected function promptForMissingArgumentsUsing(): array
{
return [
'name' => 'What is the name of your company?',
'url' => fn () => text('What is the URL of your application?', required: true, validate: function ($value) {
return str_starts_with($value, 'http') ? null : 'The URL must start with http or https.';
}),
];
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class Logs extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:logs';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Post application logs';
/**
* Execute the console command.
*/
public function handle()
{
// Read the log file of today
$today = now()->format('Y-m-d');
$logFile = storage_path("logs/laravel-{$today}.log");
if (!file_exists($logFile)) {
$this->info('No log file found for today.');
return;
}
$logContents = file_get_contents($logFile);
// Find last error message
$lastErrorMessage = $this->getLastErrorMessage($logContents);
if (!$lastErrorMessage) {
return $this->info('No error message found.');
}
if (!$this->confirm('Do you want to upload the error log (including environment variables) to Paymenter Support?', true)) {
$this->info('Here is the last error message:');
// output the last error message
$this->line($lastErrorMessage);
return;
}
// Add paymenter version and php version as first lines to $lastErrorMessage
$paymenterVersion = config('app.version');
$phpVersion = phpversion();
$lastErrorMessage = "Paymenter Version: $paymenterVersion\nPHP Version: $phpVersion\nURL: " . url('/') . "\n\n$lastErrorMessage";
// Post the error message to Paymenter Support
// nc log.paymenter.org 99
$this->info('Found error, uploading to Paymenter Support...');
// Create a socket connection
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($socket, 'log.paymenter.org', 99);
socket_write($socket, $lastErrorMessage, strlen($lastErrorMessage));
// Read response from the server (up to 1024 bytes)
$response = socket_read($socket, 1024);
socket_close($socket);
$this->line(trim($response));
}
protected function getLastErrorMessage(string $logContents): ?string
{
// [2025-07-03 11:06:06] local.ERROR: syntax error, unexpected token "if" {"exception":"[object] (ParseError(code: 0): syntax error, unexpected token \"if\" at C:\\Users\\corwi\\Projects\\Paymenter\\Paymenter\\app\\Listeners\\InvoiceItemCreatedListener.php:18)
$pattern = '/\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\].*?\.ERROR:(.*?)(?=\n\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]|$)/s';
preg_match_all($pattern, $logContents, $matches);
if (!empty($matches[1])) {
// Return the last error message trimmed
return trim(end($matches[1]));
}
return null;
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\PromptsForMissingInput;
use Illuminate\Filesystem\Filesystem;
use Qirolab\Theme\Theme;
class MakeTheme extends Command implements PromptsForMissingInput
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:theme:create {name} {author}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Execute the console command.
*/
public function handle()
{
// Get the theme's name in kebab case
$theme_name = str_replace(' ', '-', (strtolower($this->argument('name'))));
if ($this->themeExists($theme_name)) {
return $this->error("Theme \"$theme_name\" already exists.");
}
$author = $this->argument('author');
$themes_directory = 'themes/';
$fs = new Filesystem;
// Copy files from `default` theme
$fs->copyDirectory(
$themes_directory . 'default',
$themes_directory . $theme_name
);
$this->line('[1/4] Copied files from `default` theme.');
// Replace all themes/default to themes/$theme_name in config files
// `/` at the end is present because we don't want to replace `default` in `export default defineConfig`
$fs->replaceInFile(
'default/',
"$theme_name/",
$themes_directory . $theme_name . DIRECTORY_SEPARATOR . 'vite.config.js',
);
$this->line('[2/4] Replaced path values in vite.config.js.');
// Update author and description fields in new `theme.php`
$theme_file = $themes_directory . $theme_name . DIRECTORY_SEPARATOR . 'theme.php';
// Get the contents of the file
$theme_file_contents = $fs->get($theme_file);
// Replace theme name and author
$theme_file_contents = str_replace(
["'name' => 'Default'", "'author' => 'Paymenter'"],
["'name' => '$theme_name'", "'author' => '$author'"],
$theme_file_contents
);
// Save the changes back to the file
$fs->put($theme_file, $theme_file_contents);
$this->line('[3/4] Replaced variables in `theme.php`.');
$this->info("[4/4] Theme \"$theme_name\" created successfully.");
$this->newLine();
$this->comment('You can now start developing your theme by running:');
$this->comment(" `npm run dev $theme_name`");
$this->comment('After you have finished developing, you can build your theme by running:');
$this->comment(" `npm run build $theme_name`");
return Command::SUCCESS;
}
protected function themeExists(string $theme): bool
{
$directory = 'themes/' . $theme;
if (is_dir($directory)) {
return true;
}
return false;
}
protected function promptForMissingArgumentsUsing()
{
return [
'name' => ['What should be the Theme\'s name?', 'E.g. My Theme'],
'author' => ['Who is the author of this theme?', 'E.g. John Doe <johndoe@example.com>'],
];
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Console\Commands\Settings;
use App\Classes\Settings;
use App\Models\Setting;
use Illuminate\Console\Command;
use function Laravel\Prompts\form;
use function Laravel\Prompts\select;
use function Laravel\Prompts\text;
class Change extends Command
{
protected $signature = 'app:settings:change {key?} {value?}';
protected $description = 'Change a setting';
public function handle()
{
$key = $this->argument('key');
$value = $this->argument('value');
$form = form();
if (!$key) {
// Settings::settings is a array with first level keys as categories and second level keys as settings, so we need to flatten it
$settings = collect(Settings::settings())->flatten(1)->map(function ($item) {
return $item['name'];
})->toArray();
$form->suggest('Which setting would you like to change?', $settings, name: 'key');
}
if (!$value) {
$form->add(function ($responses) use ($key) {
$key = $responses['key'] ?? $key;
$setting = Settings::getSetting($key);
if (!isset($setting->type)) {
return text('What value should the setting have?', default: '', hint: 'Could not find setting but you can still change it');
}
// What type is the setting?
if ($setting->type === 'select') {
return select('What value should the setting have?', $setting->options);
} else {
return text('What value should the setting have?', default: $setting->default ?? '');
}
});
}
$form = $form->submit();
if (isset($form['key'])) {
$key = $form['key'];
}
if (isset($form[0]) || isset($form[1])) {
$value = $form[0] ?? $form[1];
}
Setting::updateOrCreate(
['key' => $key],
['value' => $value]
);
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace App\Console\Commands;
use App\Classes\Settings;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
class TelemetryCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:telemetry {--simulate}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sends telemetry data';
/**
* Execute the console command.
*/
public function handle()
{
// Send telemetry data
$this->info('Gathering telemetry data...');
$data = [
'uuid' => Settings::getTelemetry()['uuid'],
'version' => config('app.version'),
'php_version' => phpversion(),
'drivers' => [
'cache' => [
'type' => config('cache.default'),
],
'database' => [
'type' => config('database.default'),
'version' => DB::getPdo()->getAttribute(\PDO::ATTR_SERVER_VERSION),
],
],
'database_counts' => [
'invoices' => [
'count' => DB::table('invoices')->count(),
'paid' => DB::table('invoices')->where('status', 'paid')->count(),
],
'services' => [
'count' => DB::table('services')->count(),
'active' => DB::table('services')->where('status', 'active')->count(),
],
'products' => [
'count' => DB::table('products')->count(),
],
'currencies' => [
'count' => DB::table('currencies')->count(),
'currencies' => DB::table('currencies')->pluck('code')->toArray(),
],
'users' => [
'count' => DB::table('users')->count(),
'admins' => DB::table('users')->where('role_id', '!=', null)->count(),
],
'extensions' => [
'count' => DB::table('extensions')->count(),
'active' => DB::table('extensions')->where('enabled', true)->pluck('extension')->toArray(),
],
],
];
if ($this->option('simulate')) {
$this->info('Simulating telemetry data...');
$this->line(json_encode($data, JSON_PRETTY_PRINT));
return;
}
// Send telemetry data
$this->info('Sending telemetry data...');
$response = Http::post('https://api.paymenter.org/statistics', $data)->throw();
if ($response->successful()) {
$this->info('Telemetry data sent successfully.');
} else {
$this->error('Failed to send telemetry data: ' . $response->body());
}
}
}

View File

@@ -0,0 +1,104 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Symfony\Component\Process\Process;
class Upgrade extends Command
{
protected const DEFAULT_URL = 'https://github.com/paymenter/paymenter/releases/%s/paymenter.tar.gz';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:upgrade
{--user= : The user that PHP runs under. All files will be owned by this user.}
{--group= : The group that PHP runs under. All files will be owned by this group.}
{--url= : The specific archive to download.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Upgrade the website to the latest version.';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$this->info('Starting upgrade process...');
if (version_compare(PHP_VERSION, '8.2.0') < 0) {
$this->error('Cannot execute self-upgrade process. The minimum required PHP version required is 8.1, you have [' . PHP_VERSION . '].');
}
$user = 'www-data';
$group = 'www-data';
if ($this->input->isInteractive()) {
if (is_null($this->option('user'))) {
$userDetails = posix_getpwuid(fileowner('public'));
$user = $userDetails['name'] ?? 'www-data';
if (!$this->confirm("Your webserver user has been detected as <fg=blue>[{$user}]:</> is this correct?", true)) {
$user = $this->anticipate(
'Please enter the name of the user running your webserver process. This varies from system to system, but is generally "www-data", "nginx", or "apache".',
[
'www-data',
'nginx',
'apache',
]
);
}
}
if (is_null($this->option('group'))) {
$groupDetails = posix_getgrgid(filegroup('public'));
$group = $groupDetails['name'] ?? 'www-data';
if (!$this->confirm("Your webserver group has been detected as <fg=blue>[{$group}]:</> is this correct?", true)) {
$group = $this->anticipate(
'Please enter the name of the group running your webserver process. Normally this is the same as your user.',
[
'www-data',
'nginx',
'apache',
]
);
}
}
if (!$this->confirm('Are you sure you want to run the upgrade process for your Panel?')) {
$this->warn('Upgrade process terminated by user.');
return;
}
}
ini_set('output_buffering', '0');
// Call update.sh <url>
$this->line('$upgrader> curl -L "https://raw.githubusercontent.com/paymenter/paymenter/master/update.sh" | bash -s -- --user=' . $user . ' --group=' . $group . ' --url=' . $this->getUrl());
$process = Process::fromShellCommandline('curl -L "https://raw.githubusercontent.com/paymenter/paymenter/master/update.sh" | bash -s -- --user=' . $user . ' --group=' . $group . ' --url=' . $this->getUrl(), null, null, null, 1200);
$process->run(function ($type, $buffer) {
$this->{$type === Process::ERR ? 'error' : 'line'}($buffer);
});
$this->info('Upgrade process completed successfully!');
return Command::SUCCESS;
}
protected function getUrl(): string
{
if ($this->option('url')) {
return $this->option('url');
}
return sprintf(self::DEFAULT_URL, 'latest/download');
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Console\Commands\User;
use App\Models\Role;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\PromptsForMissingInput;
use function Laravel\Prompts\password;
use function Laravel\Prompts\select;
class Create extends Command implements PromptsForMissingInput
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:user:create {first_name} {last_name} {email} {password} {role}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new user';
/**
* Execute the console command.
*/
public function handle()
{
User::create([
'first_name' => $this->argument('first_name'),
'last_name' => $this->argument('last_name'),
'email' => $this->argument('email'),
'password' => $this->argument('password'),
'role_id' => $this->argument('role') ?? null,
]);
}
protected function promptForMissingArgumentsUsing(): array
{
$roleOptions = Role::all()->pluck('name', 'id')->toArray();
$roleOptions[0] = 'None';
return [
'first_name' => 'What is the user\'s first name?',
'last_name' => 'What is the user\'s last name?',
'email' => 'What is the user\'s email address?',
'password' => fn () => password('What is the user\'s password?', required: true),
'role' => fn () => select(
label: 'What is the user\'s role?',
options: $roleOptions,
default: 0,
),
];
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace App\Console\Commands\User;
use App\Helpers\NotificationHelper;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\PromptsForMissingInput;
use Illuminate\Support\Facades\Password;
class PasswordReset extends Command implements PromptsForMissingInput
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:user:password-reset {email}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send password reset email to a user';
/**
* Execute the console command.
*/
public function handle(): void
{
$email = $this->argument('email');
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$this->error('Invalid email format');
return;
}
$user = User::where('email', $email)->first();
if (!$user) {
$this->error("User with email '{$email}' not found");
return;
}
try {
NotificationHelper::passwordResetNotification($user, [
'url' => url(route('password.reset', [
'token' => Password::createToken($user),
'email' => $user->email,
], false)),
]);
$this->info("Password reset email sent successfully to '{$email}'");
} catch (\Throwable $e) {
$this->error('Failed to send password reset email: ' . $e->getMessage());
}
}
protected function promptForMissingArgumentsUsing(): array
{
return [
'email' => 'What is the user\'s email address?',
];
}
}