Add all files
This commit is contained in:
67
app/Console/Commands/CheckForUpdates.php
Normal file
67
app/Console/Commands/CheckForUpdates.php
Normal 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.');
|
||||
}
|
||||
}
|
||||
160
app/Console/Commands/CronJob.php
Normal file
160
app/Console/Commands/CronJob.php
Normal 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);
|
||||
}
|
||||
}
|
||||
72
app/Console/Commands/Extension/Create.php
Normal file
72
app/Console/Commands/Extension/Create.php
Normal 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;
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
54
app/Console/Commands/Extension/Disable.php
Normal file
54
app/Console/Commands/Extension/Disable.php
Normal 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(),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
32
app/Console/Commands/Extension/stubs/gateway.stub
Normal file
32
app/Console/Commands/Extension/stubs/gateway.stub
Normal 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 '';
|
||||
}
|
||||
}
|
||||
19
app/Console/Commands/Extension/stubs/other.stub
Normal file
19
app/Console/Commands/Extension/stubs/other.stub
Normal 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 [];
|
||||
}
|
||||
}
|
||||
92
app/Console/Commands/Extension/stubs/server.stub
Normal file
92
app/Console/Commands/Extension/stubs/server.stub
Normal 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;
|
||||
}
|
||||
}
|
||||
43
app/Console/Commands/Init.php
Normal file
43
app/Console/Commands/Init.php
Normal 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.';
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
86
app/Console/Commands/Logs.php
Normal file
86
app/Console/Commands/Logs.php
Normal 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;
|
||||
}
|
||||
}
|
||||
105
app/Console/Commands/MakeTheme.php
Normal file
105
app/Console/Commands/MakeTheme.php
Normal 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>'],
|
||||
];
|
||||
}
|
||||
}
|
||||
1485
app/Console/Commands/MigrateOldData.php
Normal file
1485
app/Console/Commands/MigrateOldData.php
Normal file
File diff suppressed because it is too large
Load Diff
64
app/Console/Commands/Settings/Change.php
Normal file
64
app/Console/Commands/Settings/Change.php
Normal 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]
|
||||
);
|
||||
}
|
||||
}
|
||||
92
app/Console/Commands/TelemetryCommand.php
Normal file
92
app/Console/Commands/TelemetryCommand.php
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
104
app/Console/Commands/Upgrade.php
Normal file
104
app/Console/Commands/Upgrade.php
Normal 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');
|
||||
}
|
||||
}
|
||||
60
app/Console/Commands/User/Create.php
Normal file
60
app/Console/Commands/User/Create.php
Normal 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,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
68
app/Console/Commands/User/PasswordReset.php
Normal file
68
app/Console/Commands/User/PasswordReset.php
Normal 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?',
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user