Files
Virtnet.bond/extensions/Servers/ManualServers/ManualServers.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 '';
}
}