Files
jabali-panel/app/Filament/Jabali/Pages/Ssl.php
2026-01-28 04:19:30 +02:00

503 lines
21 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Filament\Jabali\Pages;
use App\Models\Domain;
use App\Models\SslCertificate;
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\Textarea;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Tables\Columns\IconColumn;
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\Support\Facades\Auth;
class Ssl extends Page implements HasActions, HasForms, HasTable
{
use InteractsWithActions;
use InteractsWithForms;
use InteractsWithTable;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-lock-closed';
protected static ?int $navigationSort = 10;
protected string $view = 'filament.jabali.pages.ssl';
protected ?AgentClient $agent = null;
public static function getNavigationLabel(): string
{
return __('SSL Certificates');
}
public function getTitle(): string|Htmlable
{
return __('SSL Certificates');
}
public function getAgent(): AgentClient
{
if ($this->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(),
];
}
}