mirror of
https://github.com/vitodeploy/vito.git
synced 2025-04-19 18:01:37 +00:00
Force SSL and Multi SSL (#456)
This commit is contained in:
parent
ea396786e4
commit
262c5e040d
16
app/Actions/SSL/ActivateSSL.php
Normal file
16
app/Actions/SSL/ActivateSSL.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\SSL;
|
||||
|
||||
use App\Models\Ssl;
|
||||
|
||||
class ActivateSSL
|
||||
{
|
||||
public function activate(Ssl $ssl): void
|
||||
{
|
||||
$ssl->site->ssls()->update(['is_active' => false]);
|
||||
$ssl->is_active = true;
|
||||
$ssl->save();
|
||||
$ssl->site->webserver()->updateVHost($ssl->site);
|
||||
}
|
||||
}
|
@ -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']) {
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
54
database/migrations/2025_01_31_155828_update_ssls_table.php
Normal file
54
database/migrations/2025_01_31_155828_update_ssls_table.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\SslType;
|
||||
use App\Models\Site;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('ssls', function (Blueprint $table) {
|
||||
$table->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');
|
||||
});
|
||||
}
|
||||
};
|
3
resources/svg/force-ssl-disabled.svg
Normal file
3
resources/svg/force-ssl-disabled.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 10.5V6.75a4.5 4.5 0 1 1 9 0v3.75M3.75 21.75h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H3.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" />
|
||||
</svg>
|
After Width: | Height: | Size: 374 B |
4
resources/svg/force-ssl-enabled.svg
Normal file
4
resources/svg/force-ssl-enabled.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="#16a34a">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 365 B |
@ -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
|
||||
|
@ -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() }};
|
||||
|
@ -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',
|
||||
]);
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user