506 lines
21 KiB
PHP
506 lines
21 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Filament\Jabali\Pages;
|
|
|
|
use App\Filament\Concerns\HasPageTour;
|
|
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 HasPageTour;
|
|
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->getTourAction(),
|
|
$this->installCustomCertificateAction(),
|
|
];
|
|
}
|
|
}
|