diff --git a/app/Actions/Server/CreateServerLog.php b/app/Actions/Server/CreateServerLog.php index 9f63b31..d973ea7 100755 --- a/app/Actions/Server/CreateServerLog.php +++ b/app/Actions/Server/CreateServerLog.php @@ -3,7 +3,6 @@ namespace App\Actions\Server; use App\Models\Server; -use Illuminate\Support\Facades\Validator; use Illuminate\Validation\ValidationException; class CreateServerLog @@ -13,8 +12,6 @@ class CreateServerLog */ public function create(Server $server, array $input): void { - $this->validate($input); - $server->logs()->create([ 'is_remote' => true, 'name' => $input['path'], @@ -23,13 +20,10 @@ public function create(Server $server, array $input): void ]); } - /** - * @throws ValidationException - */ - protected function validate(array $input): void + public static function rules(): array { - Validator::make($input, [ + return [ 'path' => 'required', - ])->validate(); + ]; } } diff --git a/app/Helpers/SSH.php b/app/Helpers/SSH.php index 6322894..98f9d3b 100755 --- a/app/Helpers/SSH.php +++ b/app/Helpers/SSH.php @@ -155,9 +155,24 @@ public function upload(string $local, string $remote): void if (! $this->connection) { $this->connect(true); } + $this->connection->put($remote, $local, SFTP::SOURCE_LOCAL_FILE); } + /** + * @throws Throwable + */ + public function download(string $local, string $remote): void + { + $this->log = null; + + if (! $this->connection) { + $this->connect(true); + } + + $this->connection->get($remote, $local, SFTP::SOURCE_LOCAL_FILE); + } + /** * @throws Exception */ diff --git a/app/Models/ServerLog.php b/app/Models/ServerLog.php index 730357a..b9b339f 100755 --- a/app/Models/ServerLog.php +++ b/app/Models/ServerLog.php @@ -5,10 +5,12 @@ use Exception; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; use Symfony\Component\HttpFoundation\StreamedResponse; +use Throwable; /** * @property int $server_id @@ -44,12 +46,14 @@ public static function boot(): void parent::boot(); static::deleting(function (ServerLog $log) { - try { - if (Storage::disk($log->disk)->exists($log->name)) { - Storage::disk($log->disk)->delete($log->name); + if ($log->is_remote) { + try { + if (Storage::disk($log->disk)->exists($log->name)) { + Storage::disk($log->disk)->delete($log->name); + } + } catch (Exception $e) { + Log::error($e->getMessage(), ['exception' => $e]); } - } catch (Exception $e) { - Log::error($e->getMessage(), ['exception' => $e]); } }); } @@ -69,8 +73,28 @@ public function site(): BelongsTo return $this->belongsTo(Site::class); } + /** + * @throws Throwable + */ public function download(): StreamedResponse { + if ($this->is_remote) { + $tmpName = $this->server->id.'-'.strtotime('now').'-'.$this->type.'.log'; + $tmpPath = Storage::disk('local')->path($tmpName); + + $this->server->ssh()->download($tmpPath, $this->name); + + dispatch(function () use ($tmpPath) { + if (File::exists($tmpPath)) { + File::delete($tmpPath); + } + }) + ->delay(now()->addMinutes(5)) + ->onQueue('default'); + + return Storage::disk('local')->download($tmpName, str($this->name)->afterLast('/')); + } + return Storage::disk($this->disk)->download($this->name); } diff --git a/app/Web/Pages/Servers/Databases/Users.php b/app/Web/Pages/Servers/Databases/Users.php index a250e1d..ba70042 100644 --- a/app/Web/Pages/Servers/Databases/Users.php +++ b/app/Web/Pages/Servers/Databases/Users.php @@ -30,7 +30,7 @@ public function mount(): void protected function getHeaderActions(): array { return [ - Action::make('Create User') + Action::make('create') ->icon('heroicon-o-plus') ->modalWidth(MaxWidth::Large) ->authorize(fn () => auth()->user()?->can('create', [DatabaseUser::class, $this->server])) diff --git a/app/Web/Pages/Servers/Logs/Index.php b/app/Web/Pages/Servers/Logs/Index.php index efef5a2..f8d46b0 100644 --- a/app/Web/Pages/Servers/Logs/Index.php +++ b/app/Web/Pages/Servers/Logs/Index.php @@ -3,11 +3,14 @@ namespace App\Web\Pages\Servers\Logs; use App\Models\ServerLog; +use App\Web\Contracts\HasSecondSubNav; use App\Web\Pages\Servers\Logs\Widgets\LogsList; use App\Web\Pages\Servers\Page; -class Index extends Page +class Index extends Page implements HasSecondSubNav { + use Traits\Navigation; + protected static ?string $slug = 'servers/{server}/logs'; protected static ?string $title = 'Logs'; diff --git a/app/Web/Pages/Servers/Logs/RemoteLogs.php b/app/Web/Pages/Servers/Logs/RemoteLogs.php new file mode 100644 index 0000000..0bae179 --- /dev/null +++ b/app/Web/Pages/Servers/Logs/RemoteLogs.php @@ -0,0 +1,54 @@ +authorize('viewAny', [ServerLog::class, $this->server]); + } + + public function getWidgets(): array + { + return [ + [LogsList::class, ['server' => $this->server, 'remote' => true]], + ]; + } + + protected function getHeaderActions(): array + { + return [ + Action::make('create') + ->icon('heroicon-o-plus') + ->modalWidth(MaxWidth::Large) + ->authorize(fn () => auth()->user()?->can('create', [ServerLog::class, $this->server])) + ->form([ + TextInput::make('path') + ->helperText('The full path of the log file on the server') + ->rules(fn (callable $get) => CreateServerLog::rules()['path']), + ]) + ->modalSubmitActionLabel('Create') + ->action(function (array $data) { + app(CreateServerLog::class)->create($this->server, $data); + + $this->dispatch('$refresh'); + }), + ]; + } +} diff --git a/app/Web/Pages/Servers/Logs/Traits/Navigation.php b/app/Web/Pages/Servers/Logs/Traits/Navigation.php new file mode 100644 index 0000000..6c72bb4 --- /dev/null +++ b/app/Web/Pages/Servers/Logs/Traits/Navigation.php @@ -0,0 +1,34 @@ +user()->can('viewAny', [ServerLog::class, $this->server])) { + $items[] = NavigationItem::make(Index::getNavigationLabel()) + ->icon('heroicon-o-square-3-stack-3d') + ->isActiveWhen(fn () => request()->routeIs(Index::getRouteName())) + ->url(Index::getUrl(parameters: ['server' => $this->server])); + + $items[] = NavigationItem::make(RemoteLogs::getNavigationLabel()) + ->icon('heroicon-o-wifi') + ->isActiveWhen(fn () => request()->routeIs(RemoteLogs::getRouteName())) + ->url(RemoteLogs::getUrl(parameters: ['server' => $this->server])); + } + + return [ + NavigationGroup::make() + ->items($items), + ]; + } +} diff --git a/app/Web/Pages/Servers/Logs/Widgets/LogsList.php b/app/Web/Pages/Servers/Logs/Widgets/LogsList.php index 5523fa1..7649173 100644 --- a/app/Web/Pages/Servers/Logs/Widgets/LogsList.php +++ b/app/Web/Pages/Servers/Logs/Widgets/LogsList.php @@ -26,6 +26,8 @@ class LogsList extends Widget public ?string $label = ''; + public bool $remote = false; + protected $listeners = ['$refresh']; protected function getTableQuery(): Builder @@ -36,7 +38,8 @@ protected function getTableQuery(): Builder if ($this->site) { $query->where('site_id', $this->site->id); } - }); + }) + ->where('is_remote', $this->remote); } protected function getTableColumns(): array diff --git a/config/app.php b/config/app.php index 560e364..80f17a5 100644 --- a/config/app.php +++ b/config/app.php @@ -185,6 +185,7 @@ /* * Package Service Providers... */ + Illuminate\Concurrency\ConcurrencyServiceProvider::class, /* * Application Service Providers... diff --git a/tests/Feature/DatabaseUserTest.php b/tests/Feature/DatabaseUserTest.php index 969e25d..cdbbb85 100644 --- a/tests/Feature/DatabaseUserTest.php +++ b/tests/Feature/DatabaseUserTest.php @@ -5,7 +5,10 @@ use App\Enums\DatabaseUserStatus; use App\Facades\SSH; use App\Models\DatabaseUser; +use App\Web\Pages\Servers\Databases\Users; +use App\Web\Pages\Servers\Databases\Widgets\DatabaseUsersList; use Illuminate\Foundation\Testing\RefreshDatabase; +use Livewire\Livewire; use Tests\TestCase; class DatabaseUserTest extends TestCase @@ -18,10 +21,14 @@ public function test_create_database_user(): void SSH::fake(); - $this->post(route('servers.databases.users.store', $this->server), [ - 'username' => 'user', - 'password' => 'password', - ])->assertSessionDoesntHaveErrors(); + Livewire::test(Users::class, [ + 'server' => $this->server, + ]) + ->callAction('create', [ + 'username' => 'user', + 'password' => 'password', + ]) + ->assertSuccessful(); $this->assertDatabaseHas('database_users', [ 'username' => 'user', @@ -35,12 +42,16 @@ public function test_create_database_user_with_remote(): void SSH::fake(); - $this->post(route('servers.databases.users.store', $this->server), [ - 'username' => 'user', - 'password' => 'password', - 'remote' => 'on', - 'host' => '%', - ])->assertSessionDoesntHaveErrors(); + Livewire::test(Users::class, [ + 'server' => $this->server, + ]) + ->callAction('create', [ + 'username' => 'user', + 'password' => 'password', + 'remote' => true, + 'host' => '%', + ]) + ->assertSuccessful(); $this->assertDatabaseHas('database_users', [ 'username' => 'user', @@ -57,7 +68,11 @@ public function test_see_database_users_list(): void 'server_id' => $this->server, ]); - $this->get(route('servers.databases', $this->server)) + $this->get( + Users::getUrl([ + 'server' => $this->server, + ]) + ) ->assertSuccessful() ->assertSee($databaseUser->username); } @@ -72,8 +87,11 @@ public function test_delete_database_user(): void 'server_id' => $this->server, ]); - $this->delete(route('servers.databases.users.destroy', [$this->server, $databaseUser])) - ->assertSessionDoesntHaveErrors(); + Livewire::test(DatabaseUsersList::class, [ + 'server' => $this->server, + ]) + ->callTableAction('delete', $databaseUser->id) + ->assertSuccessful(); $this->assertDatabaseMissing('database_users', [ 'id' => $databaseUser->id, @@ -90,11 +108,13 @@ public function test_unlink_database(): void 'server_id' => $this->server, ]); - $this->post(route('servers.databases.users.link', [ + Livewire::test(DatabaseUsersList::class, [ 'server' => $this->server, - 'databaseUser' => $databaseUser, - ]), []) - ->assertSessionDoesntHaveErrors(); + ]) + ->callTableAction('link', $databaseUser->id, [ + 'databases' => [], + ]) + ->assertSuccessful(); $this->assertDatabaseHas('database_users', [ 'username' => $databaseUser->username, diff --git a/tests/Feature/FirewallTest.php b/tests/Feature/FirewallTest.php index 068fac1..c5c96a0 100644 --- a/tests/Feature/FirewallTest.php +++ b/tests/Feature/FirewallTest.php @@ -5,7 +5,10 @@ use App\Enums\FirewallRuleStatus; use App\Facades\SSH; use App\Models\FirewallRule; +use App\Web\Pages\Servers\Firewall\Index; +use App\Web\Pages\Servers\Firewall\Widgets\RulesList; use Illuminate\Foundation\Testing\RefreshDatabase; +use Livewire\Livewire; use Tests\TestCase; class FirewallTest extends TestCase @@ -18,13 +21,17 @@ public function test_create_firewall_rule(): void $this->actingAs($this->user); - $this->post(route('servers.firewall.store', $this->server), [ - 'type' => 'allow', - 'protocol' => 'tcp', - 'port' => '1234', - 'source' => '0.0.0.0', - 'mask' => '0', - ])->assertSessionDoesntHaveErrors(); + Livewire::test(Index::class, [ + 'server' => $this->server, + ]) + ->callAction('create', [ + 'type' => 'allow', + 'protocol' => 'tcp', + 'port' => '1234', + 'source' => '0.0.0.0', + 'mask' => '0', + ]) + ->assertSuccessful(); $this->assertDatabaseHas('firewall_rules', [ 'port' => '1234', @@ -40,7 +47,7 @@ public function test_see_firewall_rules(): void 'server_id' => $this->server->id, ]); - $this->get(route('servers.firewall', $this->server)) + $this->get(Index::getUrl(['server' => $this->server])) ->assertSuccessful() ->assertSee($rule->source) ->assertSee($rule->port); @@ -56,10 +63,11 @@ public function test_delete_firewall_rule(): void 'server_id' => $this->server->id, ]); - $this->delete(route('servers.firewall.destroy', [ + Livewire::test(RulesList::class, [ 'server' => $this->server, - 'firewallRule' => $rule, - ]))->assertSessionDoesntHaveErrors(); + ]) + ->callTableAction('delete', $rule->id) + ->assertSuccessful(); $this->assertDatabaseMissing('firewall_rules', [ 'id' => $rule->id, diff --git a/tests/Feature/LogsTest.php b/tests/Feature/LogsTest.php index a22996c..487d5c2 100644 --- a/tests/Feature/LogsTest.php +++ b/tests/Feature/LogsTest.php @@ -3,7 +3,10 @@ namespace Tests\Feature; use App\Models\ServerLog; +use App\Web\Pages\Servers\Logs\Index; +use App\Web\Pages\Servers\Logs\RemoteLogs; use Illuminate\Foundation\Testing\RefreshDatabase; +use Livewire\Livewire; use Tests\TestCase; class LogsTest extends TestCase @@ -19,37 +22,36 @@ public function test_see_logs() 'server_id' => $this->server->id, ]); - $this->get(route('servers.logs', $this->server)) + $this->get(Index::getUrl(['server' => $this->server])) ->assertSuccessful() - ->assertSeeText($log->type); + ->assertSee($log->name); } public function test_see_logs_remote() { $this->actingAs($this->user); - /** @var ServerLog $log */ - $log = ServerLog::factory()->create([ + ServerLog::factory()->create([ 'server_id' => $this->server->id, 'is_remote' => true, 'type' => 'remote', 'name' => 'see-remote-log', ]); - $this->get(route('servers.logs.remote', $this->server)) + $this->get(RemoteLogs::getUrl(['server' => $this->server])) ->assertSuccessful() - ->assertSeeText('see-remote-log'); + ->assertSee('see-remote-log'); } public function test_create_remote_log() { $this->actingAs($this->user); - $this->post(route('servers.logs.remote.store', [ - 'server' => $this->server->id, - ]), [ - 'path' => 'test-path', - ])->assertOk(); + Livewire::test(RemoteLogs::class, ['server' => $this->server]) + ->callAction('create', [ + 'path' => 'test-path', + ]) + ->assertSuccessful(); $this->assertDatabaseHas('server_logs', [ 'is_remote' => true,