diff --git a/resources/views/filament/pages/generate-dmarc.blade.php b/resources/views/filament/pages/generate-dmarc.blade.php new file mode 100644 index 00000000..e23f986f --- /dev/null +++ b/resources/views/filament/pages/generate-dmarc.blade.php @@ -0,0 +1,25 @@ + +
+ {{ $this->form }} +
+ + + + Generated DMARC Record + + +
+
{{ $generatedRecord }}
+ + +
+
+
+
+
\ No newline at end of file diff --git a/src/Filament/OLD/Pages/Dmarc/GenerateDmarc.php b/src/Filament/OLD/Pages/Dmarc/GenerateDmarc.php deleted file mode 100644 index 123b46c8..00000000 --- a/src/Filament/OLD/Pages/Dmarc/GenerateDmarc.php +++ /dev/null @@ -1,316 +0,0 @@ -domain = Domain::find($record); - $this->existingDmarc = DmarcCheck::where('domain', $this->domain->domain)->first(); - } - - parent::mount(); - - if ($this->existingDmarc) { - $defaults = [ - 'policy' => $this->existingDmarc->policy ?? 'none', - 'subdomain_policy' => $this->existingDmarc->subdomain_policy ?? null, - 'adkim' => $this->existingDmarc->adkim ?? 'relaxed', - 'aspf' => $this->existingDmarc->aspf ?? 'relaxed', - 'reporting' => json_decode($this->existingDmarc->reporting ?? '["all"]', true), - 'percentage' => $this->existingDmarc->percentage ?? 100, - 't' => $this->existingDmarc->t ?? 'n', - ]; - - if ($this->existingDmarc->rua) { - $rua = json_decode($this->existingDmarc->rua, true); - if (is_array($rua)) { - $defaults['rua'] = collect($rua)->map(function ($item) { - return ['email' => str_replace('mailto:', '', $item)]; - })->toArray(); - } - } - - if ($this->existingDmarc->ruf) { - $ruf = json_decode($this->existingDmarc->ruf, true); - if (is_array($ruf)) { - $defaults['ruf'] = collect($ruf)->map(function ($item) { - return ['email' => str_replace('mailto:', '', $item)]; - })->toArray(); - } - } - - $this->form->fill($defaults); - } - } - - public function form(Form $form): Form - { - $service = app(DmarcCheckService::class); - $addresses = $service->getDefaultReportingAddresses($this->domain->domain ?? config('app.domain')); - - return $form - ->schema([ - Select::make('policy') - ->label('Policy') - ->options([ - 'none' => 'None (Monitor only)', - 'quarantine' => 'Quarantine (Mark as spam)', - 'reject' => 'Reject (Block delivery)', - ]) - ->required() - ->default('none') - ->live(), - - Select::make('subdomain_policy') - ->label('Subdomain Policy') - ->options([ - '' => 'Inherit from main policy', - 'none' => 'None (Monitor only)', - 'quarantine' => 'Quarantine (Mark as spam)', - 'reject' => 'Reject (Block delivery)', - ]) - ->nullable(), - - Repeater::make('rua') - ->label('Aggregate Reports (RUA)') - ->schema([ - TextInput::make('email') - ->label('Email') - ->email() - ->prefix('mailto:') - ->required(), - ]) - ->default(function () use ($addresses) { - if ($this->existingDmarc && $this->existingDmarc->rua) { - $rua = json_decode($this->existingDmarc->rua, true); - if (is_array($rua)) { - return collect($rua)->map(function ($item) { - return ['email' => str_replace('mailto:', '', $item)]; - })->toArray(); - } - } - return [['email' => str_replace('mailto:', '', $addresses['rua'])]]; - }) - ->addable() - ->deletable() - ->reorderable() - ->columnSpanFull(), - - Repeater::make('ruf') - ->label('Forensic Reports (RUF)') - ->schema([ - TextInput::make('email') - ->label('Email') - ->email() - ->prefix('mailto:') - ->required(), - ]) - ->default(function () use ($addresses) { - if ($this->existingDmarc && $this->existingDmarc->ruf) { - $ruf = json_decode($this->existingDmarc->ruf, true); - if (is_array($ruf)) { - return collect($ruf)->map(function ($item) { - return ['email' => str_replace('mailto:', '', $item)]; - })->toArray(); - } - } - return [['email' => str_replace('mailto:', '', $addresses['ruf'])]]; - }) - ->addable() - ->deletable() - ->reorderable() - ->columnSpanFull(), - - Select::make('adkim') - ->label('DKIM Alignment') - ->options([ - 'relaxed' => 'Relaxed (subdomains allowed)', - 'strict' => 'Strict (exact match)', - ]) - ->default('relaxed'), - - Select::make('aspf') - ->label('SPF Alignment') - ->options([ - 'relaxed' => 'Relaxed (subdomains allowed)', - 'strict' => 'Strict (exact match)', - ]) - ->default('relaxed'), - - Select::make('reporting') - ->label('Failure Reporting (FO)') - ->multiple() - ->options([ - 'all' => 'All failures', - 'any' => 'Any failure', - 'dkim' => 'DKIM failures only', - 'spf' => 'SPF failures only', - ]) - ->default(['all']), - - Slider::make('percentage') - ->label('Enforcement Percentage') - ->min(0) - ->max(100) - ->default(100), - - Select::make('t') - ->label('Testing Mode') - ->options([ - 'n' => 'Off (Apply policy)', - 'y' => 'On (Don\'t apply policy)', - ]) - ->default('n'), - - Placeholder::make('generated_record_preview') - ->label('Preview') - ->content(function ($get) { - try { - $settings = $this->prepareSettings($get); - $dmarcRecord = new DmarcRecord( - policy: DmarcPolicy::tryFrom($settings['policy']), - subdomainPolicy: $settings['subdomain_policy'] ? DmarcPolicy::tryFrom($settings['subdomain_policy']) : null, - rua: $settings['rua'], - ruf: $settings['ruf'], - adkim: DmarcAlignment::tryFrom($settings['adkim']), - aspf: DmarcAlignment::tryFrom($settings['aspf']), - reporting: array_map(fn($r) => DmarcReporting::tryFrom($r), $settings['reporting']), - percentage: (int)$settings['percentage'], - t: $settings['t'], - ); - - if ($this->domain) { - return "Name: _dmarc.{$this->domain->domain}\n" . - "Type: TXT\n" . - "Value: v=DMARC1; " . $dmarcRecord->toDnsRecord(); - } - return "Please select a domain to preview"; - } catch (\Exception $e) { - return "Error generating preview: " . $e->getMessage(); - } - }) - ->extraAttributes(['class' => 'font-mono text-sm bg-gray-50 dark:bg-gray-800 p-4 rounded']) - ->columnSpanFull(), - ]); - } - - protected function prepareSettings(array $data): array - { - return [ - 'policy' => $data['policy'] ?? 'none', - 'subdomain_policy' => $data['subdomain_policy'] ?? null, - 'rua' => collect($data['rua'] ?? []) - ->filter(fn($item) => !empty($item['email'])) - ->map(fn($item) => 'mailto:' . $item['email']) - ->values() - ->toArray(), - 'ruf' => collect($data['ruf'] ?? []) - ->filter(fn($item) => !empty($item['email'])) - ->map(fn($item) => 'mailto:' . $item['email']) - ->values() - ->toArray(), - 'adkim' => $data['adkim'] ?? 'relaxed', - 'aspf' => $data['aspf'] ?? 'relaxed', - 'reporting' => $data['reporting'] ?? ['all'], - 'percentage' => (int)($data['percentage'] ?? 100), - 't' => $data['t'] ?? 'n', - ]; - } - - protected function handleRecordCreation(array $data): \Illuminate\Database\Eloquent\Model - { - $settings = $this->prepareSettings($data); - - $dmarcRecord = new DmarcRecord( - policy: DmarcPolicy::tryFrom($settings['policy']), - subdomainPolicy: $settings['subdomain_policy'] ? DmarcPolicy::tryFrom($settings['subdomain_policy']) : null, - rua: $settings['rua'], - ruf: $settings['ruf'], - adkim: DmarcAlignment::tryFrom($settings['adkim']), - aspf: DmarcAlignment::tryFrom($settings['aspf']), - reporting: array_map(fn($r) => DmarcReporting::tryFrom($r), $settings['reporting']), - percentage: (int)$settings['percentage'], - t: $settings['t'], - ); - - $dnsRecord = 'v=DMARC1; ' . $dmarcRecord->toDnsRecord(); - - return DmarcCheck::updateOrCreate( - ['domain' => $this->domain->domain], - [ - 'domain_id' => $this->domain->domain_id, - 'record' => $dnsRecord, - 'policy' => $settings['policy'], - 'subdomain_policy' => $settings['subdomain_policy'], - 'adkim' => $settings['adkim'], - 'aspf' => $settings['aspf'], - 'rua' => json_encode($settings['rua']), - 'ruf' => json_encode($settings['ruf']), - 'reporting' => json_encode($settings['reporting']), - 'percentage' => (int)$settings['percentage'], - 't' => $settings['t'], - 'valid' => true, - 'error_message' => null, - 'last_checked_at' => now(), - 'next_check_at' => now()->addHours(24), - ] - ); - } - - protected function getCreatedNotificationTitle(): ?string - { - return "DMARC record generated for {$this->domain->domain}"; - } - - protected function getRedirectUrl(): string - { - return DmarcResource::getUrl('view', ['record' => $this->domain->getKey()]); - } - - protected function getHeaderActions(): array - { - return [ - Action::make('back') - ->label('Back to View') - ->icon('heroicon-o-arrow-left') - ->url(DmarcResource::getUrl('view', ['record' => $this->domain->getKey()])), - - Action::make('back_to_list') - ->label('Back to List') - ->icon('heroicon-o-list-bullet') - ->url(DmarcResource::getUrl('index')), - ]; - } - - public function getTitle(): string - { - $action = $this->existingDmarc ? 'Update' : 'Generate'; - return "{$action} DMARC Record"; - } -} \ No newline at end of file diff --git a/src/Filament/OLD/Pages/Dmarc/ListDmarc.php b/src/Filament/OLD/Pages/Dmarc/ListDmarc.php deleted file mode 100644 index b28446c6..00000000 --- a/src/Filament/OLD/Pages/Dmarc/ListDmarc.php +++ /dev/null @@ -1,192 +0,0 @@ -query($this->getTableQuery()) - ->columns([ - TextColumn::make('domain') - ->label('Domain') - ->searchable() - ->sortable() - ->size('lg') - ->weight('bold'), - - BadgeColumn::make('dmarc_status') - ->label('DMARC Status') - ->getStateUsing(function ($record) { - $dmarc = DmarcCheck::where('domain', $record->domain)->first(); - if (!$dmarc) { - return 'Not Checked'; - } - if (!$dmarc->valid) { - return 'No Record'; - } - return $dmarc->getPolicyLabel(); - }) - ->colors([ - 'success' => 'Monitor', - 'warning' => 'Quarantine', - 'danger' => 'Reject', - 'gray' => ['Not Checked', 'No Record'], - ]), - - BadgeColumn::make('dmarc_policy') - ->label('Policy') - ->getStateUsing(function ($record) { - $dmarc = DmarcCheck::where('domain', $record->domain)->first(); - return $dmarc?->policy ?? '-'; - }) - ->colors([ - 'success' => 'none', - 'warning' => 'quarantine', - 'danger' => 'reject', - 'gray' => '-', - ]), - - TextColumn::make('dmarc_last_checked') - ->label('Last Checked') - ->getStateUsing(function ($record) { - $dmarc = DmarcCheck::where('domain', $record->domain)->first(); - return $dmarc?->last_checked_at; - }) - ->since() - ->toggleable(), - - TextColumn::make('exim_users_count') - ->label('Accounts') - ->counts('eximUsers') - ->badge() - ->color('info'), - ]) - ->filters([ - \Filament\Tables\Filters\SelectFilter::make('dmarc_status') - ->label('DMARC Status') - ->options([ - 'has_record' => 'Has DMARC Record', - 'no_record' => 'No DMARC Record', - 'not_checked' => 'Not Checked', - ]) - ->query(function ($query, $data) { - if (empty($data['value'])) { - return $query; - } - - return match($data['value']) { - 'has_record' => $query->whereIn('domain', - DmarcCheck::where('valid', true)->pluck('domain')->toArray() - ), - 'no_record' => $query->whereIn('domain', - DmarcCheck::where('valid', false)->pluck('domain')->toArray() - ), - 'not_checked' => $query->whereNotIn('domain', - DmarcCheck::pluck('domain')->toArray() - ), - default => $query, - }; - }), - - \Filament\Tables\Filters\SelectFilter::make('policy') - ->label('Policy') - ->options([ - 'none' => 'Monitor', - 'quarantine' => 'Quarantine', - 'reject' => 'Reject', - ]) - ->query(function ($query, $data) { - if (empty($data['value'])) { - return $query; - } - $domains = DmarcCheck::where('policy', $data['value']) - ->where('valid', true) - ->pluck('domain') - ->toArray(); - return $query->whereIn('domain', $domains); - }), - ]) - ->actions([ - Action::make('view') - ->label('View DMARC') - ->icon('heroicon-o-eye') - ->color('info') - ->url(fn ($record): string => DmarcResource::getUrl('view', ['record' => $record])), - - Action::make('generate') - ->label('Generate Record') - ->icon('heroicon-o-document-plus') - ->color('success') - ->url(fn ($record): string => DmarcResource::getUrl('generate', ['record' => $record])), - - Action::make('check_dns') - ->label('Check DNS') - ->icon('heroicon-o-arrow-path') - ->color('warning') - ->action(function ($record) { - $service = app(DmarcCheckService::class); - $result = $service->checkDomain($record->domain, $record->domain_id); - - if ($result && $result->valid) { - Notification::make() - ->success() - ->title('DMARC Record Found') - ->body("Policy: {$result->getPolicyLabel()}") - ->send(); - } else { - Notification::make() - ->warning() - ->title('No DMARC Record Found') - ->body($result?->error_message ?? 'No DMARC record found in DNS') - ->send(); - } - }), - ]) - ->bulkActions([ - BulkActionGroup::make([ - Action::make('check_all_dmarc') - ->label('Check DMARC for Selected') - ->icon('heroicon-o-arrow-path') - ->action(function ($records) { - $service = app(DmarcCheckService::class); - $valid = 0; - - foreach ($records as $record) { - $result = $service->checkDomain($record->domain, $record->domain_id); - if ($result && $result->valid) { - $valid++; - } - } - - Notification::make() - ->success() - ->title('DMARC Check Complete') - ->body("Checked {$records->count()} domains, {$valid} have valid DMARC records") - ->send(); - }), - ]), - ]); - } -} diff --git a/src/Filament/OLD/Pages/Dmarc/ViewDmarc.php b/src/Filament/OLD/Pages/Dmarc/ViewDmarc.php deleted file mode 100644 index 8d0e17b3..00000000 --- a/src/Filament/OLD/Pages/Dmarc/ViewDmarc.php +++ /dev/null @@ -1,76 +0,0 @@ -dmarc = DmarcCheck::where('domain', $this->record->domain)->first(); - - // If no cache or invalid, check now - if (!$this->dmarc || !$this->dmarc->valid) { - $service = app(DmarcCheckService::class); - $this->dmarc = $service->checkDomain($this->record->domain, $this->record->domain_id); - } - - return $data; - } - - protected function getHeaderActions(): array - { - return [ - Action::make('check_dns') - ->label('Check DNS Again') - ->icon('heroicon-o-arrow-path') - ->color('warning') - ->action(function () { - $service = app(DmarcCheckService::class); - $this->dmarc = $service->checkDomain($this->record->domain, $this->record->domain_id); - - Notification::make() - ->success() - ->title('DMARC Record Updated') - ->body($this->dmarc?->valid ? 'Record found and updated' : 'No valid record found') - ->send(); - - $this->refreshFormData(); - }), - - Action::make('generate') - ->label('Generate New Record') - ->icon('heroicon-o-document-plus') - ->color('success') - ->url(DmarcResource::getUrl('generate', ['record' => $this->record])), - - Action::make('back') - ->label('Back to List') - ->icon('heroicon-o-arrow-left') - ->url(DmarcResource::getUrl('index')), - ]; - } - - protected function getViewData(): array - { - return [ - 'dmarc' => $this->dmarc, - ]; - } - - public function getTitle(): string - { - return "DMARC Record for {$this->record->domain}"; - } -} diff --git a/src/Filament/OLD/Providers/DnsToolsPanelProvider.php b/src/Filament/OLD/Providers/DnsToolsPanelProvider.php deleted file mode 100644 index 095bbc72..00000000 --- a/src/Filament/OLD/Providers/DnsToolsPanelProvider.php +++ /dev/null @@ -1,27 +0,0 @@ -id('dns-tools') - ->path('dns-tools') - ->colors([ - 'primary' => Color::Blue, - ]) - ->resources([ - DmarcResource::class, - ]) - ->navigationGroups([ - 'DNS Tools', - ]); - } -} diff --git a/src/Filament/OLD/Resources/DmarcResource.php b/src/Filament/OLD/Resources/DmarcResource.php deleted file mode 100644 index 288580af..00000000 --- a/src/Filament/OLD/Resources/DmarcResource.php +++ /dev/null @@ -1,46 +0,0 @@ - ListDmarc::route('/'), - 'view' => ViewDmarc::route('/{record}/view'), - 'generate' => GenerateDmarc::route('/{record}/generate'), - ]; - } -} \ No newline at end of file diff --git a/src/Filament/Resources/Dmarc/Modals/GenerateDmarcForm.php b/src/Filament/Resources/Dmarc/Modals/GenerateDmarcForm.php index 14dbb3c4..3dbeb42a 100644 --- a/src/Filament/Resources/Dmarc/Modals/GenerateDmarcForm.php +++ b/src/Filament/Resources/Dmarc/Modals/GenerateDmarcForm.php @@ -9,32 +9,75 @@ use Filament\Forms\Components\Select; use Filament\Forms\Components\Textarea; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Hidden; +use VEximweb\Plugin\DnsTools\Models\SystemDomains as Domain; class GenerateDmarcForm { - protected static ?string $domain = null; + protected static ?Domain $domainRecord = null; + + public static function extend(callable $components, callable $onSave): void + { + $existing = app()->bound('dmarcform.extenders') + ? app('dmarcform.extenders') + : ['components' => [], 'hooks' => []]; + + $existing['components'][] = $components; + $existing['hooks'][] = $onSave; + + app()->instance('dmarcform.extenders', $existing); + } + + public static function runSaveHooks(mixed $record, array $data): void + { + if (! app()->bound('dmarcform.extenders')) { + return; + } + + foreach (app('dmarcform.extenders')['hooks'] as $hook) { + $hook($record, $data); + } + } /** - * Set the domain for the form. + * Set the domain record for the form. */ - public static function setDomain(string $domain): void + public static function setDomainRecord(Domain $domain): void { - static::$domain = $domain; + static::$domainRecord = $domain; } /** - * Get the current domain. + * Get the current domain record. */ - protected static function getDomain(): string + protected static function getDomainRecord(): ?Domain { - return static::$domain ?? 'example.com'; + return static::$domainRecord; } /** - * The Filament form schema. + * The Filament form schema - now accepts Domain record instead of string */ - public static function schema(string $domain): array + public static function schema(Domain $domain): array { + // Set the domain record for extensions + static::setDomainRecord($domain); + + $extra = []; + + if (app()->bound('dmarcform.extenders')) { + foreach (app('dmarcform.extenders')['components'] as $extender) { + // Pass the domain record directly to the extender + if (is_callable($extender)) { + $result = $extender($domain); + if (is_array($result)) { + $extra = array_merge($extra, $result); + } + } else { + $extra = array_merge($extra, $extender()); + } + } + } + return [ Section::make('Policy') ->columns(2) @@ -104,29 +147,31 @@ class GenerateDmarcForm 'all' => 'All failures', ]), - TextInput::make('dmarc_rua_localpart') - ->label('Aggregate Reports Email') - ->helperText('Email address for aggregate reports (auto-appended with @' . $domain . ')') - ->default('dmarc-reports') - ->prefix('📊') - ->suffix('@' . $domain), + TextInput::make('dmarc_rua_localpart') + ->label('Aggregate Reports Email') + ->helperText('Email address for aggregate reports (auto-appended with @' . $domain->domain . ')') + ->default('dmarc-reports') + ->prefix('📊') + ->suffix('@' . $domain->domain), - TextInput::make('dmarc_ruf_localpart') - ->label('Forensic Reports Email') - ->helperText('Email address for forensic reports (auto-appended with @' . $domain . ')') - ->default('dmarc-forensic') - ->prefix('🔍') - ->suffix('@' . $domain), + TextInput::make('dmarc_ruf_localpart') + ->label('Forensic Reports Email') + ->helperText('Email address for forensic reports (auto-appended with @' . $domain->domain . ')') + ->default('dmarc-forensic') + ->prefix('🔍') + ->suffix('@' . $domain->domain), ]), + + ...$extra, ]; } /** - * Populate the form from the repository. + * Populate the form from the repository - now accepts domain record */ - public static function values(SettingRepositoryInterface $settings): array + public static function values(SettingRepositoryInterface $settings, Domain $domain): array { - // Get settings for this domain (you might need to adjust this) + // Get settings for this domain $rua_localpart = $settings->get('dmarc_rua_localpart', 'dmarc-reports'); $ruf_localpart = $settings->get('dmarc_ruf_localpart', 'dmarc-forensic'); @@ -147,31 +192,31 @@ class GenerateDmarcForm } /** - * Save the form back to the repository. + * Save the form back to the repository - now accepts domain record */ -public static function save( - SettingRepositoryInterface $settings, - array $data, - string $domain, -): void { - foreach ([ - 'dmarc_policy', - 'dmarc_subdomain_policy', - 'dmarc_np', - 'dmarc_psd', - 'dmarc_t', - 'dmarc_percentage', - 'dmarc_report_interval', - 'dmarc_adkim', - 'dmarc_aspf', - 'dmarc_rua_localpart', - 'dmarc_ruf_localpart', - ] as $key) { - $settings->set($key, $data[$key]); - } + public static function save( + SettingRepositoryInterface $settings, + array $data, + Domain $domain, + ): void { + foreach ([ + 'dmarc_policy', + 'dmarc_subdomain_policy', + 'dmarc_np', + 'dmarc_psd', + 'dmarc_t', + 'dmarc_percentage', + 'dmarc_report_interval', + 'dmarc_adkim', + 'dmarc_aspf', + 'dmarc_rua_localpart', + 'dmarc_ruf_localpart', + ] as $key) { + $settings->set($key, $data[$key]); + } - $settings->set('dmarc_rua', [$data['dmarc_rua_localpart'] . '@' . $domain]); - $settings->set('dmarc_ruf', [$data['dmarc_ruf_localpart'] . '@' . $domain]); - $settings->set('dmarc_reporting', $data['dmarc_reporting'] ?? []); -} + $settings->set('dmarc_rua', [$data['dmarc_rua_localpart'] . '@' . $domain->domain]); + $settings->set('dmarc_ruf', [$data['dmarc_ruf_localpart'] . '@' . $domain->domain]); + $settings->set('dmarc_reporting', $data['dmarc_reporting'] ?? []); + } } \ No newline at end of file diff --git a/src/Filament/Resources/Dmarc/Pages/GenerateDmarcPage.php b/src/Filament/Resources/Dmarc/Pages/GenerateDmarcPage.php new file mode 100644 index 00000000..c0ee359a --- /dev/null +++ b/src/Filament/Resources/Dmarc/Pages/GenerateDmarcPage.php @@ -0,0 +1,259 @@ + 'refreshNotifications', + ]; + } + + public function mount(Domain $domain): void + { + $this->domain = $domain; + $domainId = $this->domain->id; + + // Load existing settings if any + $settings = app(SettingRepositoryInterface::class); + $this->form->fill($this->getValues($settings)); + } + + public function form(Schema $schema): Schema + { + return $schema + ->schema($this->getFormSchema()) + ->statePath('data'); + } + + /** + * Get the form schema as an array of components + */ + protected function getFormSchema(): array + { + $extra = []; + + if (app()->bound('dmarcform.extenders')) { + foreach (app('dmarcform.extenders')['components'] as $extender) { + if (is_callable($extender)) { + $result = $extender($this->domain); + if (is_array($result)) { + $extra = array_merge($extra, $result); + } + } else { + $extra = array_merge($extra, $extender()); + } + } + } + + return [ + Section::make('Policy') + ->columns(2) + ->schema([ + Hidden::make('domain_id') + ->default($this->domain->id), + Select::make('dmarc_policy') + ->label('Policy') + ->options([ + 'none' => 'None', + 'quarantine' => 'Quarantine', + 'reject' => 'Reject', + ]) + ->required(), + + Select::make('dmarc_subdomain_policy') + ->label('Subdomain Policy') + ->options([ + '' => 'Inherit Policy', + 'none' => 'None', + 'quarantine' => 'Quarantine', + 'reject' => 'Reject', + ]), + + Hidden::make('dmarc_np') + ->default(''), + + Hidden::make('dmarc_psd') + ->default('n'), + + Hidden::make('dmarc_t') + ->default('n'), + + Hidden::make('dmarc_percentage') + ->default(100), + + Hidden::make('dmarc_report_interval') + ->default(86400), + ]), + + Section::make('Alignment') + ->columns(2) + ->schema([ + Select::make('dmarc_adkim') + ->label('DKIM Alignment') + ->options([ + 'relaxed' => 'Relaxed', + 'strict' => 'Strict', + ]), + + Select::make('dmarc_aspf') + ->label('SPF Alignment') + ->options([ + 'relaxed' => 'Relaxed', + 'strict' => 'Strict', + ]), + ]), + + Section::make('Reporting') + ->schema([ + CheckboxList::make('dmarc_reporting') + ->label('Forensic Report Triggers') + ->helperText('"Any" and "All" are mutually exclusive; DKIM/SPF can be combined.') + ->options([ + 'any' => 'Any mechanism fails (DKIM or SPF, most common) ', + 'all' => 'All mechanisms fail (Not recommended)', + 'dkim' => 'DKIM fails', + 'spf' => 'SPF fails', + ]) + ->rule(fn () => function (string $attribute, $value, \Closure $fail) { + if (in_array('all', $value ?? []) && in_array('any', $value ?? [])) { + $fail("'Any' and 'All' cannot both be selected."); + } + }), + + TextInput::make('dmarc_rua_localpart') + ->label('Aggregate Reports Email') + ->helperText('Email address for aggregate reports (auto-appended with @' . $this->domain->domain . ')') + ->default('dmarc-reports') + ->suffix('@' . $this->domain->domain), + + TextInput::make('dmarc_ruf_localpart') + ->label('Forensic Reports Email') + ->helperText('Email address for forensic reports (auto-appended with @' . $this->domain->domain . ')') + ->default('dmarc-forensic') + ->suffix('@' . $this->domain->domain), + ]), + + ...$extra, + + Actions::make([ + Action::make('generate') + ->label('Generate DMARC Record') + ->submit('generateDMARC'), + ]), + ]; + } + + protected function getValues(SettingRepositoryInterface $settings): array + { + $rua_localpart = $settings->get('dmarc_rua_localpart', 'dmarc-reports'); + $ruf_localpart = $settings->get('dmarc_ruf_localpart', 'dmarc-forensic'); + + return [ + 'domain_id' => $this->domain->domain_id, + 'dmarc_policy' => $settings->get('dmarc_policy'), + 'dmarc_subdomain_policy' => $settings->get('dmarc_subdomain_policy'), + 'dmarc_np' => $settings->get('dmarc_np', ''), + 'dmarc_psd' => $settings->get('dmarc_psd', 'n'), + 'dmarc_t' => $settings->get('dmarc_t', 'n'), + 'dmarc_percentage' => $settings->get('dmarc_percentage', 100), + 'dmarc_report_interval' => $settings->get('dmarc_report_interval', 86400), + 'dmarc_adkim' => $settings->get('dmarc_adkim'), + 'dmarc_aspf' => $settings->get('dmarc_aspf'), + 'dmarc_reporting' => $settings->get('dmarc_reporting', []), + 'dmarc_rua_localpart' => $rua_localpart, + 'dmarc_ruf_localpart' => $ruf_localpart, + ]; + } + + public function generateDMARC(DmarcRecordService $dmarc): void + { + $data = $this->form->getState(); + \Log::debug('Form data:'. json_encode($data)); + + $this->generatedRecord = $dmarc->generate($this->domain, $data); + + $domainName = $this->domain->domain ?? $this->domain['domain'] ?? (string) $this->domain; + + \Log::debug('DMARC Event data:', [ + 'zone' => $domainName, + 'name' => '_dmarc', + 'content' => $this->generatedRecord + ]); + + event(new \App\Events\DmarcKeyGenerated( + zone: $domainName, + name: '_dmarc', + type: 'TXT', + content: $this->generatedRecord, + ttl: 3600, + operation: 'create' + )); + + $this->dispatch('open-modal', id: 'dmarc-record-modal'); + } + + + // Keep the static extend method for backward compatibility + public static function extend(callable $components, callable $onSave): void + { + $existing = app()->bound('dmarcform.extenders') + ? app('dmarcform.extenders') + : ['components' => [], 'hooks' => []]; + + $existing['components'][] = $components; + $existing['hooks'][] = $onSave; + + app()->instance('dmarcform.extenders', $existing); + } +} \ No newline at end of file diff --git a/src/Filament/Resources/DnsToolsResource.php b/src/Filament/Resources/DnsToolsResource.php index 1f6f23f5..cbd94850 100644 --- a/src/Filament/Resources/DnsToolsResource.php +++ b/src/Filament/Resources/DnsToolsResource.php @@ -14,18 +14,19 @@ use Filament\Schemas\Schema; use Filament\Support\Icons\Heroicon; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; +use VEximweb\Plugin\DnsTools\Filament\Resources\Dmarc\Pages\GenerateDmarcPage; class DnsToolsResource extends Resource { protected static ?string $model = Domain::class; - protected static ?string $slug = 'dnstools/domains'; + protected static ?string $slug = 'dnstools'; - protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack; + protected static string|BackedEnum|null $navigationIcon = Heroicon::WrenchScrewdriver; protected static ?string $recordTitleAttribute = 'domain'; - protected static string|\UnitEnum|null $navigationGroup = 'DNS Tools'; + protected static string|\UnitEnum|null $navigationGroup = 'DNS'; protected static ?string $navigationLabel = 'DNS Tools'; @@ -81,10 +82,6 @@ class DnsToolsResource extends Resource return $schema; } - public static function form_old(Schema $schema): Schema - { - return DomainForm::configure($schema); - } public static function table(Table $table): Table { @@ -168,6 +165,8 @@ class DnsToolsResource extends Resource { return [ 'index' => ListDomains::route('/'), + //'dmarc' => GenerateDmarcPage::route('/dmarc'), + 'generate' => GenerateDmarcPage::route('/{domain}/generate-dmarc'), //'create' => CreateDomain::route('/create'), //'edit' => EditDomain::route('/{record}/edit'), ]; diff --git a/src/Filament/Resources/Tables/DomainsTable.php b/src/Filament/Resources/Tables/DomainsTable.php index 6fdc3f35..ff1963bb 100644 --- a/src/Filament/Resources/Tables/DomainsTable.php +++ b/src/Filament/Resources/Tables/DomainsTable.php @@ -22,6 +22,8 @@ use Filament\Notifications\Notification; use Filament\Actions\ActionGroup; use VEximweb\Plugin\DnsTools\Filament\Resources\Dmarc\Modals\GenerateDmarcForm; use VEximweb\Core\Data\Repositories\Interfaces\SettingRepositoryInterface; +use VEximweb\Plugin\DnsTools\Filament\Resources\DnsToolsResource; + class DomainsTable { @@ -208,9 +210,25 @@ class DomainsTable //EditAction::make(), ActionGroup::make([ +Action::make('generateDmarc') + ->label('Generate DMARC') + ->icon('heroicon-o-document-text') + ->url(fn ($record) => DnsToolsResource::getUrl('generate', ['domain' => $record])), EditAction::make(), EditAction::make(), EditAction::make(), + +Action::make('dmarc') + ->fillForm(fn (SettingRepositoryInterface $settings, $record) => + GenerateDmarcForm::values($settings, $record) // Pass the record + ) + ->form(fn ($record) => + GenerateDmarcForm::schema($record) // Pass the full record, not just domain string + ) + ->action(fn (array $data, SettingRepositoryInterface $settings, $record) => + GenerateDmarcForm::save($settings, $data, $record) // Pass the record + ), +/* Action::make('dmarc') ->fillForm(fn (SettingRepositoryInterface $settings) => GenerateDmarcForm::values($settings) @@ -222,7 +240,7 @@ Action::make('dmarc') GenerateDmarcForm::save($settings, $data, $record->domain) ), - /* + */ /* Action::make('dmarc') ->fillForm(fn (SettingRepositoryInterface $settings) => GenerateDmarcForm::values($settings) diff --git a/src/Services/DmarcRecordService.php b/src/Services/DmarcRecordService.php new file mode 100644 index 00000000..c5d30c55 --- /dev/null +++ b/src/Services/DmarcRecordService.php @@ -0,0 +1,30 @@ +domain, + ruf: 'mailto:' . $data['dmarc_ruf_localpart'] . '@' . $domain->domain, + adkim: $data['dmarc_adkim'], + aspf: $data['dmarc_aspf'], + reporting: $data['dmarc_reporting'] ?? [], + np: $data['dmarc_np'] ?: null, + psd: $data['dmarc_psd'], + t: $data['dmarc_t'], + ); + } + + public function generate(Domain $domain, array $data): string + { + return (string) $this->build($domain, $data); + } +} \ No newline at end of file