Handle database status updates more reliably by listening for `ServiceChecked` and using explicit `refresh()` handlers in Livewire database components. Also switch guarded clone/create paths to `forceFill`/`forceCreate` in helper flows to avoid missing persisted attributes during app/service cloning. Update log/terminal font stacks to Geist (with bundled variable fonts) and add coverage for SSL status refresh, persistent volume UUID cloning, and log font styling.
348 lines
12 KiB
PHP
348 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Project\Database\Mongodb;
|
|
|
|
use App\Actions\Database\StartDatabaseProxy;
|
|
use App\Actions\Database\StopDatabaseProxy;
|
|
use App\Helpers\SslHelper;
|
|
use App\Models\Server;
|
|
use App\Models\StandaloneMongodb;
|
|
use App\Support\ValidationPatterns;
|
|
use Carbon\Carbon;
|
|
use Exception;
|
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Livewire\Component;
|
|
|
|
class General extends Component
|
|
{
|
|
use AuthorizesRequests;
|
|
|
|
public ?Server $server = null;
|
|
|
|
public StandaloneMongodb $database;
|
|
|
|
public string $name;
|
|
|
|
public ?string $description = null;
|
|
|
|
public ?string $mongoConf = null;
|
|
|
|
public string $mongoInitdbRootUsername;
|
|
|
|
public string $mongoInitdbRootPassword;
|
|
|
|
public string $mongoInitdbDatabase;
|
|
|
|
public string $image;
|
|
|
|
public ?string $portsMappings = null;
|
|
|
|
public ?bool $isPublic = null;
|
|
|
|
public mixed $publicPort = null;
|
|
|
|
public mixed $publicPortTimeout = 3600;
|
|
|
|
public bool $isLogDrainEnabled = false;
|
|
|
|
public ?string $customDockerRunOptions = null;
|
|
|
|
public bool $enableSsl = false;
|
|
|
|
public ?string $sslMode = null;
|
|
|
|
public ?string $db_url = null;
|
|
|
|
public ?string $db_url_public = null;
|
|
|
|
public ?Carbon $certificateValidUntil = null;
|
|
|
|
public function getListeners()
|
|
{
|
|
$userId = Auth::id();
|
|
$teamId = Auth::user()->currentTeam()->id;
|
|
|
|
return [
|
|
"echo-private:user.{$userId},DatabaseStatusChanged" => 'refresh',
|
|
"echo-private:team.{$teamId},ServiceChecked" => 'refresh',
|
|
];
|
|
}
|
|
|
|
protected function rules(): array
|
|
{
|
|
return [
|
|
'name' => ValidationPatterns::nameRules(),
|
|
'description' => ValidationPatterns::descriptionRules(),
|
|
'mongoConf' => 'nullable',
|
|
'mongoInitdbRootUsername' => 'required',
|
|
'mongoInitdbRootPassword' => 'required',
|
|
'mongoInitdbDatabase' => 'required',
|
|
'image' => 'required',
|
|
'portsMappings' => ValidationPatterns::portMappingRules(),
|
|
'isPublic' => 'nullable|boolean',
|
|
'publicPort' => 'nullable|integer|min:1|max:65535',
|
|
'publicPortTimeout' => 'nullable|integer|min:1',
|
|
'isLogDrainEnabled' => 'nullable|boolean',
|
|
'customDockerRunOptions' => 'nullable',
|
|
'enableSsl' => 'boolean',
|
|
'sslMode' => 'nullable|string|in:allow,prefer,require,verify-full',
|
|
];
|
|
}
|
|
|
|
protected function messages(): array
|
|
{
|
|
return array_merge(
|
|
ValidationPatterns::combinedMessages(),
|
|
ValidationPatterns::portMappingMessages(),
|
|
[
|
|
'name.required' => 'The Name field is required.',
|
|
'mongoInitdbRootUsername.required' => 'The Root Username field is required.',
|
|
'mongoInitdbRootPassword.required' => 'The Root Password field is required.',
|
|
'mongoInitdbDatabase.required' => 'The MongoDB Database field is required.',
|
|
'image.required' => 'The Docker Image field is required.',
|
|
'publicPort.integer' => 'The Public Port must be an integer.',
|
|
'publicPort.min' => 'The Public Port must be at least 1.',
|
|
'publicPort.max' => 'The Public Port must not exceed 65535.',
|
|
'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.',
|
|
'publicPortTimeout.min' => 'The Public Port Timeout must be at least 1.',
|
|
'sslMode.in' => 'The SSL Mode must be one of: allow, prefer, require, verify-full.',
|
|
]
|
|
);
|
|
}
|
|
|
|
protected $validationAttributes = [
|
|
'name' => 'Name',
|
|
'description' => 'Description',
|
|
'mongoConf' => 'Mongo Configuration',
|
|
'mongoInitdbRootUsername' => 'Root Username',
|
|
'mongoInitdbRootPassword' => 'Root Password',
|
|
'mongoInitdbDatabase' => 'Database',
|
|
'image' => 'Image',
|
|
'portsMappings' => 'Port Mapping',
|
|
'isPublic' => 'Is Public',
|
|
'publicPort' => 'Public Port',
|
|
'publicPortTimeout' => 'Public Port Timeout',
|
|
'customDockerRunOptions' => 'Custom Docker Run Options',
|
|
'enableSsl' => 'Enable SSL',
|
|
'sslMode' => 'SSL Mode',
|
|
];
|
|
|
|
public function mount()
|
|
{
|
|
try {
|
|
$this->authorize('view', $this->database);
|
|
$this->syncData();
|
|
$this->server = data_get($this->database, 'destination.server');
|
|
if (! $this->server) {
|
|
$this->dispatch('error', 'Database destination server is not configured.');
|
|
|
|
return;
|
|
}
|
|
|
|
$existingCert = $this->database->sslCertificates()->first();
|
|
|
|
if ($existingCert) {
|
|
$this->certificateValidUntil = $existingCert->valid_until;
|
|
}
|
|
} catch (Exception $e) {
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
public function syncData(bool $toModel = false)
|
|
{
|
|
if ($toModel) {
|
|
$this->validate();
|
|
$this->database->name = $this->name;
|
|
$this->database->description = $this->description;
|
|
$this->database->mongo_conf = $this->mongoConf;
|
|
$this->database->mongo_initdb_root_username = $this->mongoInitdbRootUsername;
|
|
$this->database->mongo_initdb_root_password = $this->mongoInitdbRootPassword;
|
|
$this->database->mongo_initdb_database = $this->mongoInitdbDatabase;
|
|
$this->database->image = $this->image;
|
|
$this->database->ports_mappings = $this->portsMappings;
|
|
$this->database->is_public = $this->isPublic;
|
|
$this->database->public_port = $this->publicPort ?: null;
|
|
$this->database->public_port_timeout = $this->publicPortTimeout ?: null;
|
|
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
|
|
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
|
|
$this->database->enable_ssl = $this->enableSsl;
|
|
$this->database->ssl_mode = $this->sslMode;
|
|
$this->database->save();
|
|
|
|
$this->db_url = $this->database->internal_db_url;
|
|
$this->db_url_public = $this->database->external_db_url;
|
|
} else {
|
|
$this->name = $this->database->name;
|
|
$this->description = $this->database->description;
|
|
$this->mongoConf = $this->database->mongo_conf;
|
|
$this->mongoInitdbRootUsername = $this->database->mongo_initdb_root_username;
|
|
$this->mongoInitdbRootPassword = $this->database->mongo_initdb_root_password;
|
|
$this->mongoInitdbDatabase = $this->database->mongo_initdb_database;
|
|
$this->image = $this->database->image;
|
|
$this->portsMappings = $this->database->ports_mappings;
|
|
$this->isPublic = $this->database->is_public;
|
|
$this->publicPort = $this->database->public_port;
|
|
$this->publicPortTimeout = $this->database->public_port_timeout;
|
|
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
|
|
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
|
|
$this->enableSsl = $this->database->enable_ssl;
|
|
$this->sslMode = $this->database->ssl_mode;
|
|
$this->db_url = $this->database->internal_db_url;
|
|
$this->db_url_public = $this->database->external_db_url;
|
|
}
|
|
}
|
|
|
|
public function instantSaveAdvanced()
|
|
{
|
|
try {
|
|
$this->authorize('update', $this->database);
|
|
|
|
if (! $this->server->isLogDrainEnabled()) {
|
|
$this->isLogDrainEnabled = false;
|
|
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
|
|
|
|
return;
|
|
}
|
|
$this->syncData(true);
|
|
$this->dispatch('success', 'Database updated.');
|
|
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
|
|
} catch (Exception $e) {
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
public function submit()
|
|
{
|
|
try {
|
|
$this->authorize('update', $this->database);
|
|
|
|
if ($this->portsMappings) {
|
|
$this->portsMappings = str($this->portsMappings)->replace(' ', '')->trim()->toString();
|
|
}
|
|
if (str($this->publicPort)->isEmpty()) {
|
|
$this->publicPort = null;
|
|
}
|
|
if (str($this->mongoConf)->isEmpty()) {
|
|
$this->mongoConf = null;
|
|
}
|
|
$this->syncData(true);
|
|
$this->dispatch('success', 'Database updated.');
|
|
} catch (Exception $e) {
|
|
return handleError($e, $this);
|
|
} finally {
|
|
if (is_null($this->database->config_hash)) {
|
|
$this->database->isConfigurationChanged(true);
|
|
} else {
|
|
$this->dispatch('configurationChanged');
|
|
}
|
|
}
|
|
}
|
|
|
|
public function instantSave()
|
|
{
|
|
try {
|
|
$this->authorize('update', $this->database);
|
|
|
|
if ($this->isPublic && ! $this->publicPort) {
|
|
$this->dispatch('error', 'Public port is required.');
|
|
$this->isPublic = false;
|
|
|
|
return;
|
|
}
|
|
if ($this->isPublic && ! str($this->database->status)->startsWith('running')) {
|
|
$this->dispatch('error', 'Database must be started to be publicly accessible.');
|
|
$this->isPublic = false;
|
|
|
|
return;
|
|
}
|
|
$this->syncData(true);
|
|
if ($this->isPublic) {
|
|
StartDatabaseProxy::run($this->database);
|
|
$this->dispatch('success', 'Database is now publicly accessible.');
|
|
} else {
|
|
StopDatabaseProxy::run($this->database);
|
|
$this->dispatch('success', 'Database is no longer publicly accessible.');
|
|
}
|
|
} catch (\Throwable $e) {
|
|
$this->isPublic = ! $this->isPublic;
|
|
$this->syncData(true);
|
|
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
public function updatedSslMode()
|
|
{
|
|
$this->instantSaveSSL();
|
|
}
|
|
|
|
public function instantSaveSSL()
|
|
{
|
|
try {
|
|
$this->authorize('update', $this->database);
|
|
|
|
$this->syncData(true);
|
|
$this->dispatch('success', 'SSL configuration updated.');
|
|
} catch (Exception $e) {
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
public function regenerateSslCertificate()
|
|
{
|
|
try {
|
|
$this->authorize('update', $this->database);
|
|
|
|
$existingCert = $this->database->sslCertificates()->first();
|
|
|
|
if (! $existingCert) {
|
|
$this->dispatch('error', 'No existing SSL certificate found for this database.');
|
|
|
|
return;
|
|
}
|
|
|
|
$caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
|
|
|
|
if (! $caCert) {
|
|
$this->server->generateCaCertificate();
|
|
$caCert = $this->server->sslCertificates()->where('is_ca_certificate', true)->first();
|
|
}
|
|
|
|
if (! $caCert) {
|
|
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
|
|
|
|
return;
|
|
}
|
|
|
|
SslHelper::generateSslCertificate(
|
|
commonName: $existingCert->common_name,
|
|
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
|
|
resourceType: $existingCert->resource_type,
|
|
resourceId: $existingCert->resource_id,
|
|
serverId: $existingCert->server_id,
|
|
caCert: $caCert->ssl_certificate,
|
|
caKey: $caCert->ssl_private_key,
|
|
configurationDir: $existingCert->configuration_dir,
|
|
mountPath: $existingCert->mount_path,
|
|
isPemKeyFileRequired: true,
|
|
);
|
|
|
|
$this->dispatch('success', 'SSL certificates have been regenerated. Please restart the database for changes to take effect.');
|
|
} catch (Exception $e) {
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
public function refresh(): void
|
|
{
|
|
$this->database->refresh();
|
|
$this->syncData();
|
|
}
|
|
|
|
public function render()
|
|
{
|
|
return view('livewire.project.database.mongodb.general');
|
|
}
|
|
}
|