nodeCapacityService->findAvailableNode((int) $plan->disk_gb); $resourceLimits = $this->resourcePolicyService->defaultLimitsForProduct(null); if (!$node) { throw new \RuntimeException('No available hosting nodes for shared hosting'); } // Generate unique username $username = $this->generateUsername($user, $domain); // Create account record $account = HostingAccount::create([ 'user_id' => $user->id, 'hosting_plan_id' => $plan->id, 'hosting_node_id' => $node->id, 'username' => $username, 'primary_domain' => $domain, 'type' => HostingAccount::TYPE_SHARED, 'status' => HostingAccount::STATUS_PROVISIONING, 'allocated_disk_gb' => (int) $plan->disk_gb, 'cpu_limit_percent' => $resourceLimits['cpu_limit_percent'], 'memory_limit_mb' => $resourceLimits['memory_limit_mb'], 'process_limit' => $resourceLimits['process_limit'], 'io_limit_mb' => $resourceLimits['io_limit_mb'], 'inode_limit' => $resourceLimits['inode_limit'], 'resource_status' => HostingAccount::RESOURCE_STATUS_ACTIVE, 'php_version' => $plan->php_versions[0] ?? '8.2', 'resource_limits' => [ 'disk_gb' => $plan->disk_gb, 'bandwidth_gb' => $plan->bandwidth_gb, 'max_domains' => $plan->max_domains, 'max_databases' => $plan->max_databases, 'cpu_limit_percent' => $resourceLimits['cpu_limit_percent'], 'memory_limit_mb' => $resourceLimits['memory_limit_mb'], 'process_limit' => $resourceLimits['process_limit'], 'io_limit_mb' => $resourceLimits['io_limit_mb'], 'inode_limit' => $resourceLimits['inode_limit'], ], ]); // Create provisioning job $job = ProvisioningJob::create([ 'type' => 'create_shared_account', 'provisionable_type' => HostingAccount::class, 'provisionable_id' => $account->id, 'provider' => 'shared_node', 'status' => ProvisioningJob::STATUS_PENDING, 'payload' => [ 'username' => $username, 'domain' => $domain, 'node_id' => $node->id, ], ]); // Dispatch async provisioning dispatch(function () use ($account, $job, $node, $domain) { $this->provisionSharedAccount($account, $job, $node, $domain); })->afterCommit(); return $account; }); } public function createVpsInstance(User $user, HostingPlan $plan, array $config): VpsInstance { return DB::transaction(function () use ($user, $plan, $config) { $hostname = $config['hostname'] ?? 'vps-' . Str::random(8) . '.ladill.com'; $instance = VpsInstance::create([ 'user_id' => $user->id, 'hosting_plan_id' => $plan->id, 'name' => $config['name'] ?? $hostname, 'hostname' => $hostname, 'provider' => 'contabo', 'region' => $config['region'] ?? 'EU', 'image' => $config['image'] ?? 'afecbb85-e2fc-46f0-9684-b46b1faf00bb', 'cpu_cores' => $plan->cpu_cores, 'ram_mb' => $plan->ram_mb, 'disk_gb' => $plan->disk_gb, 'status' => 'provisioning', 'ssh_public_key' => $config['ssh_public_key'] ?? null, 'tags' => $config['tags'] ?? [], ]); $job = ProvisioningJob::create([ 'type' => 'create_vps_instance', 'provisionable_type' => VpsInstance::class, 'provisionable_id' => $instance->id, 'provider' => 'contabo', 'status' => ProvisioningJob::STATUS_PENDING, 'payload' => [ 'product_id' => $plan->contabo_product_id, 'region' => $config['region'] ?? 'EU', 'image' => $config['image'] ?? 'afecbb85-e2fc-46f0-9684-b46b1faf00bb', 'ssh_keys' => $config['ssh_keys'] ?? [], 'user_data' => $config['user_data'] ?? null, ], ]); dispatch(function () use ($instance, $job, $config, $plan) { $this->provisionVpsInstance($instance, $job, $config, $plan); })->afterCommit(); return $instance; }); } public function addSiteToAccount(HostingAccount $account, string $domain, string $type = 'addon'): HostedSite { $docRoot = "/home/{$account->username}/public_html/{$domain}"; $site = HostedSite::create([ 'hosting_account_id' => $account->id, 'domain' => $domain, 'document_root' => $docRoot, 'type' => $type, 'status' => 'pending', 'php_version' => $account->php_version, ]); try { $this->sharedNodeProvider->addSite($site); $site->update(['status' => 'active']); // Sync to Domain registry $this->syncToDomainRegistry($account, $domain, $site); } catch (\Exception $e) { $site->update(['status' => 'disabled']); throw $e; } return $site; } /** * Sync hosting site to unified Domain registry */ private function syncToDomainRegistry(HostingAccount $account, string $host, HostedSite $site): void { try { $domain = Domain::where('host', $host)->first(); if ($domain) { // Update existing domain to add hosting $domain->update([ 'hosting_account_id' => $account->id, 'source' => $domain->email_domain_id ? 'hosting_email' : 'hosting', 'onboarding_state' => $domain->onboarding_state === 'pending_ns' ? null : $domain->onboarding_state, ]); } else { // Create new domain entry Domain::create([ 'user_id' => $account->user_id, 'host' => $host, 'hosting_account_id' => $account->id, 'source' => 'hosting', 'type' => 'custom', 'status' => 'verified', 'onboarding_state' => null, 'verified_at' => now(), ]); } Log::info('HostingProvisioningService: Synced to Domain registry', [ 'domain' => $host, 'site_id' => $site->id, ]); } catch (\Throwable $e) { Log::warning('HostingProvisioningService: Failed to sync to Domain registry', [ 'domain' => $host, 'error' => $e->getMessage(), ]); } } public function suspendAccount(HostingAccount $account, string $reason): bool { $result = $this->sharedNodeProvider->suspendAccount($account, $reason); if ($result) { $account->update([ 'status' => HostingAccount::STATUS_SUSPENDED, 'suspension_reason' => $reason, 'suspended_at' => now(), ]); } return $result; } public function unsuspendAccount(HostingAccount $account): bool { $result = $this->sharedNodeProvider->unsuspendAccount($account); if ($result) { $account->update([ 'status' => HostingAccount::STATUS_ACTIVE, 'suspension_reason' => null, 'suspended_at' => null, ]); } return $result; } public function terminateAccount(HostingAccount $account): bool { $result = $this->sharedNodeProvider->terminateAccount($account); if ($result) { $account->node?->decrementAccountCount(); $account->update(['status' => HostingAccount::STATUS_TERMINATED]); $account->delete(); } return $result; } public function controlVps(VpsInstance $instance, string $action): bool { return match ($action) { 'start' => $this->contaboProvider->startInstance($instance->provider_instance_id), 'stop' => $this->contaboProvider->stopInstance($instance->provider_instance_id), 'reboot' => $this->contaboProvider->rebootInstance($instance->provider_instance_id), default => throw new \InvalidArgumentException("Unknown action: {$action}"), }; } public function terminateVps(VpsInstance $instance): bool { if ($instance->provider_instance_id) { $this->contaboProvider->deleteInstance($instance->provider_instance_id); } $instance->update(['status' => 'terminated']); $instance->delete(); return true; } private function provisionSharedAccount(HostingAccount $account, ProvisioningJob $job, HostingNode $node, string $domain): void { $job->markRunning(); try { $result = $this->sharedNodeProvider->createAccount($account); $account->update([ 'status' => HostingAccount::STATUS_ACTIVE, 'home_directory' => $result['home_directory'], 'provisioned_at' => now(), ]); // Create primary site $site = HostedSite::create([ 'hosting_account_id' => $account->id, 'domain' => $domain, 'document_root' => $result['document_root'], 'type' => 'primary', 'status' => 'pending', 'php_version' => $account->php_version, ]); $this->sharedNodeProvider->addSite($site); $site->update(['status' => 'active']); // Sync to Domain registry $this->syncToDomainRegistry($account, $domain, $site); $node->incrementAccountCount(); $job->markCompleted([ 'username' => $result['username'], 'home_directory' => $result['home_directory'], 'initial_password' => $result['password'], ]); Log::info("Shared hosting account provisioned", [ 'account_id' => $account->id, 'username' => $account->username, ]); } catch (\Exception $e) { Log::error("Shared hosting provisioning failed", [ 'account_id' => $account->id, 'error' => $e->getMessage(), ]); $account->update(['status' => HostingAccount::STATUS_FAILED]); $job->markFailed($e->getMessage()); } } private function provisionVpsInstance(VpsInstance $instance, ProvisioningJob $job, array $config, HostingPlan $plan): void { $job->markRunning(); try { $rootPassword = Str::password(16); $result = $this->contaboProvider->createInstance([ 'product_id' => $plan->contabo_product_id, 'name' => $instance->name, 'region' => $config['region'] ?? 'EU', 'image_id' => $config['image'] ?? 'afecbb85-e2fc-46f0-9684-b46b1faf00bb', 'root_password' => $rootPassword, 'ssh_keys' => $config['ssh_keys'] ?? [], 'user_data' => $config['user_data'] ?? null, ]); $instance->update([ 'provider_instance_id' => $result['instance_id'], 'ip_address' => $result['ip_address'], 'ipv6_address' => $result['ipv6_address'], 'datacenter' => $result['datacenter'], 'status' => 'running', 'power_status' => 'on', 'root_password_encrypted' => encrypt($rootPassword), 'provisioned_at' => now(), ]); $job->markCompleted([ 'instance_id' => $result['instance_id'], 'ip_address' => $result['ip_address'], 'root_password' => $rootPassword, ]); Log::info("VPS instance provisioned", [ 'instance_id' => $instance->id, 'provider_id' => $result['instance_id'], ]); } catch (\Exception $e) { Log::error("VPS provisioning failed", [ 'instance_id' => $instance->id, 'error' => $e->getMessage(), ]); $instance->update(['status' => 'failed']); $job->markFailed($e->getMessage()); } } private function generateUsername(User $user, string $domain): string { $base = preg_replace('/[^a-z0-9]/', '', strtolower(explode('.', $domain)[0])); $base = substr($base, 0, 8) ?: 'user'; $username = $base; $counter = 1; while (HostingAccount::where('username', $username)->exists()) { $username = $base . $counter; $counter++; } return $username; } }