diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php index f087002da1..e21a2f1c81 100644 --- a/app/Console/Commands/Init.php +++ b/app/Console/Commands/Init.php @@ -257,10 +257,8 @@ class Init extends Command $database = StandalonePostgresql::withTrashed()->find(0); if ($database && $database->trashed()) { $database->restore(); - $scheduledBackup = ScheduledDatabaseBackup::find(0); - if (! $scheduledBackup) { + if (! $database->scheduledBackups()->exists()) { ScheduledDatabaseBackup::create([ - 'id' => 0, 'enabled' => true, 'save_s3' => false, 'frequency' => '0 0 * * *', diff --git a/app/Livewire/SettingsBackup.php b/app/Livewire/SettingsBackup.php index a498f9169b..43563e7c83 100644 --- a/app/Livewire/SettingsBackup.php +++ b/app/Livewire/SettingsBackup.php @@ -71,7 +71,7 @@ class SettingsBackup extends Component $this->backup->enabled = false; $this->backup->save(); } - $this->executions = $this->backup->executions; + $this->executions = $this->backup?->executions ?? []; } $this->settings = $settings; $this->s3s = $s3s; @@ -81,44 +81,46 @@ class SettingsBackup extends Component { try { $this->authorize('update', $this->settings); - $server = Server::findOrFail(0); - $out = instant_remote_process(['docker inspect coolify-db'], $server); - $envs = format_docker_envs_to_json($out); - $postgres_password = $envs['POSTGRES_PASSWORD']; - $postgres_user = $envs['POSTGRES_USER']; - $postgres_db = $envs['POSTGRES_DB']; - $this->database = new StandalonePostgresql; - $this->database->forceFill([ - 'id' => 0, - 'name' => 'coolify-db', - 'description' => 'Coolify database', - 'postgres_user' => $postgres_user, - 'postgres_password' => $postgres_password, - 'postgres_db' => $postgres_db, - 'status' => 'running', - 'destination_type' => StandaloneDocker::class, - 'destination_id' => 0, - ]); - $this->database->save(); - $this->backup = ScheduledDatabaseBackup::create([ - 'id' => 0, - 'enabled' => true, - 'save_s3' => false, - 'frequency' => '0 0 * * *', - 'database_id' => $this->database->id, - 'database_type' => StandalonePostgresql::class, - 'team_id' => currentTeam()->id, - ]); + $this->database = StandalonePostgresql::whereName('coolify-db')->first(); + if (! $this->database) { + $server = Server::findOrFail(0); + $out = instant_remote_process(['docker inspect coolify-db'], $server); + $envs = format_docker_envs_to_json($out); + $this->database = new StandalonePostgresql; + $this->database->forceFill([ + 'id' => 0, + 'name' => 'coolify-db', + 'description' => 'Coolify database', + 'postgres_user' => $envs['POSTGRES_USER'], + 'postgres_password' => $envs['POSTGRES_PASSWORD'], + 'postgres_db' => $envs['POSTGRES_DB'], + 'status' => 'running', + 'destination_type' => StandaloneDocker::class, + 'destination_id' => 0, + ]); + $this->database->save(); + } + $this->backup = $this->database->scheduledBackups()->first() + ?? ScheduledDatabaseBackup::create([ + 'enabled' => true, + 'save_s3' => false, + 'frequency' => '0 0 * * *', + 'database_id' => $this->database->id, + 'database_type' => StandalonePostgresql::class, + 'team_id' => 0, + ]); $this->database->refresh(); $this->backup->refresh(); - auditLog('ui.database.backup_schedule_created', [ - 'team_id' => $this->backup->team_id, - 'database_uuid' => $this->database->uuid, - 'database_name' => $this->database->name, - 'backup_uuid' => $this->backup->uuid, - 'frequency' => $this->backup->frequency, - 'save_s3' => (bool) $this->backup->save_s3, - ]); + if ($this->backup->wasRecentlyCreated) { + auditLog('ui.database.backup_schedule_created', [ + 'team_id' => $this->backup->team_id, + 'database_uuid' => $this->database->uuid, + 'database_name' => $this->database->name, + 'backup_uuid' => $this->backup->uuid, + 'frequency' => $this->backup->frequency, + 'save_s3' => (bool) $this->backup->save_s3, + ]); + } $this->s3s = S3Storage::whereTeamId(0)->get(); $this->uuid = $this->database->uuid; diff --git a/resources/js/copy-button.js b/resources/js/copy-button.js index 0ce8d5d67d..b62eaa120e 100644 --- a/resources/js/copy-button.js +++ b/resources/js/copy-button.js @@ -12,15 +12,22 @@ export function initializeCopyButtonComponent() { await navigator.clipboard.writeText(value); } else { // Deprecated, but the only copy path on plain http (non-secure contexts). + // The textarea goes next to the button, not into : a modal's x-trap + // pulls focus back from nodes outside it, which clears the selection. + const previousFocus = document.activeElement; const textarea = document.createElement('textarea'); textarea.value = value; textarea.setAttribute('readonly', ''); + textarea.setAttribute('tabindex', '-1'); textarea.style.position = 'fixed'; textarea.style.left = '-9999px'; - document.body.appendChild(textarea); + this.$el.after(textarea); + textarea.focus({ preventScroll: true }); textarea.select(); + textarea.setSelectionRange(0, textarea.value.length); const ok = document.execCommand('copy'); - document.body.removeChild(textarea); + textarea.remove(); + previousFocus?.focus?.({ preventScroll: true }); if (!ok) { throw new Error('Copy command was rejected.'); } diff --git a/resources/views/livewire/project/shared/cloudflare-autoconfigure.blade.php b/resources/views/livewire/project/shared/cloudflare-autoconfigure.blade.php index 8d102ff47f..9b565c4e98 100644 --- a/resources/views/livewire/project/shared/cloudflare-autoconfigure.blade.php +++ b/resources/views/livewire/project/shared/cloudflare-autoconfigure.blade.php @@ -223,39 +223,15 @@ @if (filled($dnsCopyText)) -
+

{{ count($dnsHints) }} {{ count($dnsHints) === 1 ? 'entry' : 'entries' }} ยท BIND zone format

diff --git a/tests/Feature/SettingsBackupEmptyStateTest.php b/tests/Feature/SettingsBackupEmptyStateTest.php index 3811c850e3..7bdf94341e 100644 --- a/tests/Feature/SettingsBackupEmptyStateTest.php +++ b/tests/Feature/SettingsBackupEmptyStateTest.php @@ -2,11 +2,15 @@ use App\Livewire\SettingsBackup; use App\Models\InstanceSettings; +use App\Models\ScheduledDatabaseBackup; use App\Models\Server; +use App\Models\StandaloneDocker; +use App\Models\StandalonePostgresql; use App\Models\Team; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Blade; +use Illuminate\Support\Facades\Process; use Illuminate\Support\Once; use Livewire\Livewire; @@ -84,3 +88,76 @@ test('instance backup settings show a configure button when backup is not set up ->assertSee('Configure backup') ->assertSeeHtml('wire:click="addCoolifyDatabase"'); }); + +function settingsBackupInstanceDatabaseWithoutSchedule(): StandalonePostgresql +{ + Server::flushIdentityMap(); + $rootTeam = Team::find(0) ?? Team::factory()->create(['id' => 0]); + $server = Server::factory()->create([ + 'id' => 0, + 'team_id' => $rootTeam->id, + 'ip' => '127.0.0.1', + ]); + $server->settings()->update([ + 'is_reachable' => true, + 'is_usable' => true, + 'force_disabled' => false, + ]); + StandaloneDocker::query()->where('server_id', $server->id)->firstOrFail()->forceFill(['id' => 0])->save(); + InstanceSettings::forceCreate(['id' => 0]); + Once::flush(); + + $database = new StandalonePostgresql; + $database->forceFill([ + 'id' => 0, + 'name' => 'coolify-db', + 'description' => 'Coolify database', + 'postgres_user' => 'coolify', + 'postgres_password' => 'password', + 'postgres_db' => 'coolify', + 'status' => 'running', + 'destination_type' => StandaloneDocker::class, + 'destination_id' => 0, + ]); + $database->save(); + + $user = User::factory()->create(); + $rootTeam->members()->attach($user->id, ['role' => 'admin']); + test()->actingAs($user); + session(['currentTeam' => ['id' => $rootTeam->id]]); + + return $database; +} + +test('instance backup settings render when the instance database has no backup schedule', function () { + settingsBackupInstanceDatabaseWithoutSchedule(); + + Livewire::test(SettingsBackup::class) + ->assertOk() + ->assertSet('executions', []) + ->assertSee('Backup is not configured') + ->assertSee('Configure backup'); +}); + +test('configure backup creates only the missing schedule for an existing instance database', function () { + $database = settingsBackupInstanceDatabaseWithoutSchedule(); + $otherTeam = Team::factory()->create(); + auth()->user()->teams()->attach($otherTeam->id, ['role' => 'owner']); + session(['currentTeam' => ['id' => $otherTeam->id]]); + Process::fake(); + + Livewire::test(SettingsBackup::class) + ->call('addCoolifyDatabase') + ->assertHasNoErrors() + ->assertSee('Instance database'); + + Process::assertNothingRan(); + $backup = $database->scheduledBackups()->sole(); + + expect(StandalonePostgresql::query()->where('name', 'coolify-db')->count())->toBe(1) + ->and($backup->id)->not->toBe(0) + ->and($backup->team_id)->toBe(0) + ->and($backup->enabled)->toBeTruthy() + ->and($backup->frequency)->toBe('0 0 * * *') + ->and(ScheduledDatabaseBackup::query()->count())->toBe(1); +});