Files
jabali-panel/app/Filament/Admin/Pages/DnsZones.php
2026-01-24 19:36:46 +02:00

893 lines
38 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Filament\Admin\Pages;
use App\Filament\Admin\Widgets\DnsPendingAddsTable;
use App\Filament\Concerns\HasPageTour;
use App\Models\DnsRecord;
use App\Models\DnsSetting;
use App\Models\Domain;
use App\Services\Agent\AgentClient;
use BackedEnum;
use Exception;
use Filament\Actions\Action;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Schemas\Components\EmbeddedSchema;
use Filament\Schemas\Components\EmbeddedTable;
use Filament\Schemas\Components\EmptyState;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Text;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Table;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Str;
use Livewire\Attributes\On;
class DnsZones extends Page implements HasActions, HasForms, HasTable
{
use HasPageTour;
use InteractsWithActions;
use InteractsWithForms;
use InteractsWithTable;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-server-stack';
protected static ?int $navigationSort = 7;
public ?int $selectedDomainId = null;
protected ?AgentClient $agent = null;
// Pending changes tracking
public array $pendingEdits = []; // [record_id => [field => value]]
public array $pendingDeletes = []; // [record_id, ...]
public array $pendingAdds = []; // [[field => value], ...]
public static function getNavigationLabel(): string
{
return __('DNS Zones');
}
public function getTitle(): string|Htmlable
{
return __('DNS Zone Manager');
}
public function getAgent(): AgentClient
{
return $this->agent ??= new AgentClient;
}
public function mount(): void
{
// Start with no domain selected
$this->selectedDomainId = null;
}
public function form(Schema $form): Schema
{
return $form
->schema([
Select::make('selectedDomainId')
->label(__('Select Domain'))
->options(fn () => Domain::orderBy('domain')->pluck('domain', 'id')->toArray())
->searchable()
->preload()
->live()
->afterStateUpdated(fn () => $this->onDomainChange())
->placeholder(__('Select a domain to manage DNS records')),
]);
}
public function content(Schema $schema): Schema
{
return $schema->schema([
Section::make(__('Select Domain'))
->description(__('Choose a domain to manage DNS records.'))
->schema([
EmbeddedSchema::make('form'),
]),
Section::make(__('Zone Status'))
->description(fn () => $this->getSelectedDomain()?->domain)
->icon('heroicon-o-signal')
->headerActions([
Action::make('rebuildZone')
->label(__('Rebuild Zone'))
->icon('heroicon-o-arrow-path')
->color('gray')
->action(fn () => $this->rebuildCurrentZone()),
Action::make('deleteZone')
->label(__('Delete Zone'))
->icon('heroicon-o-trash')
->color('danger')
->requiresConfirmation()
->modalHeading(__('Delete DNS Zone'))
->modalDescription(__('Delete DNS zone for this domain? All records will be removed.'))
->action(fn () => $this->deleteCurrentZone()),
])
->schema([
Grid::make(['default' => 1, 'sm' => 3])->schema([
Text::make(fn () => (($this->getZoneStatus() ?? [])['zone_file_exists'] ?? false) ? __('Active') : __('Missing'))
->badge()
->color(fn () => (($this->getZoneStatus() ?? [])['zone_file_exists'] ?? false) ? 'success' : 'danger'),
Text::make(fn () => __(':count records', ['count' => ($this->getZoneStatus() ?? [])['records_count'] ?? 0]))
->badge()
->color('gray'),
Text::make(fn () => __('Owner: :owner', ['owner' => ($this->getZoneStatus() ?? [])['user'] ?? 'N/A']))
->color('gray'),
]),
])
->visible(fn () => $this->selectedDomainId !== null),
Section::make(__('New Records to Add'))
->description(__('These records will be created when you save changes.'))
->icon('heroicon-o-plus-circle')
->iconColor('success')
->collapsible()
->schema([
EmbeddedTable::make(DnsPendingAddsTable::class, fn () => [
'records' => $this->pendingAdds,
]),
])
->headerActions([
Action::make('clearPending')
->label(__('Clear All'))
->icon('heroicon-o-trash')
->color('danger')
->size('sm')
->requiresConfirmation()
->action(fn () => $this->clearPendingAdds()),
])
->visible(fn () => $this->selectedDomainId !== null && count($this->pendingAdds) > 0),
EmbeddedTable::make()
->visible(fn () => $this->selectedDomainId !== null),
EmptyState::make(__('No Domain Selected'))
->description(__('Select a domain from the dropdown above to manage DNS records.'))
->icon('heroicon-o-globe-alt')
->iconColor('gray')
->visible(fn () => $this->selectedDomainId === null),
]);
}
public function onDomainChange(): void
{
// Discard pending changes when switching domains
$this->pendingEdits = [];
$this->pendingDeletes = [];
$this->pendingAdds = [];
$this->resetTable();
}
public function updatedSelectedDomainId(): void
{
$this->onDomainChange();
}
// Pending changes helpers
public function hasPendingChanges(): bool
{
return count($this->pendingEdits) > 0 || count($this->pendingDeletes) > 0 || count($this->pendingAdds) > 0;
}
public function getPendingChangesCount(): int
{
return count($this->pendingEdits) + count($this->pendingDeletes) + count($this->pendingAdds);
}
public function isRecordPendingDelete(int $recordId): bool
{
return in_array($recordId, $this->pendingDeletes);
}
public function isRecordPendingEdit(int $recordId): bool
{
return isset($this->pendingEdits[$recordId]);
}
public function clearPendingAdds(): void
{
$this->pendingAdds = [];
Notification::make()->title(__('Pending records cleared'))->success()->send();
}
public function table(Table $table): Table
{
return $table
->query(
DnsRecord::query()
->when($this->selectedDomainId, fn (Builder $query) => $query->where('domain_id', $this->selectedDomainId))
->orderByRaw("CASE type
WHEN 'NS' THEN 1
WHEN 'A' THEN 2
WHEN 'AAAA' THEN 3
WHEN 'CNAME' THEN 4
WHEN 'MX' THEN 5
WHEN 'TXT' THEN 6
WHEN 'SRV' THEN 7
WHEN 'CAA' THEN 8
ELSE 9 END")
->orderBy('name')
)
->columns([
TextColumn::make('type')
->label(__('Type'))
->badge()
->color(fn (DnsRecord $record) => $this->isRecordPendingDelete($record->id) ? 'danger' : match ($record->type) {
'A', 'AAAA' => 'info',
'CNAME' => 'primary',
'MX' => 'warning',
'TXT' => 'success',
'NS' => 'danger',
'SRV' => 'primary',
'CAA' => 'warning',
default => 'gray',
})
->sortable(),
TextColumn::make('name')
->label(__('Name'))
->fontFamily('mono')
->color(fn (DnsRecord $record) => $this->isRecordPendingDelete($record->id) ? 'danger' : null)
->searchable(),
TextColumn::make('content')
->label(__('Content'))
->fontFamily('mono')
->limit(50)
->tooltip(fn ($record) => $record->content)
->color(fn (DnsRecord $record) => $this->isRecordPendingDelete($record->id) ? 'danger' : ($this->isRecordPendingEdit($record->id) ? 'warning' : null))
->searchable(),
TextColumn::make('ttl')
->label(__('TTL'))
->color(fn (DnsRecord $record) => $this->isRecordPendingDelete($record->id) ? 'danger' : null)
->sortable(),
TextColumn::make('priority')
->label(__('Priority'))
->placeholder('-')
->color(fn (DnsRecord $record) => $this->isRecordPendingDelete($record->id) ? 'danger' : null)
->sortable(),
TextColumn::make('domain.user.username')
->label(__('Owner'))
->placeholder('N/A')
->sortable(),
])
->filters([])
->headerActions([
Action::make('resetToDefaults')
->label(__('Reset to Defaults'))
->icon('heroicon-o-arrow-path')
->color('gray')
->requiresConfirmation()
->modalHeading(__('Reset DNS Records'))
->modalDescription(__('This will delete all existing DNS records and create default records. This action cannot be undone.'))
->modalIcon('heroicon-o-exclamation-triangle')
->modalIconColor('warning')
->action(fn () => $this->resetToDefaults()),
Action::make('saveChanges')
->label(__('Save'))
->icon('heroicon-o-check')
->color('primary')
->action(fn () => $this->saveChanges()),
])
->recordActions([
Action::make('edit')
->label(__('Edit'))
->icon('heroicon-o-pencil')
->color(fn (DnsRecord $record) => $this->isRecordPendingEdit($record->id) ? 'warning' : 'gray')
->hidden(fn (DnsRecord $record) => $this->isRecordPendingDelete($record->id))
->modalHeading(__('Edit DNS Record'))
->modalDescription(__('Changes will be queued until you click "Save Changes".'))
->modalIcon('heroicon-o-pencil-square')
->modalIconColor('primary')
->modalSubmitActionLabel(__('Queue Changes'))
->fillForm(fn (DnsRecord $record) => [
'type' => $this->pendingEdits[$record->id]['type'] ?? $record->type,
'name' => $this->pendingEdits[$record->id]['name'] ?? $record->name,
'content' => $this->pendingEdits[$record->id]['content'] ?? $record->content,
'ttl' => $this->pendingEdits[$record->id]['ttl'] ?? $record->ttl,
'priority' => $this->pendingEdits[$record->id]['priority'] ?? $record->priority,
])
->form([
Select::make('type')
->label(__('Record Type'))
->options([
'A' => __('A - IPv4 Address'),
'AAAA' => __('AAAA - IPv6 Address'),
'CNAME' => __('CNAME - Canonical Name'),
'MX' => __('MX - Mail Exchange'),
'TXT' => __('TXT - Text Record'),
'NS' => __('NS - Nameserver'),
'SRV' => __('SRV - Service'),
'CAA' => __('CAA - Certificate Authority'),
])
->required()
->reactive(),
TextInput::make('name')
->label(__('Name'))
->placeholder(__('@ for root, or subdomain'))
->required()
->maxLength(255),
TextInput::make('content')
->label(__('Content'))
->required()
->maxLength(1024),
TextInput::make('ttl')
->label(__('TTL (seconds)'))
->numeric()
->minValue(60)
->maxValue(86400),
TextInput::make('priority')
->label(__('Priority'))
->numeric()
->visible(fn ($get) => in_array($get('type'), ['MX', 'SRV'])),
])
->action(function (DnsRecord $record, array $data): void {
// Queue the edit
$this->pendingEdits[$record->id] = [
'type' => $data['type'],
'name' => $data['name'],
'content' => $data['content'],
'ttl' => $data['ttl'] ?? 3600,
'priority' => $data['priority'] ?? null,
];
Notification::make()
->title(__('Edit queued'))
->body(__('Click "Save Changes" to apply.'))
->info()
->send();
}),
Action::make('delete')
->label(__('Delete'))
->icon('heroicon-o-trash')
->color('danger')
->requiresConfirmation()
->modalHeading(__('Delete Record'))
->modalDescription(fn (DnsRecord $record) => __('Delete the :type record for :name?', ['type' => $record->type, 'name' => $record->name]))
->modalIcon('heroicon-o-trash')
->modalIconColor('danger')
->modalSubmitActionLabel(__('Delete'))
->action(function (DnsRecord $record): void {
if (! in_array($record->id, $this->pendingDeletes)) {
$this->pendingDeletes[] = $record->id;
}
unset($this->pendingEdits[$record->id]);
}),
])
->emptyStateHeading(__('No DNS records'))
->emptyStateDescription(__('Add DNS records to manage this domain\'s DNS configuration.'))
->emptyStateIcon('heroicon-o-server-stack')
->striped();
}
public function getSelectedDomain(): ?Domain
{
return $this->selectedDomainId ? Domain::find($this->selectedDomainId) : null;
}
public function getZoneStatus(): ?array
{
if (! $this->selectedDomainId) {
return null;
}
$domain = Domain::find($this->selectedDomainId);
if (! $domain) {
return null;
}
$zoneFile = "/etc/bind/zones/db.{$domain->domain}";
$recordsCount = DnsRecord::where('domain_id', $this->selectedDomainId)->count();
return [
'domain' => $domain->domain,
'user' => $domain->user->username ?? 'N/A',
'records_count' => $recordsCount,
'zone_file_exists' => file_exists($zoneFile),
];
}
#[On('dns-pending-add-remove')]
public function removePendingAddFromTable(string $key): void
{
$this->removePendingAdd($key);
}
public function removePendingAdd(int|string $identifier): void
{
if (is_int($identifier)) {
unset($this->pendingAdds[$identifier]);
$this->pendingAdds = array_values($this->pendingAdds);
Notification::make()->title(__('Pending record removed'))->success()->send();
return;
}
$this->pendingAdds = array_values(array_filter(
$this->pendingAdds,
fn (array $record): bool => ($record['key'] ?? null) !== $identifier
));
Notification::make()->title(__('Pending record removed'))->success()->send();
}
protected function queuePendingAdd(array $record): void
{
$record['key'] ??= (string) Str::uuid();
$this->pendingAdds[] = $record;
}
protected function sanitizePendingAdd(array $record): array
{
unset($record['key']);
return $record;
}
public function saveChanges(bool $notify = true): void
{
if (! $this->hasPendingChanges()) {
if ($notify) {
Notification::make()->title(__('No changes to save'))->warning()->send();
}
return;
}
$domain = Domain::find($this->selectedDomainId);
if (! $domain) {
Notification::make()->title(__('Domain not found'))->danger()->send();
return;
}
try {
// Apply deletes
foreach ($this->pendingDeletes as $recordId) {
DnsRecord::where('id', $recordId)->delete();
}
// Apply edits
foreach ($this->pendingEdits as $recordId => $data) {
$record = DnsRecord::find($recordId);
if ($record) {
$record->update($data);
}
}
// Apply adds
foreach ($this->pendingAdds as $data) {
DnsRecord::create(array_merge(['domain_id' => $this->selectedDomainId], $this->sanitizePendingAdd($data)));
}
// Sync zone file
$this->syncZoneFile($domain->domain);
// Clear pending changes
$this->pendingEdits = [];
$this->pendingDeletes = [];
$this->pendingAdds = [];
// Reset table to refresh data
$this->resetTable();
if ($notify) {
Notification::make()
->title(__('Changes saved'))
->body(__('DNS records updated. Changes may take up to 48 hours to propagate.'))
->success()
->send();
}
} catch (Exception $e) {
Notification::make()
->title(__('Failed to save changes'))
->body($e->getMessage())
->danger()
->send();
}
}
public function discardChanges(): void
{
$this->pendingEdits = [];
$this->pendingDeletes = [];
$this->pendingAdds = [];
Notification::make()->title(__('Changes discarded'))->success()->send();
}
public function resetToDefaults(): void
{
$domain = Domain::find($this->selectedDomainId);
if (! $domain) {
Notification::make()->title(__('Domain not found'))->danger()->send();
return;
}
try {
// Delete all existing records
DnsRecord::where('domain_id', $this->selectedDomainId)->delete();
// Create default records
$settings = DnsSetting::getAll();
$serverIp = $domain->ip_address ?: ($settings['default_ip'] ?? trim(shell_exec("hostname -I | awk '{print $1}'") ?? '') ?: '127.0.0.1');
$serverIpv6 = $domain->ipv6_address ?: ($settings['default_ipv6'] ?? null);
$ns1 = $settings['ns1'] ?? 'ns1.'.$domain->domain;
$ns2 = $settings['ns2'] ?? 'ns2.'.$domain->domain;
$defaultRecords = [
['name' => '@', 'type' => 'NS', 'content' => $ns1, 'ttl' => 3600, 'priority' => null],
['name' => '@', 'type' => 'NS', 'content' => $ns2, 'ttl' => 3600, 'priority' => null],
['name' => '@', 'type' => 'A', 'content' => $serverIp, 'ttl' => 3600, 'priority' => null],
['name' => 'www', 'type' => 'A', 'content' => $serverIp, 'ttl' => 3600, 'priority' => null],
['name' => 'mail', 'type' => 'A', 'content' => $serverIp, 'ttl' => 3600, 'priority' => null],
['name' => '@', 'type' => 'MX', 'content' => 'mail.'.$domain->domain, 'ttl' => 3600, 'priority' => 10],
['name' => '@', 'type' => 'TXT', 'content' => 'v=spf1 mx a ~all', 'ttl' => 3600, 'priority' => null],
];
if (! empty($serverIpv6)) {
$defaultRecords[] = ['name' => '@', 'type' => 'AAAA', 'content' => $serverIpv6, 'ttl' => 3600, 'priority' => null];
$defaultRecords[] = ['name' => 'www', 'type' => 'AAAA', 'content' => $serverIpv6, 'ttl' => 3600, 'priority' => null];
$defaultRecords[] = ['name' => 'mail', 'type' => 'AAAA', 'content' => $serverIpv6, 'ttl' => 3600, 'priority' => null];
}
foreach ($defaultRecords as $record) {
DnsRecord::create(array_merge(['domain_id' => $this->selectedDomainId], $record));
}
// Sync zone file
$this->syncZoneFile($domain->domain);
// Clear any pending changes
$this->pendingEdits = [];
$this->pendingDeletes = [];
$this->pendingAdds = [];
$this->resetTable();
Notification::make()
->title(__('DNS records reset'))
->body(__('Default records have been created for :domain', ['domain' => $domain->domain]))
->success()
->send();
} catch (Exception $e) {
Notification::make()
->title(__('Failed to reset records'))
->body($e->getMessage())
->danger()
->send();
}
}
protected function getHeaderActions(): array
{
return [
$this->getTourAction(),
Action::make('syncAllZones')
->label(__('Sync All Zones'))
->icon('heroicon-o-arrow-path')
->color('primary')
->requiresConfirmation()
->modalDescription(__('This will regenerate all zone files from the database.'))
->action(function () {
$count = 0;
$domains = Domain::all();
$settings = DnsSetting::getAll();
foreach ($domains as $domain) {
try {
$records = DnsRecord::where('domain_id', $domain->id)->get()->toArray();
$this->getAgent()->send('dns.sync_zone', [
'domain' => $domain->domain,
'records' => $records,
'ns1' => $settings['ns1'] ?? 'ns1.example.com',
'ns2' => $settings['ns2'] ?? 'ns2.example.com',
'admin_email' => $settings['admin_email'] ?? 'admin.example.com',
'default_ttl' => $settings['default_ttl'] ?? 3600,
]);
$count++;
} catch (Exception $e) {
// Continue with other zones
}
}
Notification::make()->title(__(':count zones synced', ['count' => $count]))->success()->send();
}),
$this->applyTemplateAction()
->visible(fn () => $this->selectedDomainId !== null),
$this->addRecordAction()
->visible(fn () => $this->selectedDomainId !== null),
];
}
public function addRecordAction(): Action
{
return Action::make('addRecord')
->label(__('Add Record'))
->icon('heroicon-o-plus')
->color('primary')
->modalHeading(__('Add DNS Record'))
->modalDescription(__('The record will be queued until you click "Save Changes".'))
->modalIcon('heroicon-o-plus-circle')
->modalIconColor('primary')
->modalSubmitActionLabel(__('Queue Record'))
->modalWidth('lg')
->form([
Select::make('type')
->label(__('Record Type'))
->options([
'A' => __('A - IPv4 Address'),
'AAAA' => __('AAAA - IPv6 Address'),
'CNAME' => __('CNAME - Canonical Name'),
'MX' => __('MX - Mail Exchange'),
'TXT' => __('TXT - Text Record'),
'NS' => __('NS - Nameserver'),
'SRV' => __('SRV - Service'),
'CAA' => __('CAA - Certificate Authority'),
])
->required()
->reactive(),
TextInput::make('name')
->label(__('Name'))
->placeholder(__('@ for root, or subdomain'))
->required(),
TextInput::make('content')
->label(__('Content'))
->required(),
TextInput::make('ttl')
->label(__('TTL'))
->numeric()
->default(3600),
TextInput::make('priority')
->label(__('Priority'))
->numeric()
->visible(fn ($get) => in_array($get('type'), ['MX', 'SRV'])),
])
->action(function (array $data) {
// Queue the add
$this->queuePendingAdd([
'type' => $data['type'],
'name' => $data['name'],
'content' => $data['content'],
'ttl' => $data['ttl'] ?? 3600,
'priority' => $data['priority'] ?? null,
]);
Notification::make()
->title(__('Record queued'))
->body(__('Click "Save Changes" to apply.'))
->info()
->send();
});
}
public function applyTemplateAction(): Action
{
return Action::make('applyTemplate')
->label(__('Apply Template'))
->icon('heroicon-o-document-duplicate')
->color('gray')
->modalHeading(__('Apply Email Template'))
->modalDescription(__('This will apply the selected email DNS records immediately.'))
->modalIcon('heroicon-o-envelope')
->modalIconColor('warning')
->modalSubmitActionLabel(__('Apply Template'))
->modalWidth('lg')
->form([
Select::make('template')
->label(__('Email Provider'))
->options([
'google' => __('Google Workspace (Gmail)'),
'microsoft' => __('Microsoft 365 (Outlook)'),
'zoho' => __('Zoho Mail'),
'protonmail' => __('ProtonMail'),
'fastmail' => __('Fastmail'),
'local' => __('Local Mail Server (This Server)'),
'none' => __('Remove All Email Records'),
])
->required()
->reactive(),
TextInput::make('verification_code')
->label(__('Domain Verification Code (optional)'))
->placeholder(__('e.g., google-site-verification=xxx'))
->visible(fn ($get) => $get('template') && $get('template') !== 'none'),
])
->action(function (array $data) {
$domain = Domain::find($this->selectedDomainId);
if (! $domain) {
Notification::make()->title(__('Domain not found'))->danger()->send();
return;
}
$domainName = $domain->domain;
$template = $data['template'];
$verificationCode = $data['verification_code'] ?? null;
// Queue deletion of existing MX and email-related records
$recordsToDelete = DnsRecord::where('domain_id', $this->selectedDomainId)
->where(function ($query) {
$query->where('type', 'MX')
->orWhere(function ($q) {
$q->where('type', 'A')->where('name', 'mail');
})
->orWhere(function ($q) {
$q->where('type', 'CNAME')->where('name', 'autodiscover');
})
->orWhere(function ($q) {
$q->where('type', 'TXT')
->where(function ($inner) {
$inner->where('content', 'like', '%spf%')
->orWhere('content', 'like', '%v=spf1%')
->orWhere('content', 'like', '%google-site-verification%')
->orWhere('content', 'like', '%MS=%')
->orWhere('content', 'like', '%zoho-verification%')
->orWhere('content', 'like', '%protonmail-verification%')
->orWhere('name', 'like', '%_domainkey%');
});
});
})
->pluck('id')
->toArray();
foreach ($recordsToDelete as $id) {
if (! in_array($id, $this->pendingDeletes)) {
$this->pendingDeletes[] = $id;
}
unset($this->pendingEdits[$id]);
}
// Queue new records
if ($template !== 'none') {
$records = $this->getTemplateRecords($template, $domainName, $verificationCode);
foreach ($records as $record) {
$this->queuePendingAdd($record);
}
}
if (! $this->hasPendingChanges()) {
Notification::make()
->title(__('No changes to apply'))
->warning()
->send();
return;
}
$this->saveChanges(false);
$message = $template === 'none'
? __('Email records removed.')
: __('Email records for :provider have been applied.', ['provider' => ucfirst($template)]);
Notification::make()
->title(__('Template applied'))
->body($message.' '.__('Changes may take up to 48 hours to propagate.'))
->success()
->send();
});
}
protected function getTemplateRecords(string $template, string $domain, ?string $verificationCode): array
{
$settings = DnsSetting::getAll();
$serverIp = $settings['default_ip'] ?? trim(shell_exec("hostname -I | awk '{print $1}'") ?? '') ?: '127.0.0.1';
$records = match ($template) {
'google' => [
['name' => '@', 'type' => 'MX', 'content' => 'aspmx.l.google.com', 'ttl' => 3600, 'priority' => 1],
['name' => '@', 'type' => 'MX', 'content' => 'alt1.aspmx.l.google.com', 'ttl' => 3600, 'priority' => 5],
['name' => '@', 'type' => 'MX', 'content' => 'alt2.aspmx.l.google.com', 'ttl' => 3600, 'priority' => 5],
['name' => '@', 'type' => 'MX', 'content' => 'alt3.aspmx.l.google.com', 'ttl' => 3600, 'priority' => 10],
['name' => '@', 'type' => 'MX', 'content' => 'alt4.aspmx.l.google.com', 'ttl' => 3600, 'priority' => 10],
['name' => '@', 'type' => 'TXT', 'content' => 'v=spf1 include:_spf.google.com ~all', 'ttl' => 3600, 'priority' => null],
],
'microsoft' => [
['name' => '@', 'type' => 'MX', 'content' => str_replace('.', '-', $domain).'.mail.protection.outlook.com', 'ttl' => 3600, 'priority' => 0],
['name' => '@', 'type' => 'TXT', 'content' => 'v=spf1 include:spf.protection.outlook.com ~all', 'ttl' => 3600, 'priority' => null],
['name' => 'autodiscover', 'type' => 'CNAME', 'content' => 'autodiscover.outlook.com', 'ttl' => 3600, 'priority' => null],
],
'zoho' => [
['name' => '@', 'type' => 'MX', 'content' => 'mx.zoho.com', 'ttl' => 3600, 'priority' => 10],
['name' => '@', 'type' => 'MX', 'content' => 'mx2.zoho.com', 'ttl' => 3600, 'priority' => 20],
['name' => '@', 'type' => 'MX', 'content' => 'mx3.zoho.com', 'ttl' => 3600, 'priority' => 50],
['name' => '@', 'type' => 'TXT', 'content' => 'v=spf1 include:zoho.com ~all', 'ttl' => 3600, 'priority' => null],
],
'protonmail' => [
['name' => '@', 'type' => 'MX', 'content' => 'mail.protonmail.ch', 'ttl' => 3600, 'priority' => 10],
['name' => '@', 'type' => 'MX', 'content' => 'mailsec.protonmail.ch', 'ttl' => 3600, 'priority' => 20],
['name' => '@', 'type' => 'TXT', 'content' => 'v=spf1 include:_spf.protonmail.ch mx ~all', 'ttl' => 3600, 'priority' => null],
],
'fastmail' => [
['name' => '@', 'type' => 'MX', 'content' => 'in1-smtp.messagingengine.com', 'ttl' => 3600, 'priority' => 10],
['name' => '@', 'type' => 'MX', 'content' => 'in2-smtp.messagingengine.com', 'ttl' => 3600, 'priority' => 20],
['name' => '@', 'type' => 'TXT', 'content' => 'v=spf1 include:spf.messagingengine.com ~all', 'ttl' => 3600, 'priority' => null],
],
'local' => [
['name' => '@', 'type' => 'MX', 'content' => 'mail.'.$domain, 'ttl' => 3600, 'priority' => 10],
['name' => 'mail', 'type' => 'A', 'content' => $serverIp, 'ttl' => 3600, 'priority' => null],
['name' => '@', 'type' => 'TXT', 'content' => 'v=spf1 mx a ~all', 'ttl' => 3600, 'priority' => null],
],
default => [],
};
if ($verificationCode) {
$records[] = ['name' => '@', 'type' => 'TXT', 'content' => $verificationCode, 'ttl' => 3600, 'priority' => null];
}
return $records;
}
public function rebuildCurrentZone(): void
{
if (! $this->selectedDomainId) {
return;
}
$domain = Domain::find($this->selectedDomainId);
if (! $domain) {
return;
}
try {
$this->syncZoneFile($domain->domain);
Notification::make()->title(__('Zone rebuilt for :domain', ['domain' => $domain->domain]))->success()->send();
} catch (Exception $e) {
Notification::make()->title(__('Failed'))->body($e->getMessage())->danger()->send();
}
}
public function deleteCurrentZone(): void
{
if (! $this->selectedDomainId) {
return;
}
$domain = Domain::find($this->selectedDomainId);
if (! $domain) {
return;
}
try {
$this->getAgent()->send('dns.delete_zone', ['domain' => $domain->domain]);
DnsRecord::where('domain_id', $this->selectedDomainId)->delete();
Notification::make()->title(__('Zone deleted for :domain', ['domain' => $domain->domain]))->success()->send();
$this->selectedDomainId = null;
$this->pendingEdits = [];
$this->pendingDeletes = [];
$this->pendingAdds = [];
} catch (Exception $e) {
Notification::make()->title(__('Failed'))->body($e->getMessage())->danger()->send();
}
}
protected function syncZoneFile(string $domain): void
{
$records = DnsRecord::whereHas('domain', fn ($q) => $q->where('domain', $domain))->get()->toArray();
$settings = DnsSetting::getAll();
$this->getAgent()->send('dns.sync_zone', [
'domain' => $domain,
'records' => $records,
'ns1' => $settings['ns1'] ?? 'ns1.example.com',
'ns2' => $settings['ns2'] ?? 'ns2.example.com',
'admin_email' => $settings['admin_email'] ?? 'admin.example.com',
'default_ttl' => $settings['default_ttl'] ?? 3600,
]);
}
}