loadServerZones(); } /** * Get public IP addresses for the server */ public function getPublicIpsProperty(): array { $publicIps = []; foreach ($this->server->ipAddresses as $ip) { if ($this->isPublicIp($ip->ip, $ip->type)) { $publicIps[] = $ip; } } return $publicIps; } /** * Check if IP is public */ private function isPublicIp(string $ip, string $type = 'ipv4'): bool { if ($type === 'ipv6' || filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { if ($ip === '::1' || strpos($ip, '::1/') === 0) { return false; } if (preg_match('/^fe[89ab][0-9a-f]:/i', $ip)) { return false; } if (preg_match('/^fc[0-9a-f]{1,2}:/i', $ip) || preg_match('/^fd[0-9a-f]{1,2}:/i', $ip)) { return false; } if (strpos($ip, '::ffff:') === 0 || strpos($ip, '::ffff:') !== false) { return false; } return true; } else { $parts = explode('.', $ip); if (count($parts) !== 4) { return false; } $first = (int)$parts[0]; $second = (int)$parts[1]; if ($first === 10) { return false; } if ($first === 172 && $second >= 16 && $second <= 31) { return false; } if ($first === 192 && $second === 168) { return false; } if ($first === 127) { return false; } if ($first === 169 && $second === 254) { return false; } return true; } } /** * Load zones associated with this server */ public function loadServerZones(): void { try { $dns = new TechnitiumDns; $this->serverZones = $dns->getServerZones($this->server->id); } catch (\Exception $e) { $this->serverZones = []; $this->notify('Failed to load subdomains: ' . $e->getMessage(), 'error'); } } /** * Check if subdomain is available */ public function checkSubdomainAvailability(): void { if (empty($this->newSubdomain)) { return; } // Validate subdomain format if (!preg_match('/^[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?$/i', $this->newSubdomain)) { $this->notify('Invalid subdomain format. Use only letters, numbers, and hyphens.', 'error'); return; } try { $dns = new TechnitiumDns; if ($dns->subdomainExists($this->newSubdomain)) { $this->notify('This subdomain is already taken.', 'error'); } else { $this->notify('Subdomain is available!', 'success'); } } catch (\Exception $e) { $this->notify('Failed to check subdomain availability: ' . $e->getMessage(), 'error'); } } /** * Create a new subdomain */ public function createSubdomain(): void { $this->validate([ 'newSubdomain' => 'required|string|regex:/^[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?$/i', 'selectedIpId' => 'required|exists:ext_proxmox_ip_addresses,id', ], [ 'newSubdomain.regex' => 'Invalid subdomain format. Use only letters, numbers, and hyphens.', 'selectedIpId.required' => 'Please select an IP address.', ]); // Check if user already has a subdomain for this IP $selectedIp = $this->server->ipAddresses->firstWhere('id', $this->selectedIpId); if (!$selectedIp) { $this->notify('Selected IP address not found.', 'error'); return; } // Check if subdomain already exists in database try { // Determine the selected IP's type (IPv4 or IPv6) $selectedIpType = filter_var($selectedIp->ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? 'ipv6' : 'ipv4'; $recordType = $selectedIpType === 'ipv6' ? 'AAAA' : 'A'; // Check if user already has a subdomain for this specific IP address $selectedIpSubdomainData = $this->normalizeSubdomainData($selectedIp->subdomain); if ($selectedIpSubdomainData && !empty($selectedIpSubdomainData['subdomain'])) { $this->notify('You already have a subdomain for this IP address.', 'error'); return; } // Load every subdomain entry in the database to ensure global uniqueness rules $allSubdomainEntries = IPAddress::query() ->whereNotNull('subdomain') ->get(); $existingIpv4Count = 0; $existingIpv6Count = 0; foreach ($allSubdomainEntries as $existingIp) { $subdomainData = $this->normalizeSubdomainData($existingIp->subdomain); if (!$subdomainData) { continue; } $existingSubdomainName = $subdomainData['subdomain'] ?? null; if ($existingSubdomainName === $this->newSubdomain) { $existingIpType = filter_var($existingIp->ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? 'ipv6' : 'ipv4'; if ($existingIpType === 'ipv4') { $existingIpv4Count++; } else { $existingIpv6Count++; } } } // Check if we're trying to create a duplicate of the same IP type if ($selectedIpType === 'ipv4' && $existingIpv4Count > 0) { $this->notify('This subdomain already exists for an IPv4 address on this server. Please choose a different name.', 'error'); return; } if ($selectedIpType === 'ipv6' && $existingIpv6Count > 0) { $this->notify('This subdomain already exists for an IPv6 address on this server. Please choose a different name.', 'error'); return; } // Check if we already have both IPv4 and IPv6 for this subdomain if ($existingIpv4Count > 0 && $existingIpv6Count > 0) { $this->notify('This subdomain already exists for both IPv4 and IPv6 addresses on this server. Please choose a different name.', 'error'); return; } // Create the subdomain via Technitium DNS API $dns = new TechnitiumDns; $result = $dns->createZone($this->newSubdomain, $selectedIp->ip, $this->server->id, $selectedIp->id); if (($result['success'] ?? false) === true) { $recordType = $result['recordType'] ?? ($selectedIpType === 'ipv6' ? 'AAAA' : 'A'); $records = [ ['name' => '@', 'type' => $recordType, 'rdata' => $selectedIp->ip, 'ttl' => 3600], ]; if (($result['wildcardCreated'] ?? false) === true) { $records[] = ['name' => '*', 'type' => $recordType, 'rdata' => $selectedIp->ip, 'ttl' => 3600]; } $selectedIp->subdomain = [ 'subdomain' => $this->newSubdomain, 'fullDomain' => $this->newSubdomain . '.dns.virtnet.host', 'ipAddress' => $selectedIp->ip, 'records' => $records, ]; $selectedIp->save(); $selectedIp->refresh(); // Refresh the server's IP addresses collection $this->server->load('ipAddresses'); $this->notify('Subdomain created successfully!', 'success'); $this->newSubdomain = ''; $this->selectedIpId = null; $this->showCreateModal = false; $this->loadServerZones(); } else { $errorMessage = $result['message'] ?? 'Failed to create subdomain.'; $this->notify($errorMessage, 'error'); } } catch (\Exception $e) { $this->notify('Failed to create subdomain: ' . $e->getMessage(), 'error'); } } /** * Generate a random subdomain from word pool */ public function generateRandomSubdomain(): void { $adjectives = $this->adjectivePool(); $animals = $this->animalPool(); $adjective = Str::slug(Arr::random($adjectives), ''); $animal = Str::slug(Arr::random($animals), ''); $this->newSubdomain = strtolower($adjective . '-' . $animal); } protected function adjectivePool(): array { static $pool = null; if ($pool === null) { $pool = $this->expandPool($this->baseAdjectives); } return $pool; } protected function animalPool(): array { static $pool = null; if ($pool === null) { $pool = $this->expandPool($this->baseAnimals); } return $pool; } protected function expandPool(array $base): array { if (count($base) >= 1000) { return array_slice($base, 0, 1000); } $pool = $base; while (count($pool) < 1000) { $pool = array_merge($pool, $base); } return array_slice($pool, 0, 1000); } /** * Open delete confirmation modal */ public function openDeleteModal(string $subdomain, int $ipAddressId): void { $this->subdomainToDelete = $subdomain; $this->ipAddressIdToDelete = $ipAddressId; $this->subdomainToDeleteFull = $subdomain . '.dns.virtnet.host'; $this->showDeleteModal = true; } /** * Close delete confirmation modal */ public function closeDeleteModal(): void { $this->showDeleteModal = false; $this->subdomainToDelete = null; $this->ipAddressIdToDelete = null; $this->subdomainToDeleteFull = null; } /** * Delete a subdomain */ public function deleteSubdomain(): void { if (!$this->subdomainToDelete || !$this->ipAddressIdToDelete) { $this->notify('Invalid subdomain selection.', 'error'); return; } try { $dns = new TechnitiumDns; if ($dns->deleteZone($this->subdomainToDelete, $this->ipAddressIdToDelete)) { $this->notify('Subdomain deleted successfully!', 'success'); $this->loadServerZones(); $this->closeDeleteModal(); } else { $this->notify('Failed to delete subdomain.', 'error'); } } catch (\Exception $e) { $this->notify('Failed to delete subdomain: ' . $e->getMessage(), 'error'); } } /** * Open record creation modal */ public function openRecordModal(string $subdomain): void { $this->selectedZone = $subdomain; $this->recordName = ''; $this->recordType = 'A'; $this->recordRdata = ''; $this->recordTtl = 3600; $this->loadZoneRecords(); $this->showRecordModal = true; } /** * Load records for a zone */ public function loadZoneRecords(): void { if (!$this->selectedZone) { return; } try { $dns = new TechnitiumDns; $records = $dns->getZoneRecords($this->selectedZone); // Ensure we have an array and it's properly formatted $this->zoneRecords = is_array($records) ? $records : []; } catch (\Exception $e) { $this->zoneRecords = []; // Don't show error on every load, only log it \Log::debug('Failed to load DNS records: ' . $e->getMessage()); } } /** * Add a DNS record */ public function addRecord(): void { $this->validate([ 'recordName' => 'required|string', 'recordType' => 'required|in:A,AAAA', 'recordRdata' => 'required|string', 'recordTtl' => 'required|integer|min:60|max:86400', ]); if (!$this->selectedZone) { $this->notify('No zone selected.', 'error'); return; } // Validate IP address format if ($this->recordType === 'A' && !filter_var($this->recordRdata, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { $this->notify('Invalid IPv4 address.', 'error'); return; } if ($this->recordType === 'AAAA' && !filter_var($this->recordRdata, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $this->notify('Invalid IPv6 address.', 'error'); return; } try { $dns = new TechnitiumDns; if ($dns->addRecord($this->selectedZone, $this->recordName, $this->recordType, $this->recordRdata, $this->recordTtl)) { $this->notify('DNS record added successfully!', 'success'); $this->recordName = ''; $this->recordRdata = ''; // Small delay to ensure API has processed the record usleep(500000); // 0.5 seconds // Reload records from DNS API to get the latest data $this->loadZoneRecords(); } else { $this->notify('Failed to add DNS record.', 'error'); } } catch (\Exception $e) { $this->notify('Failed to add DNS record: ' . $e->getMessage(), 'error'); } } /** * Delete a DNS record */ public function deleteRecord(string $name, string $type): void { if (!$this->selectedZone) { $this->notify('No zone selected.', 'error'); return; } try { // Find the record to get its IP address $ipAddress = null; foreach ($this->zoneRecords as $record) { if (($record['name'] ?? '') === $name && ($record['type'] ?? '') === $type) { $ipAddress = $record['rdata'] ?? $record['ipAddress'] ?? null; break; } } $dns = new TechnitiumDns; if ($dns->deleteRecord($this->selectedZone, $name, $type, $ipAddress)) { // Update database with new record list $this->updateSubdomainRecords(); $this->notify('DNS record deleted successfully!', 'success'); $this->loadZoneRecords(); } else { $this->notify('Failed to delete DNS record.', 'error'); } } catch (\Exception $e) { $this->notify('Failed to delete DNS record: ' . $e->getMessage(), 'error'); } } /** * Get available IPs for creating subdomains */ public function getAvailableIpsForSubdomainProperty(): array { $publicIps = $this->publicIps; // Filter out IPs that already have subdomains return collect($publicIps)->filter(function ($ip) { // Check if this IP already has a subdomain assigned $subdomainData = $this->normalizeSubdomainData($ip->subdomain); if ($subdomainData && !empty($subdomainData['subdomain'])) { return false; } return true; })->values()->all(); } /** * Normalize subdomain storage values to an array regardless of DB column type */ private function normalizeSubdomainData(mixed $value): ?array { if (!$value) { return null; } if (is_array($value)) { return $value; } if (is_string($value)) { $decoded = json_decode($value, true); return is_array($decoded) ? $decoded : null; } return null; } public function render() { return view('proxmox::subdomain')->layoutData([ 'sidebar' => true, ]); } }