'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 ''; } }