agent === null) { $this->agent = new AgentClient; } return $this->agent; } public function getUsername(): string { return Auth::user()->username; } public function table(Table $table): Table { return $table ->query(Domain::query()->where('user_id', Auth::id())->with('sslCertificate')) ->columns([ TextColumn::make('domain') ->label(__('Domain')) ->icon(fn (Domain $record) => $record->sslCertificate?->isActive() ? 'heroicon-o-lock-closed' : 'heroicon-o-lock-open') ->iconColor(fn (Domain $record) => $record->sslCertificate?->status_color ?? 'gray') ->description(fn (Domain $record) => $record->sslCertificate?->issuer ?? __('No certificate')) ->searchable() ->sortable(), TextColumn::make('sslCertificate.type') ->label(__('Type')) ->badge() ->formatStateUsing(fn (?string $state) => match ($state) { 'lets_encrypt' => __("Let's Encrypt"), 'self_signed' => __('Self-Signed'), 'custom' => __('Custom'), default => __('No SSL'), }) ->color('gray'), TextColumn::make('sslCertificate.status') ->label(__('Status')) ->badge() ->getStateUsing(fn (Domain $record) => $record->sslCertificate?->status_label ?? __('No Certificate')) ->color(fn (Domain $record) => $record->sslCertificate?->status_color ?? 'gray'), TextColumn::make('sslCertificate.expires_at') ->label(__('Expires')) ->date('M d, Y') ->description(fn (Domain $record) => $record->sslCertificate?->days_until_expiry !== null ? ($record->sslCertificate->days_until_expiry < 0 ? __('Expired :days days ago', ['days' => abs($record->sslCertificate->days_until_expiry)]) : __(':days days left', ['days' => $record->sslCertificate->days_until_expiry])) : null) ->placeholder(__('-')) ->sortable(), IconColumn::make('sslCertificate.auto_renew') ->label(__('Auto-Renew')) ->boolean() ->trueIcon('heroicon-o-check-circle') ->falseIcon('heroicon-o-x-circle') ->trueColor('success') ->falseColor('gray') ->action(fn (Domain $record) => $record->sslCertificate?->type === 'lets_encrypt' ? $this->toggleAutoRenew($record->domain) : null), ]) ->recordActions([ Action::make('issueSsl') ->label(__('Issue SSL')) ->icon('heroicon-o-shield-check') ->color('success') ->visible(fn (Domain $record) => ! $record->sslCertificate || $record->sslCertificate->type === 'self_signed' || $record->sslCertificate->status === 'failed') ->requiresConfirmation() ->modalHeading(__('Issue SSL Certificate')) ->modalDescription(fn (Domain $record) => __("Issue a free Let's Encrypt SSL certificate for :domain? This will enable HTTPS for your domain.", ['domain' => $record->domain])) ->modalIcon('heroicon-o-shield-check') ->modalIconColor('success') ->modalSubmitActionLabel(__('Issue Certificate')) ->action(fn (Domain $record) => $this->issueLetsEncrypt($record->domain)), Action::make('renew') ->label(__('Renew')) ->icon('heroicon-o-arrow-path') ->color('info') ->visible(fn (Domain $record) => $record->sslCertificate?->type === 'lets_encrypt' && $record->sslCertificate?->status === 'active') ->requiresConfirmation() ->modalHeading(__('Renew SSL Certificate')) ->modalDescription(fn (Domain $record) => __("Renew the Let's Encrypt certificate for :domain? This will extend the certificate validity.", ['domain' => $record->domain])) ->modalIcon('heroicon-o-arrow-path') ->modalIconColor('info') ->modalSubmitActionLabel(__('Renew Certificate')) ->action(fn (Domain $record) => $this->renewCertificate($record->domain)), Action::make('selfSigned') ->label(__('Self-Signed')) ->icon('heroicon-o-exclamation-triangle') ->color('warning') ->visible(fn (Domain $record) => ! $record->sslCertificate) ->requiresConfirmation() ->modalHeading(__('Generate Self-Signed Certificate')) ->modalDescription(fn (Domain $record) => __('Generate a self-signed certificate for :domain? Note: Browsers will show a security warning for self-signed certificates.', ['domain' => $record->domain])) ->modalIcon('heroicon-o-exclamation-triangle') ->modalIconColor('warning') ->modalSubmitActionLabel(__('Generate Certificate')) ->action(fn (Domain $record) => $this->generateSelfSigned($record->domain)), Action::make('check') ->label(__('Check')) ->icon('heroicon-o-magnifying-glass') ->color('gray') ->action(fn (Domain $record) => $this->checkCertificate($record->domain)), ]) ->emptyStateHeading(__('No domains yet')) ->emptyStateDescription(__('Add a domain first to manage SSL certificates')) ->emptyStateIcon('heroicon-o-lock-closed') ->striped(); } public function issueLetsEncrypt(string $domainName): void { try { $domain = Domain::where('domain', $domainName) ->where('user_id', Auth::id()) ->firstOrFail(); $result = $this->getAgent()->sslIssue( $domainName, $this->getUsername(), Auth::user()->email, true ); if ($result['success'] ?? false) { // Update or create certificate record SslCertificate::updateOrCreate( ['domain_id' => $domain->id], [ 'type' => 'lets_encrypt', 'status' => 'active', 'issuer' => "Let's Encrypt", 'certificate' => $result['certificate'] ?? null, 'issued_at' => now(), 'expires_at' => isset($result['valid_to']) ? \Carbon\Carbon::parse($result['valid_to']) : now()->addMonths(3), 'last_check_at' => now(), 'last_error' => null, 'renewal_attempts' => 0, 'auto_renew' => true, ] ); $domain->update(['ssl_enabled' => true]); Notification::make() ->title(__('SSL Certificate Issued')) ->body(__("Let's Encrypt certificate has been issued for :domain", ['domain' => $domainName])) ->success() ->send(); } else { $error = $result['error'] ?? __('Unknown error'); // Record the failure SslCertificate::updateOrCreate( ['domain_id' => $domain->id], [ 'type' => 'lets_encrypt', 'status' => 'failed', 'last_check_at' => now(), 'last_error' => $error, ] ); Notification::make() ->title(__('SSL Certificate Failed')) ->body($error) ->danger() ->send(); } } catch (Exception $e) { Notification::make() ->title(__('Error')) ->body($e->getMessage()) ->danger() ->send(); } } public function generateSelfSigned(string $domainName): void { try { $domain = Domain::where('domain', $domainName) ->where('user_id', Auth::id()) ->firstOrFail(); $result = $this->getAgent()->sslGenerateSelfSigned( $domainName, $this->getUsername(), 365 ); if ($result['success'] ?? false) { SslCertificate::updateOrCreate( ['domain_id' => $domain->id], [ 'type' => 'self_signed', 'status' => 'active', 'issuer' => 'Self-Signed', 'issued_at' => now(), 'expires_at' => now()->addDays($result['valid_days'] ?? 365), 'last_check_at' => now(), 'last_error' => null, 'auto_renew' => false, ] ); $domain->update(['ssl_enabled' => true]); Notification::make() ->title(__('Self-Signed Certificate Generated')) ->body(__('Self-signed certificate created for :domain', ['domain' => $domainName])) ->success() ->send(); } else { Notification::make() ->title(__('Certificate Generation Failed')) ->body($result['error'] ?? __('Unknown error')) ->danger() ->send(); } } catch (Exception $e) { Notification::make() ->title(__('Error')) ->body($e->getMessage()) ->danger() ->send(); } } public function renewCertificate(string $domainName): void { try { $domain = Domain::where('domain', $domainName) ->where('user_id', Auth::id()) ->firstOrFail(); $result = $this->getAgent()->sslRenew($domainName, $this->getUsername()); if ($result['success'] ?? false) { $ssl = $domain->sslCertificate; if ($ssl) { $ssl->update([ 'status' => 'active', 'issued_at' => now(), 'expires_at' => isset($result['valid_to']) ? \Carbon\Carbon::parse($result['valid_to']) : now()->addMonths(3), 'last_check_at' => now(), 'last_error' => null, 'renewal_attempts' => 0, ]); } Notification::make() ->title(__('Certificate Renewed')) ->body(__('SSL certificate has been renewed for :domain', ['domain' => $domainName])) ->success() ->send(); } else { Notification::make() ->title(__('Renewal Failed')) ->body($result['error'] ?? __('Unknown error')) ->danger() ->send(); } } catch (Exception $e) { Notification::make() ->title(__('Error')) ->body($e->getMessage()) ->danger() ->send(); } } public function checkCertificate(string $domainName): void { try { $domain = Domain::where('domain', $domainName) ->where('user_id', Auth::id()) ->firstOrFail(); $result = $this->getAgent()->sslCheck($domainName, $this->getUsername()); if ($result['success'] ?? false) { $sslData = $result['ssl'] ?? []; if ($sslData['has_ssl'] ?? false) { SslCertificate::updateOrCreate( ['domain_id' => $domain->id], [ 'type' => $sslData['type'] ?? 'custom', 'status' => ($sslData['is_expired'] ?? false) ? 'expired' : 'active', 'issuer' => $sslData['issuer'], 'certificate' => $sslData['certificate'] ?? null, 'issued_at' => isset($sslData['valid_from']) ? \Carbon\Carbon::parse($sslData['valid_from']) : null, 'expires_at' => isset($sslData['valid_to']) ? \Carbon\Carbon::parse($sslData['valid_to']) : null, 'last_check_at' => now(), ] ); $domain->update(['ssl_enabled' => true]); } Notification::make() ->title(__('Certificate Checked')) ->body($sslData['has_ssl'] ? __('Certificate found: :issuer', ['issuer' => $sslData['issuer']]) : __('No certificate found')) ->success() ->send(); } else { Notification::make() ->title(__('Check Failed')) ->body($result['error'] ?? __('Unknown error')) ->danger() ->send(); } } catch (Exception $e) { Notification::make() ->title(__('Error')) ->body($e->getMessage()) ->danger() ->send(); } } public function toggleAutoRenew(string $domainName): void { try { $domain = Domain::where('domain', $domainName) ->where('user_id', Auth::id()) ->firstOrFail(); $ssl = $domain->sslCertificate; if ($ssl) { $ssl->update(['auto_renew' => ! $ssl->auto_renew]); Notification::make() ->title(__('Auto-Renew Updated')) ->body($ssl->auto_renew ? __('Auto-renewal enabled') : __('Auto-renewal disabled')) ->success() ->send(); } } catch (Exception $e) { Notification::make() ->title(__('Error')) ->body($e->getMessage()) ->danger() ->send(); } } public function installCustomCertificateAction(): Action { return Action::make('installCustomCertificate') ->label(__('Install Custom Certificate')) ->icon('heroicon-o-document-plus') ->modalHeading(__('Install Custom SSL Certificate')) ->modalDescription(__('Upload your own SSL certificate files to secure your domain with a custom certificate.')) ->modalIcon('heroicon-o-document-plus') ->modalIconColor('primary') ->modalSubmitActionLabel(__('Install Certificate')) ->modalWidth('lg') ->form([ Select::make('domain') ->label(__('Domain')) ->options(function () { return Domain::where('user_id', Auth::id()) ->pluck('domain', 'domain') ->toArray(); }) ->required() ->searchable() ->helperText(__('Select the domain to install the certificate on')), Textarea::make('certificate') ->label(__('Certificate (PEM format)')) ->placeholder(__("-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----")) ->rows(8) ->required() ->helperText(__('Paste your SSL certificate in PEM format')), Textarea::make('private_key') ->label(__('Private Key (PEM format)')) ->placeholder(__("-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----")) ->rows(8) ->required() ->helperText(__('Paste your private key in PEM format. Keep this secure!')), Textarea::make('ca_bundle') ->label(__('CA Bundle (optional)')) ->placeholder(__("-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----")) ->rows(6) ->helperText(__('Paste the certificate authority chain if required by your certificate provider')), ]) ->action(function (array $data): void { try { $domain = Domain::where('domain', $data['domain']) ->where('user_id', Auth::id()) ->firstOrFail(); $result = $this->getAgent()->sslInstall( $data['domain'], $this->getUsername(), $data['certificate'], $data['private_key'], $data['ca_bundle'] ?? null ); if ($result['success'] ?? false) { SslCertificate::updateOrCreate( ['domain_id' => $domain->id], [ 'type' => 'custom', 'status' => 'active', 'issuer' => $result['issuer'] ?? 'Custom', 'certificate' => $data['certificate'], 'private_key' => $data['private_key'], 'ca_bundle' => $data['ca_bundle'] ?? null, 'issued_at' => isset($result['valid_from']) ? \Carbon\Carbon::parse($result['valid_from']) : now(), 'expires_at' => isset($result['valid_to']) ? \Carbon\Carbon::parse($result['valid_to']) : null, 'last_check_at' => now(), 'last_error' => null, 'auto_renew' => false, ] ); $domain->update(['ssl_enabled' => true]); Notification::make() ->title(__('Certificate Installed')) ->body(__('Custom SSL certificate installed for :domain', ['domain' => $data['domain']])) ->success() ->send(); } else { Notification::make() ->title(__('Installation Failed')) ->body($result['error'] ?? __('Unknown error')) ->danger() ->send(); } } catch (Exception $e) { Notification::make() ->title(__('Error')) ->body($e->getMessage()) ->danger() ->send(); } }); } protected function getHeaderActions(): array { return [ $this->installCustomCertificateAction(), ]; } }