588 lines
22 KiB
PHP
588 lines
22 KiB
PHP
<?php
|
|
namespace Paymenter\Extensions\Servers\ManualServers;
|
|
use App\Classes\Extension\Server;
|
|
use App\Helpers\NotificationHelper;
|
|
use App\Models\Service;
|
|
use Illuminate\Support\Facades\Event;
|
|
use Illuminate\Support\Facades\Gate;
|
|
use Illuminate\Support\Facades\View;
|
|
use Paymenter\Extensions\Servers\ManualServers\Policies\ManualServerPolicy;
|
|
class ManualServers extends Server
|
|
{
|
|
protected static $version = '1.0.0';
|
|
public function boot()
|
|
{
|
|
View::addNamespace('manual-servers', __DIR__ . '/views');
|
|
if (is_dir(__DIR__ . '/resources/views')) {
|
|
View::addNamespace('manual-servers', __DIR__ . '/resources/views');
|
|
}
|
|
if (class_exists(\Filament\Filament::class)) {
|
|
\Filament\Filament::serving(function () {
|
|
\Filament\Filament::registerPages([
|
|
Admin\Pages\ManualServersQueue::class,
|
|
Admin\Pages\ManualServersManage::class,
|
|
]);
|
|
});
|
|
}
|
|
|
|
Gate::policy(self::class, ManualServerPolicy::class);
|
|
|
|
Event::listen('permissions', function () {
|
|
return [
|
|
'admin.manualservers.view' => 'View Manual Servers',
|
|
'admin.manualservers.manage' => 'Manage Manual Servers',
|
|
'admin.manualservers.deploy' => 'Deploy Manual Servers',
|
|
'admin.manualservers.credentials' => 'Manage Manual Server Credentials',
|
|
'admin.manualservers.message' => 'Send Manual Server Client Messages',
|
|
];
|
|
});
|
|
|
|
Event::listen('api.permissions', function () {
|
|
return [
|
|
'admin.manualservers.view' => 'View Manual Servers',
|
|
'admin.manualservers.manage' => 'Manage Manual Servers',
|
|
'admin.manualservers.deploy' => 'Deploy Manual Servers',
|
|
'admin.manualservers.credentials' => 'Manage Manual Server Credentials',
|
|
'admin.manualservers.message' => 'Send Manual Server Client Messages',
|
|
];
|
|
});
|
|
}
|
|
public function getConfig($values = [])
|
|
{
|
|
return [
|
|
[
|
|
'name' => 'discord_webhook',
|
|
'label' => 'Discord Webhook URL',
|
|
'type' => 'text',
|
|
'description' => 'Discord webhook for deployment notifications',
|
|
'required' => false,
|
|
],
|
|
];
|
|
}
|
|
public function getProductConfig($values = [])
|
|
{
|
|
$config = [
|
|
[
|
|
'name' => 'sla_hours',
|
|
'label' => 'SLA Threshold (hours)',
|
|
'type' => 'text',
|
|
'description' => 'Maximum time allowed for manual setup before marked as overdue (default: 24 hours).',
|
|
'required' => false,
|
|
'placeholder' => '24',
|
|
],
|
|
[
|
|
'name' => 'processes',
|
|
'label' => 'Manual Server Setup Steps',
|
|
'type' => 'tags',
|
|
'description' => 'Define the sequential steps required to manually set up this server. These steps will appear as a checklist in the admin panel.',
|
|
'required' => true,
|
|
'placeholder' => 'e.g., Verify Domain, Configure DNS, Install Software, Final Verification',
|
|
'database_type' => 'array',
|
|
],
|
|
[
|
|
'name' => 'instructions',
|
|
'label' => 'Server Setup Instructions',
|
|
'type' => 'textarea',
|
|
'description' => 'General instructions displayed to all clients for this server type (e.g., DNS configuration, nameservers, or setup requirements).',
|
|
'required' => false,
|
|
'rows' => 5,
|
|
],
|
|
[
|
|
'name' => '',
|
|
'type' => 'placeholder',
|
|
'required' => false,
|
|
],
|
|
|
|
];
|
|
|
|
for ($i = 1; $i <= 6; $i++) {
|
|
$config[] = [
|
|
'name' => "credential_{$i}_label",
|
|
'label' => "Credential {$i} Label",
|
|
'type' => 'text',
|
|
'description' => $i === 1 ? 'Create credentials without JSON. Fill label and type for each credential you want to collect.' : 'Leave empty if not needed.',
|
|
'required' => false,
|
|
'placeholder' => $i === 1 ? 'Username' : null,
|
|
];
|
|
|
|
$config[] = [
|
|
'name' => "credential_{$i}_type",
|
|
'label' => "Credential {$i} Type",
|
|
'type' => 'select',
|
|
'required' => false,
|
|
'default' => 'text',
|
|
'options' => [
|
|
'text' => 'Text',
|
|
'password' => 'Password',
|
|
'textarea' => 'Textarea',
|
|
'url' => 'URL',
|
|
'email' => 'Email',
|
|
],
|
|
];
|
|
}
|
|
|
|
for ($i = 1; $i <= 5; $i++) {
|
|
$config[] = [
|
|
'name' => "button_{$i}_label",
|
|
'label' => "Quick Action {$i} Label",
|
|
'type' => 'text',
|
|
'description' => $i === 1 ? 'Create quick action buttons without JSON. Fill label and URL for each button you want to show.' : 'Leave empty if not needed.',
|
|
'required' => false,
|
|
'placeholder' => $i === 1 ? 'Control Panel' : null,
|
|
];
|
|
|
|
$config[] = [
|
|
'name' => "button_{$i}_url",
|
|
'label' => "Quick Action {$i} URL",
|
|
'type' => 'text',
|
|
'required' => false,
|
|
'placeholder' => $i === 1 ? 'https://example.com/panel' : null,
|
|
];
|
|
}
|
|
|
|
$config[] = [
|
|
'name' => 'buttons_json',
|
|
'label' => 'Quick Access Buttons (JSON) - Legacy',
|
|
'type' => 'textarea',
|
|
'description' => 'Optional legacy format. Use the Quick Action Label/URL fields above instead.',
|
|
'required' => false,
|
|
'validation' => 'json',
|
|
'rows' => 8,
|
|
];
|
|
|
|
$config[] = [
|
|
'name' => 'credentials_json',
|
|
'label' => 'Server Credentials (JSON) - Legacy',
|
|
'type' => 'textarea',
|
|
'description' => 'Optional legacy format. Use the Credential Label/Type fields above instead.',
|
|
'required' => false,
|
|
'validation' => 'json',
|
|
'rows' => 10,
|
|
];
|
|
|
|
return $config;
|
|
}
|
|
|
|
/**
|
|
* Get configuration options for checkout (user-provided data during purchase)
|
|
*
|
|
* @param Product $product
|
|
* @return array
|
|
*/
|
|
public function getCheckoutConfig($product = null): array
|
|
{
|
|
if (!$product) {
|
|
return [];
|
|
}
|
|
|
|
// Create a temporary service object to use getCredentialFieldsForService
|
|
$tempService = new Service(['product_id' => $product->id]);
|
|
$tempService->setRelation('product', $product);
|
|
|
|
$fields = $this->getCredentialFieldsForService($tempService);
|
|
|
|
// Convert to checkout config format
|
|
$checkoutFields = [];
|
|
foreach ($fields as $field) {
|
|
$checkoutFields[] = [
|
|
'name' => $field['name'],
|
|
'type' => $field['type'],
|
|
'label' => $field['label'],
|
|
'required' => true,
|
|
'placeholder' => 'Enter ' . strtolower($field['label']),
|
|
];
|
|
}
|
|
|
|
return $checkoutFields;
|
|
}
|
|
|
|
/**
|
|
* Get configuration options for admin when managing a service
|
|
* This allows admins to enter credentials and other service-specific data
|
|
*
|
|
* @param Service $service
|
|
* @return array
|
|
*/
|
|
public function getUserConfig(Service $service): array
|
|
{
|
|
$fields = $this->getCredentialFieldsForService($service);
|
|
|
|
// Get existing credentials
|
|
$credentialsJson = $service->properties->where('key', 'credentials_data')->first()?->value ?? '[]';
|
|
$existingCredentials = json_decode($credentialsJson, true) ?? [];
|
|
|
|
// Format fields for the admin interface
|
|
$configFields = [];
|
|
foreach ($fields as $field) {
|
|
$configFields[] = [
|
|
'name' => $field['name'],
|
|
'type' => $field['type'],
|
|
'label' => $field['label'],
|
|
'required' => false,
|
|
'default' => $existingCredentials[$field['name']] ?? '',
|
|
];
|
|
}
|
|
|
|
return $configFields;
|
|
}
|
|
|
|
public function createServer(Service $service, $params = []): bool
|
|
{
|
|
$processes = $this->getProcessesForService($service);
|
|
$processData = [];
|
|
foreach ($processes as $index => $process) {
|
|
$processData[$index] = [
|
|
'name' => trim($process),
|
|
'completed' => false,
|
|
'completed_at' => null,
|
|
];
|
|
}
|
|
$service->properties()->updateOrCreate(
|
|
['key' => 'deploy_status'],
|
|
['value' => 'pending']
|
|
);
|
|
$service->properties()->updateOrCreate(
|
|
['key' => 'processes_data'],
|
|
['value' => json_encode($processData)]
|
|
);
|
|
|
|
// Extract credential values from params (checkout form data)
|
|
$credentialFields = $this->getCredentialFieldsForService($service);
|
|
$credentials = [];
|
|
foreach ($credentialFields as $field) {
|
|
$fieldName = $field['name'];
|
|
if (isset($params[$fieldName])) {
|
|
$credentials[$fieldName] = $params[$fieldName];
|
|
}
|
|
}
|
|
|
|
$service->properties()->updateOrCreate(
|
|
['key' => 'credentials_data'],
|
|
['value' => json_encode($credentials)]
|
|
);
|
|
$service->properties()->updateOrCreate(
|
|
['key' => 'deployed_at'],
|
|
['value' => '']
|
|
);
|
|
$this->sendDiscordNotification($service, "🆕 New manual server order received and queued for setup.");
|
|
return true;
|
|
}
|
|
public function suspendServer(Service $service, $params = []): bool
|
|
{
|
|
$service->properties()->updateOrCreate(
|
|
['key' => 'deploy_status'],
|
|
['value' => 'suspended']
|
|
);
|
|
return true;
|
|
}
|
|
public function unsuspendServer(Service $service, $params = []): bool
|
|
{
|
|
$currentStatus = $service->properties->where('key', 'deploy_status')->first()?->value;
|
|
if ($currentStatus === 'deployed') {
|
|
return true;
|
|
}
|
|
$service->properties()->updateOrCreate(
|
|
['key' => 'deploy_status'],
|
|
['value' => 'pending']
|
|
);
|
|
return true;
|
|
}
|
|
public function terminateServer(Service $service, $params = []): bool
|
|
{
|
|
$service->properties()->updateOrCreate(
|
|
['key' => 'deploy_status'],
|
|
['value' => 'terminated']
|
|
);
|
|
return true;
|
|
}
|
|
public function getProcessesForService(Service $service): array
|
|
{
|
|
$product = $service->product;
|
|
$processesSetting = $product->settings->where('key', 'processes')->first();
|
|
if ($processesSetting) {
|
|
$value = $processesSetting->value;
|
|
if (is_array($value)) {
|
|
return array_map('trim', $value);
|
|
}
|
|
if (is_string($value)) {
|
|
$decoded = json_decode($value, true);
|
|
if (is_array($decoded)) {
|
|
if (isset($decoded[0]) && is_array($decoded[0]) && isset($decoded[0]['name'])) {
|
|
return array_column($decoded, 'name');
|
|
}
|
|
return array_map('trim', $decoded);
|
|
}
|
|
}
|
|
}
|
|
return ['Order Received', 'Server Setup In Progress', 'Server Ready'];
|
|
}
|
|
public function getCredentialFieldsForService(Service $service): array
|
|
{
|
|
$product = $service->product;
|
|
|
|
$fields = [];
|
|
$usedNames = [];
|
|
$allowedTypes = ['text', 'password', 'textarea', 'url', 'email'];
|
|
|
|
for ($i = 1; $i <= 6; $i++) {
|
|
$label = trim((string) ($product->settings->where('key', "credential_{$i}_label")->first()?->value ?? ''));
|
|
if ($label === '') {
|
|
continue;
|
|
}
|
|
|
|
$type = strtolower(trim((string) ($product->settings->where('key', "credential_{$i}_type")->first()?->value ?? 'text')));
|
|
if (!in_array($type, $allowedTypes, true)) {
|
|
$type = 'text';
|
|
}
|
|
|
|
$name = \Illuminate\Support\Str::slug($label, '_');
|
|
if ($name === '') {
|
|
$name = "credential_{$i}";
|
|
}
|
|
|
|
if (in_array($name, $usedNames, true)) {
|
|
$name = $name . '_' . $i;
|
|
}
|
|
|
|
$usedNames[] = $name;
|
|
$fields[] = [
|
|
'name' => $name,
|
|
'label' => $label,
|
|
'type' => $type,
|
|
];
|
|
}
|
|
|
|
if (!empty($fields)) {
|
|
return $fields;
|
|
}
|
|
|
|
$credentialsJson = $product->settings->where('key', 'credentials_json')->first()?->value;
|
|
if (!empty($credentialsJson)) {
|
|
$credentials = json_decode($credentialsJson, true);
|
|
if (is_array($credentials)) {
|
|
return $credentials;
|
|
}
|
|
}
|
|
return [];
|
|
}
|
|
public function getButtonsForService(Service $service): array
|
|
{
|
|
$product = $service->product;
|
|
|
|
$buttons = [];
|
|
for ($i = 1; $i <= 5; $i++) {
|
|
$label = trim((string) ($product->settings->where('key', "button_{$i}_label")->first()?->value ?? ''));
|
|
$url = trim((string) ($product->settings->where('key', "button_{$i}_url")->first()?->value ?? ''));
|
|
|
|
if ($label === '' || $url === '') {
|
|
continue;
|
|
}
|
|
|
|
$buttons[] = [
|
|
'label' => $label,
|
|
'url' => $url,
|
|
];
|
|
}
|
|
|
|
if (!empty($buttons)) {
|
|
return $buttons;
|
|
}
|
|
|
|
$buttonsJson = $product->settings->where('key', 'buttons_json')->first()?->value;
|
|
if (!empty($buttonsJson)) {
|
|
$buttons = json_decode($buttonsJson, true);
|
|
if (is_array($buttons)) {
|
|
return $buttons;
|
|
}
|
|
}
|
|
return [];
|
|
}
|
|
public function sendDiscordNotification(Service $service, string $message): void
|
|
{
|
|
$webhookUrl = $this->config('discord_webhook');
|
|
if (empty($webhookUrl)) {
|
|
return;
|
|
}
|
|
try {
|
|
$user = $service->user;
|
|
$product = $service->product;
|
|
$data = $this->getServiceData($service);
|
|
$completedCount = collect($data['processes'])->where('completed', true)->count();
|
|
$totalCount = count($data['processes']);
|
|
$embed = [
|
|
'title' => 'Manual Server Status Update',
|
|
'description' => $message,
|
|
'color' => $data['status'] === 'deployed' ? 5763719 : 16776960,
|
|
'fields' => [
|
|
['name' => 'Service ID', 'value' => '#' . (string)$service->id, 'inline' => true],
|
|
['name' => 'User', 'value' => $user->name . ' (' . $user->email . ')', 'inline' => false],
|
|
['name' => 'Product', 'value' => $product->name, 'inline' => true],
|
|
['name' => 'Status', 'value' => ucfirst($data['status']), 'inline' => true],
|
|
['name' => 'Setup Progress', 'value' => "{$completedCount}/{$totalCount} steps completed", 'inline' => true],
|
|
],
|
|
'footer' => ['text' => 'ManualServers v' . self::$version],
|
|
'timestamp' => now()->toIso8601String(),
|
|
];
|
|
\Illuminate\Support\Facades\Http::withOptions([
|
|
'verify' => false,
|
|
'timeout' => 10,
|
|
'connect_timeout' => 10,
|
|
])->post($webhookUrl, [
|
|
'embeds' => [$embed],
|
|
]);
|
|
} catch (\Exception $e) {
|
|
\Illuminate\Support\Facades\Log::error('Discord notification failed', [
|
|
'service_id' => $service->id,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
public function getServiceData(Service $service): array
|
|
{
|
|
$service->load('properties');
|
|
$status = $service->properties->where('key', 'deploy_status')->first()?->value ?? 'pending';
|
|
$processesJson = $service->properties->where('key', 'processes_data')->first()?->value ?? '[]';
|
|
$credentialsJson = $service->properties->where('key', 'credentials_data')->first()?->value ?? '[]';
|
|
$deployedAt = $service->properties->where('key', 'deployed_at')->first()?->value;
|
|
$feedback = $service->properties->where('key', 'feedback')->first()?->value ?? '';
|
|
|
|
// Read credentials from individual properties
|
|
$credentialFields = $this->getCredentialFieldsForService($service);
|
|
$credentials = [];
|
|
foreach ($credentialFields as $field) {
|
|
$fieldName = $field['name'];
|
|
$value = $service->properties->where('key', $fieldName)->first()?->value;
|
|
if ($value !== null) {
|
|
$credentials[$fieldName] = $value;
|
|
}
|
|
}
|
|
|
|
// Fallback to credentials_data JSON if individual properties are empty
|
|
if (empty($credentials)) {
|
|
$credentials = json_decode($credentialsJson, true) ?? [];
|
|
}
|
|
|
|
return [
|
|
'status' => $status,
|
|
'processes' => json_decode($processesJson, true) ?? [],
|
|
'credentials' => $credentials,
|
|
'deployed_at' => $deployedAt,
|
|
'feedback' => $feedback,
|
|
'credential_fields' => $credentialFields,
|
|
'buttons' => $this->getButtonsForService($service),
|
|
'instructions' => $service->product->settings->where('key', 'instructions')->first()?->value ?? '',
|
|
];
|
|
}
|
|
public function updateProcessStatus(Service $service, int $processIndex, bool $completed): bool
|
|
{
|
|
$processesJson = $service->properties->where('key', 'processes_data')->first()?->value ?? '[]';
|
|
$processes = json_decode($processesJson, true) ?? [];
|
|
if (isset($processes[$processIndex])) {
|
|
$processes[$processIndex]['completed'] = $completed;
|
|
$processes[$processIndex]['completed_at'] = $completed ? now()->toDateTimeString() : null;
|
|
$service->properties()->updateOrCreate(
|
|
['key' => 'processes_data'],
|
|
['value' => json_encode($processes)]
|
|
);
|
|
$service->refresh();
|
|
if ($completed) {
|
|
$this->sendDiscordNotification($service, "Setup step **{$processes[$processIndex]['name']}** has been completed.");
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
public function deployService(Service $service, array $credentials): bool
|
|
{
|
|
$service->properties()->updateOrCreate(
|
|
['key' => 'credentials_data'],
|
|
['value' => json_encode($credentials)]
|
|
);
|
|
$service->properties()->updateOrCreate(
|
|
['key' => 'deploy_status'],
|
|
['value' => 'deployed']
|
|
);
|
|
$service->properties()->updateOrCreate(
|
|
['key' => 'deployed_at'],
|
|
['value' => now()->toDateTimeString()]
|
|
);
|
|
$service->refresh();
|
|
$this->sendDiscordNotification($service, "🎉 Manual server has been **set up and deployed** successfully!");
|
|
NotificationHelper::serverCreatedNotification($service->user, $service);
|
|
return true;
|
|
}
|
|
public function reverseDeployment(Service $service, bool $resetSteps = true): bool
|
|
{
|
|
if ($resetSteps) {
|
|
$processesJson = $service->properties->where('key', 'processes_data')->first()?->value ?? '[]';
|
|
$processes = json_decode($processesJson, true) ?? [];
|
|
foreach ($processes as $index => $process) {
|
|
$processes[$index]['completed'] = false;
|
|
$processes[$index]['completed_at'] = null;
|
|
}
|
|
$service->properties()->updateOrCreate(
|
|
['key' => 'processes_data'],
|
|
['value' => json_encode($processes)]
|
|
);
|
|
}
|
|
$service->properties()->updateOrCreate(
|
|
['key' => 'deploy_status'],
|
|
['value' => 'pending']
|
|
);
|
|
$service->properties()->updateOrCreate(
|
|
['key' => 'credentials_data'],
|
|
['value' => json_encode([])]
|
|
);
|
|
$service->properties()->updateOrCreate(
|
|
['key' => 'deployed_at'],
|
|
['value' => '']
|
|
);
|
|
$service->refresh();
|
|
$this->sendDiscordNotification($service, "↩️ Manual server deployment was reverted and setup steps were reset.");
|
|
return true;
|
|
}
|
|
public function areAllProcessesCompleted(Service $service): bool
|
|
{
|
|
$processesJson = $service->properties->where('key', 'processes_data')->first()?->value ?? '[]';
|
|
$processes = json_decode($processesJson, true) ?? [];
|
|
foreach ($processes as $process) {
|
|
if (!($process['completed'] ?? false)) {
|
|
return false;
|
|
}
|
|
}
|
|
return !empty($processes);
|
|
}
|
|
public function getActions(Service $service, $settings = [], $properties = []): array
|
|
{
|
|
$actions = [
|
|
[
|
|
'type' => 'view',
|
|
'name' => 'deployment_status',
|
|
'label' => 'Server Status',
|
|
],
|
|
];
|
|
$customButtons = $this->getButtonsForService($service);
|
|
foreach ($customButtons as $button) {
|
|
if (!empty($button['label']) && !empty($button['url'])) {
|
|
$actions[] = [
|
|
'type' => 'button',
|
|
'label' => $button['label'],
|
|
'url' => $button['url'],
|
|
];
|
|
}
|
|
}
|
|
return $actions;
|
|
}
|
|
public function getView(Service $service, $settings = [], $properties = [], $viewName = null)
|
|
{
|
|
if ($viewName === 'deployment_status') {
|
|
$deployData = $this->getServiceData($service);
|
|
return view('manual-servers::client.service-status', [
|
|
'service' => $service,
|
|
'deployData' => $deployData,
|
|
])->render();
|
|
}
|
|
return '';
|
|
}
|
|
}
|