Manage site aliases (#206)

* manage site aliases

* build assets

* fix tests
This commit is contained in:
Saeed Vaziry 2024-05-15 11:23:24 +02:00 committed by GitHub
parent 30ef8ad5eb
commit de468ae1ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 219 additions and 38 deletions

View File

@ -28,6 +28,10 @@ public function create(Site $site, array $input): void
'expires_at' => $input['type'] === SslType::LETSENCRYPT ? now()->addMonths(3) : $input['expires_at'],
'status' => SslStatus::CREATING,
]);
$ssl->domains = [$site->domain];
if (isset($input['aliases']) && $input['aliases']) {
$ssl->domains = array_merge($ssl->domains, $site->aliases);
}
$ssl->save();
dispatch(function () use ($site, $ssl) {

View File

@ -21,7 +21,7 @@
class CreateSite
{
/**
* @throws ValidationException
* @throws SourceControlIsNotConnected
*/
public function create(Server $server, array $input): Site
{
@ -33,7 +33,7 @@ public function create(Server $server, array $input): Site
'server_id' => $server->id,
'type' => $input['type'],
'domain' => $input['domain'],
'aliases' => isset($input['alias']) ? [$input['alias']] : [],
'aliases' => $input['aliases'] ?? [],
'path' => '/home/'.$server->getSshUser().'/'.$input['domain'],
'status' => SiteStatus::INSTALLING,
]);
@ -115,7 +115,7 @@ private function validateInputs(Server $server, array $input): void
return $query->where('server_id', $server->id);
}),
],
'alias' => [
'aliases.*' => [
new DomainRule(),
],
];

View File

@ -0,0 +1,33 @@
<?php
namespace App\Actions\Site;
use App\Models\Site;
use App\SSH\Services\Webserver\Webserver;
use App\ValidationRules\DomainRule;
use Illuminate\Support\Facades\Validator;
class UpdateAliases
{
public function update(Site $site, array $input): void
{
$this->validate($input);
$site->aliases = $input['aliases'] ?? [];
/** @var Webserver $webserver */
$webserver = $site->server->webserver()->handler();
$webserver->updateVHost($site, ! $site->hasSSL());
$site->save();
}
private function validate(array $input): void
{
Validator::make($input, [
'aliases.*' => [
new DomainRule(),
],
])->validate();
}
}

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers;
use App\Actions\Site\UpdateAliases;
use App\Actions\Site\UpdateSourceControl;
use App\Facades\Toast;
use App\Helpers\HtmxResponse;
@ -83,10 +84,21 @@ public function updateSourceControl(Server $server, Site $site, Request $request
{
$this->authorize('manage', $server);
$site = app(UpdateSourceControl::class)->update($site, $request->input());
app(UpdateSourceControl::class)->update($site, $request->input());
Toast::success('Source control updated successfully!');
return htmx()->back();
}
public function updateAliases(Server $server, Site $site, Request $request): HtmxResponse
{
$this->authorize('manage', $server);
app(UpdateAliases::class)->update($site, $request->input());
Toast::success('Aliases updated successfully!');
return htmx()->back();
}
}

View File

@ -278,4 +278,9 @@ public function getEnv(): string
return '';
}
}
public function hasSSL(): bool
{
return $this->ssls->isNotEmpty();
}
}

View File

@ -17,6 +17,7 @@
* @property string $status
* @property Site $site
* @property string $ca_path
* @property ?array $domains
*/
class Ssl extends AbstractModel
{
@ -30,6 +31,7 @@ class Ssl extends AbstractModel
'ca',
'expires_at',
'status',
'domains',
];
protected $casts = [
@ -38,6 +40,7 @@ class Ssl extends AbstractModel
'pk' => 'encrypted',
'ca' => 'encrypted',
'expires_at' => 'datetime',
'domains' => 'array',
];
public function site(): BelongsTo
@ -111,4 +114,16 @@ public function validateSetup(string $result): bool
return true;
}
public function getDomains(): array
{
if (! empty($this->domains) && is_array($this->domains)) {
return $this->domains;
}
$this->domains = [$this->site->domain];
$this->save();
return $this->domains;
}
}

View File

