diff --git a/app/Actions/SSL/ActivateSSL.php b/app/Actions/SSL/ActivateSSL.php
new file mode 100644
index 0000000..4cf8f64
--- /dev/null
+++ b/app/Actions/SSL/ActivateSSL.php
@@ -0,0 +1,16 @@
+site->ssls()->update(['is_active' => false]);
+ $ssl->is_active = true;
+ $ssl->save();
+ $ssl->site->webserver()->updateVHost($ssl->site);
+ }
+}
diff --git a/app/Actions/SSL/CreateSSL.php b/app/Actions/SSL/CreateSSL.php
index 08215b1..5f2392e 100644
--- a/app/Actions/SSL/CreateSSL.php
+++ b/app/Actions/SSL/CreateSSL.php
@@ -31,6 +31,7 @@ public function create(Site $site, array $input): void
'expires_at' => $input['type'] === SslType::LETSENCRYPT ? now()->addMonths(3) : $input['expires_at'],
'status' => SslStatus::CREATING,
'email' => $input['email'] ?? null,
+ 'is_active' => ! $site->activeSsl,
]);
$ssl->domains = [$site->domain];
if (isset($input['aliases']) && $input['aliases']) {
diff --git a/app/Models/Site.php b/app/Models/Site.php
index d36e0df..8d8083a 100755
--- a/app/Models/Site.php
+++ b/app/Models/Site.php
@@ -75,6 +75,7 @@ class Site extends AbstractModel
'port',
'progress',
'user',
+ 'force_ssl',
];
protected $casts = [
@@ -84,6 +85,7 @@ class Site extends AbstractModel
'progress' => 'integer',
'aliases' => 'array',
'source_control_id' => 'integer',
+ 'force_ssl' => 'boolean',
];
public static array $statusColors = [
@@ -227,6 +229,7 @@ public function activeSsl(): HasOne
return $this->hasOne(Ssl::class)
->where('expires_at', '>=', now())
->where('status', SslStatus::CREATED)
+ ->where('is_active', true)
->orderByDesc('id');
}
diff --git a/app/Models/Ssl.php b/app/Models/Ssl.php
index fb827b6..7131448 100644
--- a/app/Models/Ssl.php
+++ b/app/Models/Ssl.php
@@ -17,10 +17,13 @@
* @property Carbon $expires_at
* @property string $status
* @property Site $site
- * @property string $ca_path
* @property ?array $domains
* @property int $log_id
* @property string $email
+ * @property bool $is_active
+ * @property string $certificate_path
+ * @property string $pk_path
+ * @property string $ca_path
* @property ?ServerLog $log
*/
class Ssl extends AbstractModel
@@ -38,6 +41,10 @@ class Ssl extends AbstractModel
'domains',
'log_id',
'email',
+ 'is_active',
+ 'certificate_path',
+ 'pk_path',
+ 'ca_path',
];
protected $casts = [
@@ -48,6 +55,7 @@ class Ssl extends AbstractModel
'expires_at' => 'datetime',
'domains' => 'array',
'log_id' => 'integer',
+ 'is_active' => 'boolean',
];
public static array $statusColors = [
@@ -62,58 +70,6 @@ public function site(): BelongsTo
return $this->belongsTo(Site::class);
}
- public function getCertsDirectoryPath(): ?string
- {
- if ($this->type == 'letsencrypt') {
- return '/etc/letsencrypt/live/'.$this->site->domain;
- }
-
- if ($this->type == 'custom') {
- return '/etc/ssl/'.$this->site->domain;
- }
-
- return '';
- }
-
- public function getCertificatePath(): ?string
- {
- if ($this->type == 'letsencrypt') {
- return $this->certificate;
- }
-
- if ($this->type == 'custom') {
- return $this->getCertsDirectoryPath().'/cert.pem';
- }
-
- return '';
- }
-
- public function getPkPath(): ?string
- {
- if ($this->type == 'letsencrypt') {
- return $this->pk;
- }
-
- if ($this->type == 'custom') {
- return $this->getCertsDirectoryPath().'/privkey.pem';
- }
-
- return '';
- }
-
- public function getCaPath(): ?string
- {
- if ($this->type == 'letsencrypt') {
- return $this->ca;
- }
-
- if ($this->type == 'custom') {
- return $this->getCertsDirectoryPath().'/fullchain.pem';
- }
-
- return '';
- }
-
public function validateSetup(string $result): bool
{
if (! Str::contains($result, 'Successfully received certificate')) {
@@ -121,8 +77,8 @@ public function validateSetup(string $result): bool
}
if ($this->type == 'letsencrypt') {
- $this->certificate = $this->getCertsDirectoryPath().'/fullchain.pem';
- $this->pk = $this->getCertsDirectoryPath().'/privkey.pem';
+ $this->certificate_path = '/etc/letsencrypt/live/'.$this->id.'/fullchain.pem';
+ $this->pk_path = '/etc/letsencrypt/live/'.$this->id.'/privkey.pem';
$this->save();
}
@@ -145,13 +101,4 @@ public function log(): BelongsTo
{
return $this->belongsTo(ServerLog::class);
}
-
- public function getEmailAttribute(?string $value): string
- {
- if ($value) {
- return $value;
- }
-
- return $this->site->server->creator->email;
- }
}
diff --git a/app/SSH/Services/Webserver/Nginx.php b/app/SSH/Services/Webserver/Nginx.php
index 3c85ab0..73a423d 100755
--- a/app/SSH/Services/Webserver/Nginx.php
+++ b/app/SSH/Services/Webserver/Nginx.php
@@ -169,16 +169,19 @@ public function setupSSL(Ssl $ssl): void
}
$command = view('ssh.services.webserver.nginx.create-letsencrypt-ssl', [
'email' => $ssl->email,
- 'domain' => $ssl->site->domain,
+ 'name' => $ssl->id,
'domains' => $domains,
]);
if ($ssl->type == 'custom') {
+ $ssl->certificate_path = '/etc/ssl/'.$ssl->id.'/cert.pem';
+ $ssl->pk_path = '/etc/ssl/'.$ssl->id.'/privkey.pem';
+ $ssl->save();
$command = view('ssh.services.webserver.nginx.create-custom-ssl', [
- 'path' => $ssl->getCertsDirectoryPath(),
+ 'path' => dirname($ssl->certificate_path),
'certificate' => $ssl->certificate,
'pk' => $ssl->pk,
- 'certificatePath' => $ssl->getCertificatePath(),
- 'pkPath' => $ssl->getPkPath(),
+ 'certificatePath' => $ssl->certificate_path,
+ 'pkPath' => $ssl->pk_path,
]);
}
$result = $this->service->server->ssh()->setLog($ssl->log)->exec(
@@ -197,7 +200,7 @@ public function setupSSL(Ssl $ssl): void
public function removeSSL(Ssl $ssl): void
{
$this->service->server->ssh()->exec(
- 'sudo rm -rf '.$ssl->getCertsDirectoryPath().'*',
+ 'sudo rm -rf '.dirname($ssl->certificate_path).'*',
'remove-ssl',
$ssl->site_id
);
diff --git a/app/Web/Pages/Servers/Sites/Pages/SSL/Index.php b/app/Web/Pages/Servers/Sites/Pages/SSL/Index.php
index 1b24af9..b9893e0 100644
--- a/app/Web/Pages/Servers/Sites/Pages/SSL/Index.php
+++ b/app/Web/Pages/Servers/Sites/Pages/SSL/Index.php
@@ -15,6 +15,7 @@
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Get;
+use Filament\Notifications\Notification;
use Filament\Support\Enums\MaxWidth;
class Index extends Page
@@ -44,6 +45,24 @@ protected function getHeaderActions(): array
->color('gray')
->url('https://vitodeploy.com/sites/ssl')
->openUrlInNewTab(),
+ Action::make('force-ssl')
+ ->label('Force SSL')
+ ->tooltip(fn () => $this->site->force_ssl ? 'Disable force SSL' : 'Enable force SSL')
+ ->icon(fn () => $this->site->force_ssl ? 'icon-force-ssl-enabled' : 'icon-force-ssl-disabled')
+ ->requiresConfirmation()
+ ->modalSubmitActionLabel(fn () => $this->site->force_ssl ? 'Disable' : 'Enable')
+ ->action(function () {
+ $this->site->update([
+ 'force_ssl' => ! $this->site->force_ssl,
+ ]);
+ $this->site->webserver()->updateVHost($this->site);
+ Notification::make()
+ ->success()
+ ->title('SSL status has been updated.')
+ ->send();
+ $this->dispatch('$refresh');
+ })
+ ->color('gray'),
CreateAction::make('create')
->label('New Certificate')
->icon('heroicon-o-lock-closed')
diff --git a/app/Web/Pages/Servers/Sites/Pages/SSL/Widgets/SslsList.php b/app/Web/Pages/Servers/Sites/Pages/SSL/Widgets/SslsList.php
index 94f0480..aaedbed 100644
--- a/app/Web/Pages/Servers/Sites/Pages/SSL/Widgets/SslsList.php
+++ b/app/Web/Pages/Servers/Sites/Pages/SSL/Widgets/SslsList.php
@@ -2,11 +2,14 @@
namespace App\Web\Pages\Servers\Sites\Pages\SSL\Widgets;
+use App\Actions\SSL\ActivateSSL;
use App\Actions\SSL\DeleteSSL;
use App\Models\Site;
use App\Models\Ssl;
+use Filament\Notifications\Notification;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\DeleteAction;
+use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as Widget;
@@ -27,6 +30,9 @@ protected function getTableQuery(): Builder
protected function getTableColumns(): array
{
return [
+ IconColumn::make('is_active')
+ ->color(fn (Ssl $record) => $record->is_active ? 'green' : 'gray')
+ ->icon(fn (Ssl $record) => $record->is_active ? 'heroicon-o-lock-closed' : 'heroicon-o-lock-open'),
TextColumn::make('type')
->searchable()
->sortable(),
@@ -52,6 +58,25 @@ public function table(Table $table): Table
->query($this->getTableQuery())
->columns($this->getTableColumns())
->actions([
+ Action::make('activate-ssl')
+ ->hiddenLabel()
+ ->visible(fn (Ssl $record) => ! $record->is_active)
+ ->tooltip('Activate SSL')
+ ->icon('heroicon-o-lock-closed')
+ ->authorize(fn (Ssl $record) => auth()->user()->can('update', [$record->site, $this->site->server]))
+ ->requiresConfirmation()
+ ->modalHeading('Activate SSL')
+ ->modalSubmitActionLabel('Activate')
+ ->action(function (Ssl $record) {
+ run_action($this, function () use ($record) {
+ app(ActivateSSL::class)->activate($record);
+
+ Notification::make()
+ ->success()
+ ->title('SSL has been activated.')
+ ->send();
+ });
+ }),
Action::make('logs')
->hiddenLabel()
->tooltip('Logs')
diff --git a/app/Web/Pages/Servers/Sites/View.php b/app/Web/Pages/Servers/Sites/View.php
index d431a96..f6073b0 100644
--- a/app/Web/Pages/Servers/Sites/View.php
+++ b/app/Web/Pages/Servers/Sites/View.php
@@ -82,7 +82,9 @@ public function getHeaderActions(): array
if (in_array(SiteFeature::DEPLOYMENT, $this->site->type()->supportedFeatures())) {
$actions[] = $this->deployAction();
- $actionsGroup[] = $this->autoDeploymentAction();
+ if ($this->site->sourceControl) {
+ $actionsGroup[] = $this->autoDeploymentAction();
+ }
$actionsGroup[] = $this->deploymentScriptAction();
}
diff --git a/database/migrations/2025_01_31_155828_update_ssls_table.php b/database/migrations/2025_01_31_155828_update_ssls_table.php
new file mode 100644
index 0000000..4bf5ba6
--- /dev/null
+++ b/database/migrations/2025_01_31_155828_update_ssls_table.php
@@ -0,0 +1,54 @@
+boolean('is_active')->default(false);
+ $table->string('certificate_path')->nullable();
+ $table->string('pk_path')->nullable();
+ $table->string('ca_path')->nullable();
+ });
+ Site::query()->chunk(100, function ($sites) {
+ foreach ($sites as $site) {
+ foreach ($site->ssls as $ssl) {
+ if ($ssl->type === SslType::LETSENCRYPT) {
+ $ssl->certificate_path = $ssl->certificate;
+ $ssl->pk_path = $ssl->pk;
+ $ssl->ca_path = $ssl->ca;
+ $ssl->certificate = null;
+ $ssl->pk = null;
+ $ssl->ca = null;
+ }
+ if ($ssl->type === SslType::CUSTOM) {
+ $ssl->certificate_path = '/etc/ssl/'.$ssl->site->domain.'/cert.pem';
+ $ssl->pk_path = '/etc/ssl/'.$ssl->site->domain.'/privkey.pem';
+ $ssl->ca_path = '/etc/ssl/'.$ssl->site->domain.'/fullchain.pem';
+ }
+ $ssl->save();
+ }
+ $activeSSL = $site->ssls()->where('expires_at', '>=', now())->latest()->first();
+ if ($activeSSL) {
+ $activeSSL->update(['is_active' => true]);
+ }
+ }
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('ssls', function (Blueprint $table) {
+ $table->dropColumn('is_active');
+ $table->dropColumn('certificate_path');
+ $table->dropColumn('pk_path');
+ $table->dropColumn('ca_path');
+ });
+ }
+};
diff --git a/resources/svg/force-ssl-disabled.svg b/resources/svg/force-ssl-disabled.svg
new file mode 100644
index 0000000..8b942f1
--- /dev/null
+++ b/resources/svg/force-ssl-disabled.svg
@@ -0,0 +1,3 @@
+
diff --git a/resources/svg/force-ssl-enabled.svg b/resources/svg/force-ssl-enabled.svg
new file mode 100644
index 0000000..72db4e8
--- /dev/null
+++ b/resources/svg/force-ssl-enabled.svg
@@ -0,0 +1,4 @@
+
diff --git a/resources/views/ssh/services/webserver/nginx/create-letsencrypt-ssl.blade.php b/resources/views/ssh/services/webserver/nginx/create-letsencrypt-ssl.blade.php
index 032ac6a..d818b38 100644
--- a/resources/views/ssh/services/webserver/nginx/create-letsencrypt-ssl.blade.php
+++ b/resources/views/ssh/services/webserver/nginx/create-letsencrypt-ssl.blade.php
@@ -1,3 +1,3 @@
-if ! sudo certbot certonly --force-renewal --nginx --noninteractive --agree-tos --cert-name {{ $domain }} -m {{ $email }} {{ $domains }} --verbose; then
+if ! sudo certbot certonly --force-renewal --nginx --noninteractive --agree-tos --cert-name {{ $name }} -m {{ $email }} {{ $domains }} --verbose; then
echo 'VITO_SSH_ERROR' && exit 1
fi
diff --git a/resources/views/ssh/services/webserver/nginx/vhost.blade.php b/resources/views/ssh/services/webserver/nginx/vhost.blade.php
index f81ce8b..eb0bb31 100755
--- a/resources/views/ssh/services/webserver/nginx/vhost.blade.php
+++ b/resources/views/ssh/services/webserver/nginx/vhost.blade.php
@@ -33,8 +33,8 @@
@endif
@if ($site->activeSsl)
listen 443 ssl;
- ssl_certificate {{ $site->activeSsl->getCertificatePath() }};
- ssl_certificate_key {{ $site->activeSsl->getPkPath() }};
+ ssl_certificate {{ $site->activeSsl->certificate_path }};
+ ssl_certificate_key {{ $site->activeSsl->pk_path }};
@endif
server_name {{ $site->domain }} {{ $site->getAliasesString() }};
diff --git a/tests/Feature/SslTest.php b/tests/Feature/SslTest.php
index 1d4e67f..ec918f2 100644
--- a/tests/Feature/SslTest.php
+++ b/tests/Feature/SslTest.php
@@ -48,12 +48,17 @@ public function test_letsencrypt_ssl()
])
->assertSuccessful();
+ $ssl = Ssl::query()->where('site_id', $this->site->id)->first();
+ $this->assertNotEmpty($ssl);
+
$this->assertDatabaseHas('ssls', [
'site_id' => $this->site->id,
'type' => SslType::LETSENCRYPT,
'status' => SslStatus::CREATED,
'domains' => json_encode([$this->site->domain]),
'email' => 'ssl@example.com',
+ 'certificate_path' => '/etc/letsencrypt/live/'.$ssl->id.'/fullchain.pem',
+ 'pk_path' => '/etc/letsencrypt/live/'.$ssl->id.'/privkey.pem',
]);
}
@@ -101,10 +106,16 @@ public function test_custom_ssl()
])
->assertSuccessful();
+ $ssl = Ssl::query()->where('site_id', $this->site->id)->first();
+ $this->assertNotEmpty($ssl);
+
$this->assertDatabaseHas('ssls', [
'site_id' => $this->site->id,
'type' => SslType::CUSTOM,
'status' => SslStatus::CREATED,
+ 'domains' => json_encode([$this->site->domain]),
+ 'certificate_path' => '/etc/ssl/'.$ssl->id.'/cert.pem',
+ 'pk_path' => '/etc/ssl/'.$ssl->id.'/privkey.pem',
]);
}