@ -117,11 +117,9 @@ public function changePHPVersion(Site $site, $version): void
*/
public function setupSSL(Ssl $ssl): void
{
$domains = '-d '.$ssl->site->domain;
if ($ssl->site->aliases) {
foreach ($ssl->site->aliases as $alias) {
$domains .= ' -d '.$alias;
}
$domains = '';
foreach ($ssl->getDomains() as $domain) {
$domains .= ' -d '.$domain;
}
$command = $this->getScript('nginx/create-letsencrypt-ssl.sh', [
'email' => $ssl->site->server->creator->email,

View File

@ -20,7 +20,7 @@ public function definition(): array
'ca' => $this->faker->word(),
'expires_at' => Carbon::now()->addDay(),
'status' => SslStatus::CREATED,
'domains' => 'example.com',
'domains' => ['example.com'],
];
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"resources/css/app.css": {
"file": "assets/app-4e6a754c.css",
"file": "assets/app-f8a673af.css",
"isEntry": true,
"src": "resources/css/app.css"
},

View File

@ -4,11 +4,11 @@
@php
$class = [
"success" => "rounded-md border border-green-300 bg-green-50 px-2 py-1 text-xs uppercase text-green-500 dark:border-green-600 dark:bg-green-500 dark:bg-opacity-10",
"danger" => "rounded-md border border-red-300 bg-red-50 px-2 py-1 text-xs uppercase text-red-500 dark:border-red-600 dark:bg-red-500 dark:bg-opacity-10",
"warning" => "rounded-md border border-yellow-300 bg-yellow-50 px-2 py-1 text-xs uppercase text-yellow-500 dark:border-yellow-600 dark:bg-yellow-500 dark:bg-opacity-10",
"disabled" => "rounded-md border border-gray-300 bg-gray-50 px-2 py-1 text-xs uppercase text-gray-500 dark:border-gray-600 dark:bg-gray-500 dark:bg-opacity-30 dark:text-gray-400",
"info" => "rounded-md border border-primary-300 bg-primary-50 px-2 py-1 text-xs uppercase text-primary-500 dark:border-primary-600 dark:bg-primary-500 dark:bg-opacity-10",
"success" => "max-w-max rounded-md border border-green-300 bg-green-50 px-2 py-1 text-xs uppercase text-green-500 dark:border-green-600 dark:bg-green-500 dark:bg-opacity-10",
"danger" => "max-w-max rounded-md border border-red-300 bg-red-50 px-2 py-1 text-xs uppercase text-red-500 dark:border-red-600 dark:bg-red-500 dark:bg-opacity-10",
"warning" => "max-w-max rounded-md border border-yellow-300 bg-yellow-50 px-2 py-1 text-xs uppercase text-yellow-500 dark:border-yellow-600 dark:bg-yellow-500 dark:bg-opacity-10",
"disabled" => "max-w-max rounded-md border border-gray-300 bg-gray-50 px-2 py-1 text-xs uppercase text-gray-500 dark:border-gray-600 dark:bg-gray-500 dark:bg-opacity-30 dark:text-gray-400",
"info" => "max-w-max rounded-md border border-primary-300 bg-primary-50 px-2 py-1 text-xs uppercase text-primary-500 dark:border-primary-600 dark:bg-primary-500 dark:bg-opacity-10",
];
@endphp

View File

@ -1,5 +1,5 @@
<td
{!! $attributes->merge(["class" => "whitespace-nowrap px-6 py-4 text-gray-700 dark:text-gray-300 w-1"]) !!}
{!! $attributes->merge(["class" => "text-sm whitespace-nowrap px-6 py-4 text-gray-700 dark:text-gray-300 w-1"]) !!}
>
{{ $slot }}
</td>

View File

@ -3,6 +3,8 @@
@include("site-settings.partials.change-php-version")
@include("site-settings.partials.update-aliases")
@if ($site->source_control_id)
@include("site-settings.partials.update-source-control")
@endif

View File

@ -0,0 +1,30 @@
<x-card>
<x-slot name="title">{{ __("Update Aliases") }}</x-slot>
<x-slot name="description">
{{ __("Add/Remove site aliases") }}
</x-slot>
<form
id="update-aliases"
hx-post="{{ route("servers.sites.settings.aliases", ["server" => $server, "site" => $site]) }}"
hx-swap="outerHTML"
hx-select="#update-aliases"
hx-ext="disable-element"
hx-disable-element="#btn-update-aliases"
class="space-y-6"
>
@include(
"sites.partials.create.fields.aliases",
[
"aliases" => $site->aliases,
]
)
</form>
<x-slot name="actions">
<x-primary-button id="btn-update-aliases" form="update-aliases" hx-disable>
{{ __("Save") }}
</x-primary-button>
</x-slot>
</x-card>

View File

@ -55,21 +55,7 @@ class="mt-1 block w-full"
@enderror
</div>
<div>
<x-input-label for="alias" :value="__('Alias')" />
<x-text-input
value="{{ old('alias') }}"
id="alias"
name="alias"
type="text"
class="mt-1 block w-full"
autocomplete="alias"
placeholder="www.example.com"
/>
@error("alias")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
@include("sites.partials.create.fields.aliases")
@include("sites.partials.create." . $type)
</form>

View File

@ -0,0 +1,55 @@
<script>
let aliases = @json($aliases ?? []);
</script>
<div
x-data="{
aliasInput: '',
aliases: aliases,
removeAlias(alias) {
this.aliases = this.aliases.filter((a) => a !== alias)
},
addAlias() {
if (! this.aliasInput) {
return
}
if (this.aliases.includes(this.aliasInput)) {
return
}
this.aliases.push(this.aliasInput)
this.aliasInput = ''
},
}"
>
<x-input-label for="alias" :value="__('Alias')" />
<div class="flex items-center">
<x-text-input
value="{{ old('alias') }}"
id="alias"
x-model="aliasInput"
name="alias"
type="text"
class="mt-1 block w-full"
autocomplete="alias"
placeholder="www.example.com"
/>
<x-secondary-button type="button" class="ml-2 flex-none" x-on:click="addAlias()">
{{ __("Add") }}
</x-secondary-button>
</div>
<div class="mt-1">
<template x-for="alias in aliases">
<div class="mr-1 inline-flex">
<x-status status="info" class="flex items-center lowercase">
<span x-text="alias"></span>
<x-heroicon name="o-x-mark" class="ml-1 h-4 w-4 cursor-pointer" x-on:click="removeAlias(alias)" />
<input type="hidden" name="aliases[]" x-bind:value="alias" />
</x-status>
</div>
</template>
</div>
@error("aliases")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>

View File

@ -82,6 +82,12 @@ class="mt-1 w-full"
</div>
</div>
<div class="mt-6">
<x-checkbox id="aliases" name="aliases" :checked="old('aliases')" value="1">
Set SSL for site's aliases as well
</x-checkbox>
</div>
<div class="mt-6 flex justify-end">
<x-secondary-button type="button" x-on:click="$dispatch('close')">
{{ __("Cancel") }}

View File

@ -14,6 +14,7 @@
<x-table>
<x-tr>
<x-th>{{ __("Type") }}</x-th>
<x-th>{{ __("Domains") }}</x-th>
<x-th>{{ __("Created") }}</x-th>
<x-th>{{ __("Expires at") }}</x-th>
<x-th></x-th>
@ -21,6 +22,15 @@
@foreach ($ssls as $ssl)
<x-tr>
<x-td>{{ $ssl->type }}</x-td>
<x-td>
<div class="flex-col space-y-1">
@foreach ($ssl->getDomains() as $domain)
<x-status status="disabled" class="lowercase">
{{ $domain }}
</x-status>
@endforeach
</div>
</x-td>
<x-td>
<x-datetime :value="$ssl->created_at" />
</x-td>

View File

@ -67,6 +67,7 @@
Route::post('/{site}/settings/vhost', [SiteSettingController::class, 'updateVhost']);
Route::post('/{site}/settings/php', [SiteSettingController::class, 'updatePHPVersion'])->name('servers.sites.settings.php');
Route::post('/{site}/settings/source-control', [SiteSettingController::class, 'updateSourceControl'])->name('servers.sites.settings.source-control');
Route::post('/{site}/settings/update-aliases', [SiteSettingController::class, 'updateAliases'])->name('servers.sites.settings.aliases');
// site logs
Route::get('/{site}/logs', [SiteLogController::class, 'index'])->name('servers.sites.logs');

View File

@ -42,6 +42,7 @@ public function test_create_site(array $inputs): void
$this->assertDatabaseHas('sites', [
'domain' => 'example.com',
'aliases' => json_encode($inputs['aliases'] ?? []),
'status' => SiteStatus::READY,
]);
}
@ -54,7 +55,7 @@ public function test_create_site_failed_due_to_source_control(int $status): void
$inputs = [
'type' => SiteType::LARAVEL,
'domain' => 'example.com',
'alias' => 'www.example.com',
'aliases' => ['www.example.com'],
'php_version' => '8.2',
'web_directory' => 'public',
'repository' => 'test/test',
@ -220,7 +221,7 @@ public static function create_data(): array
[
'type' => SiteType::LARAVEL,
'domain' => 'example.com',
'alias' => 'www.example.com',
'aliases' => ['www.example.com', 'www2.example.com'],
'php_version' => '8.2',
'web_directory' => 'public',
'repository' => 'test/test',
@ -232,7 +233,7 @@ public static function create_data(): array
[
'type' => SiteType::WORDPRESS,
'domain' => 'example.com',
'alias' => 'www.example.com',
'aliases' => ['www.example.com'],
'php_version' => '8.2',
'title' => 'Example',
'username' => 'example',
@ -247,7 +248,7 @@ public static function create_data(): array
[
'type' => SiteType::PHP_BLANK,
'domain' => 'example.com',
'alias' => 'www.example.com',
'aliases' => ['www.example.com'],
'php_version' => '8.2',
'web_directory' => 'public',
],
@ -256,7 +257,7 @@ public static function create_data(): array
[
'type' => SiteType::PHPMYADMIN,
'domain' => 'example.com',
'alias' => 'www.example.com',
'aliases' => ['www.example.com'],
'php_version' => '8.2',
'version' => '5.1.2',
],

View File

@ -58,6 +58,29 @@ public function test_letsencrypt_ssl()
'site_id' => $this->site->id,
'type' => SslType::LETSENCRYPT,
'status' => SslStatus::CREATED,
'domains' => json_encode([$this->site->domain]),
]);
}
public function test_letsencrypt_ssl_with_aliases()
{
SSH::fake('Successfully received certificate');
$this->actingAs($this->user);
$this->post(route('servers.sites.ssl.store', [
'server' => $this->server,
'site' => $this->site,
]), [
'type' => SslType::LETSENCRYPT,
'aliases' => '1',
])->assertSessionDoesntHaveErrors();
$this->assertDatabaseHas('ssls', [
'site_id' => $this->site->id,
'type' => SslType::LETSENCRYPT,
'status' => SslStatus::CREATED,
'domains' => json_encode(array_merge([$this->site->domain], $this->site->aliases)),
]);
}