mirror of
https://github.com/pelican-dev/panel.git
synced 2025-06-18 08:31:08 +02:00
Compare commits
No commits in common. "main" and "v1.0.0-beta21" have entirely different histories.
main
...
v1.0.0-bet
@ -18,17 +18,6 @@ class QueueWorkerServiceCommand extends Command
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
if (@file_exists('/.dockerenv')) {
|
||||
$result = Process::run('supervisorctl restart queue-worker');
|
||||
if ($result->failed()) {
|
||||
$this->error('Error restarting service: ' . $result->errorOutput());
|
||||
|
||||
return;
|
||||
}
|
||||
$this->line('Queue worker service file updated successfully.');
|
||||
|
||||
return;
|
||||
}
|
||||
$serviceName = $this->option('service-name') ?? $this->ask('Queue worker service name', 'pelican-queue');
|
||||
$path = '/etc/systemd/system/' . $serviceName . '.service';
|
||||
|
||||
|
@ -24,7 +24,6 @@ class MakeNodeCommand extends Command
|
||||
{--overallocateCpu= : Enter the amount of cpu to overallocate (% or -1 to overallocate the maximum).}
|
||||
{--uploadSize= : Enter the maximum upload filesize.}
|
||||
{--daemonListeningPort= : Enter the daemon listening port.}
|
||||
{--daemonConnectingPort= : Enter the daemon connecting port.}
|
||||
{--daemonSFTPPort= : Enter the daemon SFTP listening port.}
|
||||
{--daemonSFTPAlias= : Enter the daemon SFTP alias.}
|
||||
{--daemonBase= : Enter the base folder.}';
|
||||
@ -58,7 +57,6 @@ class MakeNodeCommand extends Command
|
||||
$data['cpu_overallocate'] = $this->option('overallocateCpu') ?? $this->ask(trans('commands.make_node.cpu_overallocate'), '-1');
|
||||
$data['upload_size'] = $this->option('uploadSize') ?? $this->ask(trans('commands.make_node.upload_size'), '256');
|
||||
$data['daemon_listen'] = $this->option('daemonListeningPort') ?? $this->ask(trans('commands.make_node.daemonListen'), '8080');
|
||||
$data['daemon_connect'] = $this->option('daemonConnectingPort') ?? $this->ask(trans('commands.make_node.daemonConnect'), '8080');
|
||||
$data['daemon_sftp'] = $this->option('daemonSFTPPort') ?? $this->ask(trans('commands.make_node.daemonSFTP'), '2022');
|
||||
$data['daemon_sftp_alias'] = $this->option('daemonSFTPAlias') ?? $this->ask(trans('commands.make_node.daemonSFTPAlias'), '');
|
||||
$data['daemon_base'] = $this->option('daemonBase') ?? $this->ask(trans('commands.make_node.daemonBase'), '/var/lib/pelican/volumes');
|
||||
|
@ -7,6 +7,7 @@ use App\Console\Commands\Maintenance\CleanServiceBackupFilesCommand;
|
||||
use App\Console\Commands\Maintenance\PruneImagesCommand;
|
||||
use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand;
|
||||
use App\Console\Commands\Schedule\ProcessRunnableCommand;
|
||||
use App\Jobs\NodeStatistics;
|
||||
use App\Models\ActivityLog;
|
||||
use App\Models\Webhook;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
@ -43,6 +44,8 @@ class Kernel extends ConsoleKernel
|
||||
$schedule->command(PruneImagesCommand::class)->daily();
|
||||
$schedule->command(CheckEggUpdatesCommand::class)->hourly();
|
||||
|
||||
$schedule->job(new NodeStatistics())->everyFiveSeconds()->withoutOverlapping();
|
||||
|
||||
if (config('backups.prune_age')) {
|
||||
// Every 30 minutes, run the backup pruning command so that any abandoned backups can be deleted.
|
||||
$schedule->command(PruneOrphanedBackupsCommand::class)->everyThirtyMinutes();
|
||||
|
@ -88,7 +88,7 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
|
||||
|
||||
public function isStartable(): bool
|
||||
{
|
||||
return !in_array($this, [ContainerStatus::Running, ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting, ContainerStatus::Missing]);
|
||||
return !in_array($this, [ContainerStatus::Running, ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting]);
|
||||
}
|
||||
|
||||
public function isRestartable(): bool
|
||||
@ -97,16 +97,18 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
|
||||
return true;
|
||||
}
|
||||
|
||||
return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Missing]);
|
||||
return !in_array($this, [ContainerStatus::Offline]);
|
||||
}
|
||||
|
||||
public function isStoppable(): bool
|
||||
{
|
||||
return !in_array($this, [ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting, ContainerStatus::Exited, ContainerStatus::Offline, ContainerStatus::Missing]);
|
||||
return !in_array($this, [ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting, ContainerStatus::Exited, ContainerStatus::Offline]);
|
||||
}
|
||||
|
||||
public function isKillable(): bool
|
||||
{
|
||||
return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Running, ContainerStatus::Exited, ContainerStatus::Missing]);
|
||||
// [ContainerStatus::Restarting, ContainerStatus::Removing, ContainerStatus::Dead, ContainerStatus::Created]
|
||||
|
||||
return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Running, ContainerStatus::Exited]);
|
||||
}
|
||||
}
|
||||
|
@ -27,16 +27,8 @@ enum ServerState: string implements HasColor, HasIcon, HasLabel
|
||||
};
|
||||
}
|
||||
|
||||
public function getColor(bool $hex = false): string
|
||||
public function getColor(): string
|
||||
{
|
||||
if ($hex) {
|
||||
return match ($this) {
|
||||
self::Normal, self::Installing, self::RestoringBackup => '#2563EB',
|
||||
self::Suspended => '#D97706',
|
||||
self::InstallFailed, self::ReinstallFailed => '#EF4444',
|
||||
};
|
||||
}
|
||||
|
||||
return match ($this) {
|
||||
self::Normal => 'primary',
|
||||
self::Installing => 'primary',
|
||||
|
11
app/Events/Auth/DirectLogin.php
Normal file
11
app/Events/Auth/DirectLogin.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Events\Event;
|
||||
|
||||
class DirectLogin extends Event
|
||||
{
|
||||
public function __construct(public User $user, public bool $remember) {}
|
||||
}
|
16
app/Events/Auth/FailedPasswordReset.php
Normal file
16
app/Events/Auth/FailedPasswordReset.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Auth;
|
||||
|
||||
use App\Events\Event;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class FailedPasswordReset extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public string $ip, public string $email) {}
|
||||
}
|
@ -630,6 +630,7 @@ class Settings extends Page implements HasForms
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_INSTALL_NOTIFICATION', (bool) $state))
|
||||
->default(env('PANEL_SEND_INSTALL_NOTIFICATION', config('panel.email.send_install_notification'))),
|
||||
@ -640,6 +641,7 @@ class Settings extends Page implements HasForms
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_REINSTALL_NOTIFICATION', (bool) $state))
|
||||
->default(env('PANEL_SEND_REINSTALL_NOTIFICATION', config('panel.email.send_reinstall_notification'))),
|
||||
|
@ -79,7 +79,7 @@ class ApiKeyResource extends Resource
|
||||
TextColumn::make('user.username')
|
||||
->label(trans('admin/apikey.table.created_by'))
|
||||
->icon('tabler-user')
|
||||
->url(fn (ApiKey $apiKey) => auth()->user()->can('update', $apiKey->user) ? EditUser::getUrl(['record' => $apiKey->user]) : null),
|
||||
->url(fn (ApiKey $apiKey) => auth()->user()->can('update user', $apiKey->user) ? EditUser::getUrl(['record' => $apiKey->user]) : null),
|
||||
])
|
||||
->actions([
|
||||
DeleteAction::make(),
|
||||
|
@ -164,10 +164,8 @@ class DatabaseHostResource extends Resource
|
||||
{
|
||||
$query = parent::getEloquentQuery();
|
||||
|
||||
return $query->where(function (Builder $query) {
|
||||
return $query->whereHas('nodes', function (Builder $query) {
|
||||
$query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'));
|
||||
})->orDoesntHave('nodes');
|
||||
});
|
||||
return $query->whereHas('nodes', function (Builder $query) {
|
||||
$query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'));
|
||||
})->orDoesntHave('nodes');
|
||||
}
|
||||
}
|
||||
|
@ -71,10 +71,10 @@ class DatabasesRelationManager extends RelationManager
|
||||
])
|
||||
->actions([
|
||||
DeleteAction::make()
|
||||
->authorize(fn (Database $database) => auth()->user()->can('delete', $database)),
|
||||
->authorize(fn (Database $database) => auth()->user()->can('delete database', $database)),
|
||||
ViewAction::make()
|
||||
->color('primary')
|
||||
->hidden(fn () => !auth()->user()->can('viewAny', Database::class)),
|
||||
->hidden(fn () => !auth()->user()->can('viewList database')),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ class MountResource extends Resource
|
||||
->badge()
|
||||
->icon(fn ($state) => $state ? 'tabler-writing-off' : 'tabler-writing')
|
||||
->color(fn ($state) => $state ? 'success' : 'warning')
|
||||
->formatStateUsing(fn ($state) => $state ? trans('admin/mount.toggles.read_only') : trans('admin/mount.toggles.writable')),
|
||||
->formatStateUsing(fn ($state) => $state ? trans('admin/mount.toggles.read_only') : trans('admin/mount.toggles.writeable')),
|
||||
])
|
||||
->actions([
|
||||
ViewAction::make()
|
||||
@ -176,10 +176,8 @@ class MountResource extends Resource
|
||||
{
|
||||
$query = parent::getEloquentQuery();
|
||||
|
||||
return $query->where(function (Builder $query) {
|
||||
return $query->whereHas('nodes', function (Builder $query) {
|
||||
$query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'));
|
||||
})->orDoesntHave('nodes');
|
||||
});
|
||||
return $query->whereHas('nodes', function (Builder $query) {
|
||||
$query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'));
|
||||
})->orDoesntHave('nodes');
|
||||
}
|
||||
}
|
||||
|
@ -124,10 +124,15 @@ class CreateNode extends CreateRecord
|
||||
'lg' => 1,
|
||||
]),
|
||||
|
||||
TextInput::make('daemon_connect')
|
||||
->columnSpan(1)
|
||||
->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port'))
|
||||
->helperText(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port_help') : trans('admin/node.port_help'))
|
||||
TextInput::make('daemon_listen')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->label(trans('admin/node.port'))
|
||||
->helperText(trans('admin/node.port_help'))
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(8080)
|
||||
@ -188,21 +193,7 @@ class CreateNode extends CreateRecord
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
$set('scheme', $state === 'http' ? 'http' : 'https');
|
||||
$set('behind_proxy', $state === 'https_proxy');
|
||||
|
||||
$set('daemon_connect', $state === 'https_proxy' ? 443 : 8080);
|
||||
$set('daemon_listen', 8080);
|
||||
}),
|
||||
|
||||
TextInput::make('daemon_listen')
|
||||
->columnSpan(1)
|
||||
->label(trans('admin/node.listen_port'))
|
||||
->helperText(trans('admin/node.listen_port_help'))
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(8080)
|
||||
->required()
|
||||
->integer()
|
||||
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
|
||||
]),
|
||||
Step::make('advanced')
|
||||
->label(trans('admin/node.tabs.advanced_settings'))
|
||||
@ -418,13 +409,4 @@ class CreateNode extends CreateRecord
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
if (!$data['behind_proxy']) {
|
||||
$data['daemon_listen'] = $data['daemon_connect'];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
@ -181,10 +181,10 @@ class EditNode extends EditRecord
|
||||
false => 'danger',
|
||||
])
|
||||
->columnSpan(1),
|
||||
TextInput::make('daemon_connect')
|
||||
TextInput::make('daemon_listen')
|
||||
->columnSpan(1)
|
||||
->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port'))
|
||||
->helperText(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port_help') : trans('admin/node.port_help'))
|
||||
->label(trans('admin/node.port'))
|
||||
->helperText(trans('admin/node.port_help'))
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(8080)
|
||||
@ -239,20 +239,7 @@ class EditNode extends EditRecord
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
$set('scheme', $state === 'http' ? 'http' : 'https');
|
||||
$set('behind_proxy', $state === 'https_proxy');
|
||||
|
||||
$set('daemon_connect', $state === 'https_proxy' ? 443 : 8080);
|
||||
$set('daemon_listen', 8080);
|
||||
}),
|
||||
TextInput::make('daemon_listen')
|
||||
->columnSpan(1)
|
||||
->label(trans('admin/node.listen_port'))
|
||||
->helperText(trans('admin/node.listen_port_help'))
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(8080)
|
||||
->required()
|
||||
->integer()
|
||||
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
|
||||
]),
|
||||
Tab::make('adv')
|
||||
->label(trans('admin/node.tabs.advanced_settings'))
|
||||
@ -640,15 +627,6 @@ class EditNode extends EditRecord
|
||||
];
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
if (!$data['behind_proxy']) {
|
||||
$data['daemon_listen'] = $data['daemon_connect'];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
$this->fillForm();
|
||||
|
@ -97,7 +97,7 @@ class AllocationsRelationManager extends RelationManager
|
||||
])
|
||||
->groupedBulkActions([
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('update', $this->getOwnerRecord())),
|
||||
->authorize(fn () => auth()->user()->can('update node')),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace App\Filament\Admin\Resources\NodeResource\Widgets;
|
||||
|
||||
use App\Models\Node;
|
||||
use Carbon\Carbon;
|
||||
use Filament\Support\RawJs;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Support\Number;
|
||||
@ -15,34 +16,22 @@ class NodeCpuChart extends ChartWidget
|
||||
|
||||
public Node $node;
|
||||
|
||||
/**
|
||||
* @var array<int, array{cpu: string, timestamp: string}>
|
||||
*/
|
||||
protected array $cpuHistory = [];
|
||||
|
||||
protected int $threads = 0;
|
||||
|
||||
protected function getData(): array
|
||||
{
|
||||
$sessionKey = "node_stats.{$this->node->id}";
|
||||
$threads = $this->node->systemInformation()['cpu_count'] ?? 0;
|
||||
|
||||
$data = $this->node->statistics();
|
||||
|
||||
$this->threads = session("{$sessionKey}.threads", $this->node->systemInformation()['cpu_count'] ?? 0);
|
||||
|
||||
$this->cpuHistory = session("{$sessionKey}.cpu_history", []);
|
||||
$this->cpuHistory[] = [
|
||||
'cpu' => round($data['cpu_percent'] * $this->threads, 2),
|
||||
'timestamp' => now(auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
|
||||
];
|
||||
|
||||
$this->cpuHistory = array_slice($this->cpuHistory, -60);
|
||||
session()->put("{$sessionKey}.cpu_history", $this->cpuHistory);
|
||||
$cpu = collect(cache()->get("nodes.{$this->node->id}.cpu_percent"))
|
||||
->slice(-10)
|
||||
->map(fn ($value, $key) => [
|
||||
'cpu' => round($value * $threads, 2),
|
||||
'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
|
||||
])
|
||||
->all();
|
||||
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'data' => array_column($this->cpuHistory, 'cpu'),
|
||||
'data' => array_column($cpu, 'cpu'),
|
||||
'backgroundColor' => [
|
||||
'rgba(96, 165, 250, 0.3)',
|
||||
],
|
||||
@ -50,7 +39,7 @@ class NodeCpuChart extends ChartWidget
|
||||
'fill' => true,
|
||||
],
|
||||
],
|
||||
'labels' => array_column($this->cpuHistory, 'timestamp'),
|
||||
'labels' => array_column($cpu, 'timestamp'),
|
||||
'locale' => auth()->user()->language ?? 'en',
|
||||
];
|
||||
}
|
||||
@ -80,10 +69,10 @@ class NodeCpuChart extends ChartWidget
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
$data = array_slice(end($this->cpuHistory), -60);
|
||||
$threads = $this->node->systemInformation()['cpu_count'] ?? 0;
|
||||
|
||||
$cpu = Number::format($data['cpu'], maxPrecision: 2, locale: auth()->user()->language);
|
||||
$max = Number::format($this->threads * 100, locale: auth()->user()->language);
|
||||
$cpu = Number::format(collect(cache()->get("nodes.{$this->node->id}.cpu_percent"))->last() * $threads, maxPrecision: 2, locale: auth()->user()->language);
|
||||
$max = Number::format($threads * 100, locale: auth()->user()->language);
|
||||
|
||||
return trans('admin/node.cpu_chart', ['cpu' => $cpu, 'max' => $max]);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace App\Filament\Admin\Resources\NodeResource\Widgets;
|
||||
|
||||
use App\Models\Node;
|
||||
use Carbon\Carbon;
|
||||
use Filament\Support\RawJs;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Support\Number;
|
||||
@ -15,36 +16,19 @@ class NodeMemoryChart extends ChartWidget
|
||||
|
||||
public Node $node;
|
||||
|
||||
/**
|
||||
* @var array<int, array{memory: string, timestamp: string}>
|
||||
*/
|
||||
protected array $memoryHistory = [];
|
||||
|
||||
protected int $totalMemory = 0;
|
||||
|
||||
protected function getData(): array
|
||||
{
|
||||
$sessionKey = "node_stats.{$this->node->id}";
|
||||
|
||||
$data = $this->node->statistics();
|
||||
|
||||
$this->totalMemory = session("{$sessionKey}.total_memory", $data['memory_total']);
|
||||
|
||||
$this->memoryHistory = session("{$sessionKey}.memory_history", []);
|
||||
$this->memoryHistory[] = [
|
||||
'memory' => round(config('panel.use_binary_prefix')
|
||||
? $data['memory_used'] / 1024 / 1024 / 1024
|
||||
: $data['memory_used'] / 1000 / 1000 / 1000, 2),
|
||||
'timestamp' => now(auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
|
||||
];
|
||||
|
||||
$this->memoryHistory = array_slice($this->memoryHistory, -60);
|
||||
session()->put("{$sessionKey}.memory_history", $this->memoryHistory);
|
||||
$memUsed = collect(cache()->get("nodes.{$this->node->id}.memory_used"))->slice(-10)
|
||||
->map(fn ($value, $key) => [
|
||||
'memory' => round(config('panel.use_binary_prefix') ? $value / 1024 / 1024 / 1024 : $value / 1000 / 1000 / 1000, 2),
|
||||
'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
|
||||
])
|
||||
->all();
|
||||
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'data' => array_column($this->memoryHistory, 'memory'),
|
||||
'data' => array_column($memUsed, 'memory'),
|
||||
'backgroundColor' => [
|
||||
'rgba(96, 165, 250, 0.3)',
|
||||
],
|
||||
@ -52,7 +36,7 @@ class NodeMemoryChart extends ChartWidget
|
||||
'fill' => true,
|
||||
],
|
||||
],
|
||||
'labels' => array_column($this->memoryHistory, 'timestamp'),
|
||||
'labels' => array_column($memUsed, 'timestamp'),
|
||||
'locale' => auth()->user()->language ?? 'en',
|
||||
];
|
||||
}
|
||||
@ -82,15 +66,16 @@ class NodeMemoryChart extends ChartWidget
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
$latestMemoryUsed = array_slice(end($this->memoryHistory), -60);
|
||||
$latestMemoryUsed = collect(cache()->get("nodes.{$this->node->id}.memory_used"))->last();
|
||||
$totalMemory = collect(cache()->get("nodes.{$this->node->id}.memory_total"))->last();
|
||||
|
||||
$used = config('panel.use_binary_prefix')
|
||||
? Number::format($latestMemoryUsed['memory'], maxPrecision: 2, locale: auth()->user()->language) .' GiB'
|
||||
: Number::format($latestMemoryUsed['memory'], maxPrecision: 2, locale: auth()->user()->language) . ' GB';
|
||||
? Number::format($latestMemoryUsed / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
|
||||
: Number::format($latestMemoryUsed / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
|
||||
|
||||
$total = config('panel.use_binary_prefix')
|
||||
? Number::format($this->totalMemory / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
|
||||
: Number::format($this->totalMemory / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
|
||||
? Number::format($totalMemory / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
|
||||
: Number::format($totalMemory / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
|
||||
|
||||
return trans('admin/node.memory_chart', ['used' => $used, 'total' => $total]);
|
||||
}
|
||||
|
@ -147,8 +147,6 @@ class RoleResource extends Resource
|
||||
*/
|
||||
private static function makeSection(string $model, array $options): Section
|
||||
{
|
||||
$model = ucwords($model);
|
||||
|
||||
$icon = null;
|
||||
|
||||
if (class_exists('\App\Filament\Admin\Resources\\' . $model . 'Resource')) {
|
||||
|
@ -79,6 +79,8 @@ class ServerResource extends Resource
|
||||
{
|
||||
$query = parent::getEloquentQuery();
|
||||
|
||||
return $query->whereIn('node_id', auth()->user()->accessibleNodes()->pluck('id'));
|
||||
return $query->whereHas('node', function (Builder $query) {
|
||||
$query->whereIn('id', auth()->user()->accessibleNodes()->pluck('id'));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -144,7 +144,6 @@ class CreateServer extends CreateRecord
|
||||
->relationship('user', 'username')
|
||||
->searchable(['username', 'email'])
|
||||
->getOptionLabelFromRecordUsing(fn (User $user) => "$user->username ($user->email)")
|
||||
->createOptionAction(fn (Action $action) => $action->authorize(fn () => auth()->user()->can('create', User::class)))
|
||||
->createOptionForm([
|
||||
TextInput::make('username')
|
||||
->label(trans('admin/user.username'))
|
||||
@ -206,7 +205,6 @@ class CreateServer extends CreateRecord
|
||||
->where('node_id', $get('node_id'))
|
||||
->whereNull('server_id'),
|
||||
)
|
||||
->createOptionAction(fn (Action $action) => $action->authorize(fn (Get $get) => auth()->user()->can('create', Node::find($get('node_id')))))
|
||||
->createOptionForm(function (Get $get) {
|
||||
$getPage = $get;
|
||||
|
||||
|
@ -686,7 +686,7 @@ class EditServer extends EditRecord
|
||||
ServerResource::getMountCheckboxList($get),
|
||||
]),
|
||||
Tab::make(trans('admin/server.databases'))
|
||||
->hidden(fn () => !auth()->user()->can('viewAny', Database::class))
|
||||
->hidden(fn () => !auth()->user()->can('viewList database'))
|
||||
->icon('tabler-database')
|
||||
->columns(4)
|
||||
->schema([
|
||||
@ -710,7 +710,7 @@ class EditServer extends EditRecord
|
||||
->hintAction(
|
||||
Action::make('Delete')
|
||||
->label(trans('filament-actions::delete.single.modal.actions.delete.label'))
|
||||
->authorize(fn (Database $database) => auth()->user()->can('delete', $database))
|
||||
->authorize(fn (Database $database) => auth()->user()->can('delete database', $database))
|
||||
->color('danger')
|
||||
->icon('tabler-trash')
|
||||
->requiresConfirmation()
|
||||
@ -763,7 +763,7 @@ class EditServer extends EditRecord
|
||||
->columnSpan(4),
|
||||
FormActions::make([
|
||||
Action::make('createDatabase')
|
||||
->authorize(fn () => auth()->user()->can('create', Database::class))
|
||||
->authorize(fn () => auth()->user()->can('create database'))
|
||||
->disabled(fn () => DatabaseHost::query()->count() < 1)
|
||||
->label(fn () => DatabaseHost::query()->count() < 1 ? trans('admin/server.no_db_hosts') : trans('admin/server.create_database'))
|
||||
->color(fn () => DatabaseHost::query()->count() < 1 ? 'danger' : 'primary')
|
||||
@ -1065,7 +1065,7 @@ class EditServer extends EditRecord
|
||||
}
|
||||
})
|
||||
->hidden(fn () => $canForceDelete)
|
||||
->authorize(fn (Server $server) => auth()->user()->can('delete', $server)),
|
||||
->authorize(fn (Server $server) => auth()->user()->can('delete server', $server)),
|
||||
Actions\Action::make('ForceDelete')
|
||||
->color('danger')
|
||||
->label(trans('filament-actions::force-delete.single.label'))
|
||||
@ -1082,7 +1082,7 @@ class EditServer extends EditRecord
|
||||
}
|
||||
})
|
||||
->visible(fn () => $canForceDelete)
|
||||
->authorize(fn (Server $server) => auth()->user()->can('delete', $server)),
|
||||
->authorize(fn (Server $server) => auth()->user()->can('delete server', $server)),
|
||||
Actions\Action::make('console')
|
||||
->label(trans('admin/server.console'))
|
||||
->icon('tabler-terminal')
|
||||
|
@ -68,13 +68,13 @@ class ListServers extends ListRecords
|
||||
->searchable(),
|
||||
SelectColumn::make('allocation_id')
|
||||
->label(trans('admin/server.primary_allocation'))
|
||||
->hidden(!auth()->user()->can('update server')) // TODO: update to policy check (fn (Server $server) --> $server is empty)
|
||||
->hidden(!auth()->user()->can('update server'))
|
||||
->options(fn (Server $server) => $server->allocations->mapWithKeys(fn ($allocation) => [$allocation->id => $allocation->address]))
|
||||
->selectablePlaceholder(false)
|
||||
->sortable(),
|
||||
TextColumn::make('allocation_id_readonly')
|
||||
->label(trans('admin/server.primary_allocation'))
|
||||
->hidden(auth()->user()->can('update server')) // TODO: update to policy check (fn (Server $server) --> $server is empty)
|
||||
->hidden(auth()->user()->can('update server'))
|
||||
->state(fn (Server $server) => $server->allocation->address),
|
||||
TextColumn::make('image')->hidden(),
|
||||
TextColumn::make('backups_count')
|
||||
|
@ -9,13 +9,12 @@ use App\Filament\Server\Pages\Console;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonPowerRepository;
|
||||
use AymanAlhattami\FilamentContextMenu\Columns\ContextMenuTextColumn;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Components\Tab;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Support\Enums\Alignment;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\ActionGroup;
|
||||
use Filament\Tables\Columns\Column;
|
||||
use Filament\Tables\Columns\ColumnGroup;
|
||||
use Filament\Tables\Columns\Layout\Stack;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
@ -39,73 +38,121 @@ class ListServers extends ListRecords
|
||||
$this->daemonPowerRepository = new DaemonPowerRepository();
|
||||
}
|
||||
|
||||
/** @return Stack[] */
|
||||
protected function gridColumns(): array
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return [
|
||||
Stack::make([
|
||||
ServerEntryColumn::make('server_entry')
|
||||
->searchable(['name']),
|
||||
]),
|
||||
];
|
||||
}
|
||||
$baseQuery = auth()->user()->accessibleServers();
|
||||
|
||||
/** @return Column[] */
|
||||
protected function tableColumns(): array
|
||||
{
|
||||
return [
|
||||
TextColumn::make('condition')
|
||||
->label('Status')
|
||||
$menuOptions = function (Server $server) {
|
||||
$status = $server->retrieveStatus();
|
||||
|
||||
return [
|
||||
Action::make('start')
|
||||
->color('primary')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_START, $server))
|
||||
->visible(fn () => $status->isStartable())
|
||||
->dispatch('powerAction', ['server' => $server, 'action' => 'start'])
|
||||
->icon('tabler-player-play-filled'),
|
||||
Action::make('restart')
|
||||
->color('gray')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_RESTART, $server))
|
||||
->visible(fn () => $status->isRestartable())
|
||||
->dispatch('powerAction', ['server' => $server, 'action' => 'restart'])
|
||||
->icon('tabler-refresh'),
|
||||
Action::make('stop')
|
||||
->color('danger')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
|
||||
->visible(fn () => $status->isStoppable())
|
||||
->dispatch('powerAction', ['server' => $server, 'action' => 'stop'])
|
||||
->icon('tabler-player-stop-filled'),
|
||||
Action::make('kill')
|
||||
->color('danger')
|
||||
->tooltip('This can result in data corruption and/or data loss!')
|
||||
->dispatch('powerAction', ['server' => $server, 'action' => 'kill'])
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
|
||||
->visible(fn () => $status->isKillable())
|
||||
->icon('tabler-alert-square'),
|
||||
];
|
||||
};
|
||||
|
||||
$viewOne = [
|
||||
ContextMenuTextColumn::make('condition')
|
||||
->label('')
|
||||
->default('unknown')
|
||||
->wrap()
|
||||
->badge()
|
||||
->alignCenter()
|
||||
->tooltip(fn (Server $server) => $server->formatResource('uptime', type: ServerResourceType::Time))
|
||||
->icon(fn (Server $server) => $server->condition->getIcon())
|
||||
->color(fn (Server $server) => $server->condition->getColor()),
|
||||
TextColumn::make('name')
|
||||
->label('Server')
|
||||
->description(fn (Server $server) => $server->description)
|
||||
->grow()
|
||||
->searchable(),
|
||||
TextColumn::make('allocation.address')
|
||||
->color(fn (Server $server) => $server->condition->getColor())
|
||||
->contextMenuActions($menuOptions)
|
||||
->enableContextMenu(fn (Server $server) => !$server->isInConflictState()),
|
||||
];
|
||||
|
||||
$viewTwo = [
|
||||
ContextMenuTextColumn::make('name')
|
||||
->label('')
|
||||
->size('md')
|
||||
->searchable()
|
||||
->contextMenuActions($menuOptions)
|
||||
->enableContextMenu(fn (Server $server) => !$server->isInConflictState()),
|
||||
ContextMenuTextColumn::make('allocation.address')
|
||||
->label('')
|
||||
->badge()
|
||||
->visibleFrom('md')
|
||||
->copyable(request()->isSecure()),
|
||||
->copyable(request()->isSecure())
|
||||
->contextMenuActions($menuOptions)
|
||||
->enableContextMenu(fn (Server $server) => !$server->isInConflictState()),
|
||||
];
|
||||
|
||||
$viewThree = [
|
||||
TextColumn::make('cpuUsage')
|
||||
->label('Resources')
|
||||
->label('')
|
||||
->icon('tabler-cpu')
|
||||
->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('cpu', limit: true, type: ServerResourceType::Percentage, precision: 0))
|
||||
->state(fn (Server $server) => $server->formatResource('cpu_absolute', type: ServerResourceType::Percentage))
|
||||
->color(fn (Server $server) => $this->getResourceColor($server, 'cpu')),
|
||||
TextColumn::make('memoryUsage')
|
||||
->label('')
|
||||
->icon('tabler-device-desktop-analytics')
|
||||
->icon('tabler-memory')
|
||||
->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('memory', limit: true))
|
||||
->state(fn (Server $server) => $server->formatResource('memory_bytes'))
|
||||
->color(fn (Server $server) => $this->getResourceColor($server, 'memory')),
|
||||
TextColumn::make('diskUsage')
|
||||
->label('')
|
||||
->icon('tabler-device-sd-card')
|
||||
->icon('tabler-device-floppy')
|
||||
->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('disk', limit: true))
|
||||
->state(fn (Server $server) => $server->formatResource('disk_bytes'))
|
||||
->color(fn (Server $server) => $this->getResourceColor($server, 'disk')),
|
||||
];
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
$baseQuery = auth()->user()->accessibleServers();
|
||||
|
||||
$usingGrid = (auth()->user()->getCustomization()['dashboard_layout'] ?? 'grid') === 'grid';
|
||||
|
||||
return $table
|
||||
->paginated(false)
|
||||
->query(fn () => $baseQuery)
|
||||
->poll('15s')
|
||||
->columns($usingGrid ? $this->gridColumns() : $this->tableColumns())
|
||||
->recordUrl(!$usingGrid ? (fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server)) : null)
|
||||
->actions(!$usingGrid ? ActionGroup::make(static::getPowerActions()) : [])
|
||||
->actionsAlignment(Alignment::Center->value)
|
||||
->contentGrid($usingGrid ? ['default' => 1, 'md' => 2] : null)
|
||||
->columns(
|
||||
(auth()->user()->getCustomization()['dashboard_layout'] ?? 'grid') === 'grid'
|
||||
? [
|
||||
Stack::make([
|
||||
ServerEntryColumn::make('server_entry')
|
||||
->searchable(['name']),
|
||||
]),
|
||||
]
|
||||
: [
|
||||
ColumnGroup::make('Status')
|
||||
->label('Status')
|
||||
->columns($viewOne),
|
||||
ColumnGroup::make('Server')
|
||||
->label('Servers')
|
||||
->columns($viewTwo),
|
||||
ColumnGroup::make('Resources')
|
||||
->label('Resources')
|
||||
->columns($viewThree),
|
||||
]
|
||||
)
|
||||
->recordUrl(fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server))
|
||||
->contentGrid([
|
||||
'default' => 1,
|
||||
'md' => 2,
|
||||
])
|
||||
->emptyStateIcon('tabler-brand-docker')
|
||||
->emptyStateDescription('')
|
||||
->emptyStateHeading(fn () => $this->activeTab === 'my' ? 'You don\'t own any servers!' : 'You don\'t have access to any servers!')
|
||||
@ -148,33 +195,36 @@ class ListServers extends ListRecords
|
||||
];
|
||||
}
|
||||
|
||||
protected function getResourceColor(Server $server, string $resource): ?string
|
||||
public function getResourceColor(Server $server, string $resource): ?string
|
||||
{
|
||||
$current = null;
|
||||
$limit = null;
|
||||
|
||||
switch ($resource) {
|
||||
case 'cpu':
|
||||
$current = $server->retrieveResources()['cpu_absolute'] ?? 0;
|
||||
$current = $server->resources()['cpu_absolute'] ?? 0;
|
||||
$limit = $server->cpu;
|
||||
if ($server->cpu === 0) {
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'memory':
|
||||
$current = $server->retrieveResources()['memory_bytes'] ?? 0;
|
||||
$current = $server->resources()['memory_bytes'] ?? 0;
|
||||
$limit = $server->memory * 2 ** 20;
|
||||
if ($server->memory === 0) {
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'disk':
|
||||
$current = $server->retrieveResources()['disk_bytes'] ?? 0;
|
||||
$current = $server->resources()['disk_bytes'] ?? 0;
|
||||
$limit = $server->disk * 2 ** 20;
|
||||
if ($server->disk === 0) {
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@ -188,6 +238,7 @@ class ListServers extends ListRecords
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
#[On('powerAction')]
|
||||
@ -202,8 +253,6 @@ class ListServers extends ListRecords
|
||||
->success()
|
||||
->send();
|
||||
|
||||
cache()->forget("servers.$server->uuid.status");
|
||||
|
||||
$this->redirect(self::getUrl(['activeTab' => $this->activeTab]));
|
||||
} catch (ConnectionException) {
|
||||
Notification::make()
|
||||
@ -212,36 +261,4 @@ class ListServers extends ListRecords
|
||||
->send();
|
||||
}
|
||||
}
|
||||
|
||||
/** @return Action[] */
|
||||
public static function getPowerActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('start')
|
||||
->color('primary')
|
||||
->icon('tabler-player-play-filled')
|
||||
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_START, $server))
|
||||
->visible(fn (Server $server) => !$server->isInConflictState() & $server->retrieveStatus()->isStartable())
|
||||
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'start']),
|
||||
Action::make('restart')
|
||||
->color('gray')
|
||||
->icon('tabler-reload')
|
||||
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_RESTART, $server))
|
||||
->visible(fn (Server $server) => !$server->isInConflictState() & $server->retrieveStatus()->isRestartable())
|
||||
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'restart']),
|
||||
Action::make('stop')
|
||||
->color('danger')
|
||||
->icon('tabler-player-stop-filled')
|
||||
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
|
||||
->visible(fn (Server $server) => !$server->isInConflictState() & $server->retrieveStatus()->isStoppable())
|
||||
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'stop']),
|
||||
Action::make('kill')
|
||||
->color('danger')
|
||||
->icon('tabler-alert-square')
|
||||
->tooltip('This can result in data corruption and/or data loss!')
|
||||
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
|
||||
->visible(fn (Server $server) => !$server->isInConflictState() & $server->retrieveStatus()->isKillable())
|
||||
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'kill']),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ class RotateDatabasePasswordAction extends Action
|
||||
|
||||
$this->icon('tabler-refresh');
|
||||
|
||||
$this->authorize(fn (Database $database) => auth()->user()->can('update', $database));
|
||||
$this->authorize(fn (Database $database) => auth()->user()->can('update database', $database));
|
||||
|
||||
$this->modalHeading(trans('admin/databasehost.rotate_password'));
|
||||
|
||||
|
@ -6,5 +6,5 @@ use Filament\Tables\Columns\Column;
|
||||
|
||||
class ServerEntryColumn extends Column
|
||||
{
|
||||
protected string $view = 'livewire.columns.server-entry-column';
|
||||
protected string $view = 'tables.columns.server-entry-column';
|
||||
}
|
||||
|
@ -32,7 +32,6 @@ use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Auth\EditProfile as BaseEditProfile;
|
||||
use Filament\Support\Colors\Color;
|
||||
@ -289,8 +288,6 @@ class EditProfile extends BaseEditProfile
|
||||
);
|
||||
|
||||
Activity::event('user:api-key.create')
|
||||
->actor($user)
|
||||
->subject($user)
|
||||
->subject($token->accessToken)
|
||||
->property('identifier', $token->accessToken->identifier)
|
||||
->log();
|
||||
@ -403,38 +400,30 @@ class EditProfile extends BaseEditProfile
|
||||
})
|
||||
->reactive()
|
||||
->default('monospace')
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('font_preview', $state)),
|
||||
->afterStateUpdated(fn ($state, callable $set) => $set('font_preview', $state)),
|
||||
Placeholder::make('font_preview')
|
||||
->label(trans('profile.font_preview'))
|
||||
->columnSpan(2)
|
||||
->content(function (Get $get) {
|
||||
$fontName = $get('console_font') ?? 'monospace';
|
||||
$fontSize = $get('console_font_size') . 'px';
|
||||
$style = <<<CSS
|
||||
.preview-text {
|
||||
font-family: $fontName;
|
||||
font-size: $fontSize;
|
||||
margin-top: 10px;
|
||||
display: block;
|
||||
}
|
||||
CSS;
|
||||
if ($fontName !== 'monospace') {
|
||||
$fontUrl = asset("storage/fonts/$fontName.ttf");
|
||||
$style = <<<CSS
|
||||
@font-face {
|
||||
font-family: $fontName;
|
||||
src: url("$fontUrl");
|
||||
}
|
||||
$style
|
||||
CSS;
|
||||
}
|
||||
$fontUrl = asset("storage/fonts/{$fontName}.ttf");
|
||||
|
||||
return new HtmlString(<<<HTML
|
||||
<style>
|
||||
{$style}
|
||||
</style>
|
||||
<span class="preview-text">The quick blue pelican jumps over the lazy pterodactyl. :)</span>
|
||||
HTML);
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "CustomPreviewFont";
|
||||
src: url("$fontUrl");
|
||||
}
|
||||
.preview-text {
|
||||
font-family: "CustomPreviewFont";
|
||||
font-size: $fontSize;
|
||||
margin-top: 10px;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<span class="preview-text">The quick blue pelican jumps over the lazy pterodactyl. :)</span>
|
||||
HTML);
|
||||
}),
|
||||
TextInput::make('console_graph_period')
|
||||
->label(trans('profile.graph_period'))
|
||||
|
@ -2,10 +2,8 @@
|
||||
|
||||
namespace App\Filament\Pages\Auth;
|
||||
|
||||
use App\Events\Auth\ProvidedAuthenticationToken;
|
||||
use App\Extensions\Captcha\Providers\CaptchaProvider;
|
||||
use App\Extensions\OAuth\Providers\OAuthProvider;
|
||||
use App\Facades\Activity;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Actions;
|
||||
@ -56,37 +54,14 @@ class Login extends BaseLogin
|
||||
if ($token === null) {
|
||||
$this->verifyTwoFactor = true;
|
||||
|
||||
Activity::event('auth:checkpoint')
|
||||
->withRequestMetadata()
|
||||
->subject($user)
|
||||
->log();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$isValidToken = false;
|
||||
if (strlen($token) === $this->google2FA->getOneTimePasswordLength()) {
|
||||
$isValidToken = $this->google2FA->verifyKey(
|
||||
$user->totp_secret,
|
||||
$token,
|
||||
Config::integer('panel.auth.2fa.window'),
|
||||
);
|
||||
|
||||
if ($isValidToken) {
|
||||
event(new ProvidedAuthenticationToken($user));
|
||||
}
|
||||
} else {
|
||||
foreach ($user->recoveryTokens as $recoveryToken) {
|
||||
if (password_verify($token, $recoveryToken->token)) {
|
||||
$isValidToken = true;
|
||||
$recoveryToken->delete();
|
||||
|
||||
event(new ProvidedAuthenticationToken($user, true));
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$isValidToken = $this->google2FA->verifyKey(
|
||||
$user->totp_secret,
|
||||
$token,
|
||||
Config::integer('panel.auth.2fa.window'),
|
||||
);
|
||||
|
||||
if (!$isValidToken) {
|
||||
// Buffer to prevent bruteforce
|
||||
@ -133,9 +108,7 @@ class Login extends BaseLogin
|
||||
{
|
||||
return TextInput::make('2fa')
|
||||
->label(trans('auth.two-factor-code'))
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip(trans('auth.two-factor-hint'))
|
||||
->visible(fn () => $this->verifyTwoFactor)
|
||||
->hidden(fn () => !$this->verifyTwoFactor)
|
||||
->required()
|
||||
->live();
|
||||
}
|
||||
|
@ -2,27 +2,43 @@
|
||||
|
||||
namespace App\Filament\Server\Components;
|
||||
|
||||
use Closure;
|
||||
use Filament\Support\Concerns\EvaluatesClosures;
|
||||
use Filament\Widgets\StatsOverviewWidget\Stat;
|
||||
use Illuminate\Contracts\Support\Htmlable;
|
||||
use Illuminate\Contracts\View\View;
|
||||
|
||||
class SmallStatBlock extends Stat
|
||||
{
|
||||
use EvaluatesClosures;
|
||||
protected string|Htmlable $label;
|
||||
|
||||
protected bool|Closure $copyOnClick = false;
|
||||
protected $value;
|
||||
|
||||
public function copyOnClick(bool|Closure $copyOnClick = true): static
|
||||
public function label(string|Htmlable $label): static
|
||||
{
|
||||
$this->copyOnClick = $copyOnClick;
|
||||
$this->label = $label;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function shouldCopyOnClick(): bool
|
||||
public function value($value): static
|
||||
{
|
||||
return $this->evaluate($this->copyOnClick);
|
||||
$this->value = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLabel(): string|Htmlable
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function getValue()
|
||||
{
|
||||
return value($this->value);
|
||||
}
|
||||
|
||||
public function toHtml(): string
|
||||
{
|
||||
return $this->render()->render();
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
|
48
app/Filament/Server/Components/StatBlock.php
Normal file
48
app/Filament/Server/Components/StatBlock.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Components;
|
||||
|
||||
use Filament\Widgets\StatsOverviewWidget\Stat;
|
||||
use Illuminate\Contracts\Support\Htmlable;
|
||||
use Illuminate\Contracts\View\View;
|
||||
|
||||
class StatBlock extends Stat
|
||||
{
|
||||
protected string|Htmlable $label;
|
||||
|
||||
protected $value;
|
||||
|
||||
public function label(string|Htmlable $label): static
|
||||
{
|
||||
$this->label = $label;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function value($value): static
|
||||
{
|
||||
$this->value = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLabel(): string|Htmlable
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function getValue()
|
||||
{
|
||||
return value($this->value);
|
||||
}
|
||||
|
||||
public function toHtml(): string
|
||||
{
|
||||
return $this->render()->render();
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('filament.components.server-data-block', $this->data());
|
||||
}
|
||||
}
|
@ -2,8 +2,6 @@
|
||||
|
||||
namespace App\Filament\Server\Resources;
|
||||
|
||||
use App\Filament\Admin\Resources\UserResource\Pages\EditUser;
|
||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
||||
use App\Filament\Server\Resources\ActivityResource\Pages;
|
||||
use App\Models\ActivityLog;
|
||||
use App\Models\Permission;
|
||||
@ -11,20 +9,9 @@ use App\Models\Role;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\KeyValue;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class ActivityResource extends Resource
|
||||
{
|
||||
@ -38,101 +25,12 @@ class ActivityResource extends Resource
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-stack';
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $table
|
||||
->paginated([25, 50])
|
||||
->defaultPaginationPageOption(25)
|
||||
->columns([
|
||||
TextColumn::make('event')
|
||||
->html()
|
||||
->description(fn ($state) => $state)
|
||||
->icon(fn (ActivityLog $activityLog) => $activityLog->getIcon())
|
||||
->formatStateUsing(fn (ActivityLog $activityLog) => $activityLog->getLabel()),
|
||||
TextColumn::make('user')
|
||||
->state(function (ActivityLog $activityLog) use ($server) {
|
||||
if (!$activityLog->actor instanceof User) {
|
||||
return $activityLog->actor_id === null ? 'System' : 'Deleted user';
|
||||
}
|
||||
|
||||
$user = $activityLog->actor->username;
|
||||
|
||||
// Only show the email if the actor is the server owner/ a subuser or if the viewing user is an admin
|
||||
if (auth()->user()->isAdmin() || $server->owner_id === $activityLog->actor->id || $server->subusers->where('user_id', $activityLog->actor->id)->first()) {
|
||||
$user .= " ({$activityLog->actor->email})";
|
||||
}
|
||||
|
||||
return $user;
|
||||
})
|
||||
->tooltip(fn (ActivityLog $activityLog) => auth()->user()->can('seeIps activityLog') ? $activityLog->ip : '')
|
||||
->url(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User && auth()->user()->can('update', $activityLog->actor) ? EditUser::getUrl(['record' => $activityLog->actor], panel: 'admin') : '')
|
||||
->grow(false),
|
||||
DateTimeColumn::make('timestamp')
|
||||
->since()
|
||||
->sortable()
|
||||
->grow(false),
|
||||
])
|
||||
->defaultSort('timestamp', 'desc')
|
||||
->actions([
|
||||
ViewAction::make()
|
||||
//->visible(fn (ActivityLog $activityLog) => $activityLog->hasAdditionalMetadata())
|
||||
->form([
|
||||
Placeholder::make('event')
|
||||
->content(fn (ActivityLog $activityLog) => new HtmlString($activityLog->getLabel())),
|
||||
TextInput::make('user')
|
||||
->formatStateUsing(function (ActivityLog $activityLog) use ($server) {
|
||||
if (!$activityLog->actor instanceof User) {
|
||||
return $activityLog->actor_id === null ? 'System' : 'Deleted user';
|
||||
}
|
||||
|
||||
$user = $activityLog->actor->username;
|
||||
|
||||
// Only show the email if the actor is the server owner/ a subuser or if the viewing user is an admin
|
||||
if (auth()->user()->isAdmin() || $server->owner_id === $activityLog->actor->id || $server->subusers->where('user_id', $activityLog->actor->id)->first()) {
|
||||
$user .= " ({$activityLog->actor->email})";
|
||||
}
|
||||
|
||||
if (auth()->user()->can('seeIps activityLog')) {
|
||||
$user .= " - $activityLog->ip";
|
||||
}
|
||||
|
||||
return $user;
|
||||
})
|
||||
->hintAction(
|
||||
Action::make('edit')
|
||||
->label(trans('filament-actions::edit.single.label'))
|
||||
->icon('tabler-edit')
|
||||
->visible(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User && auth()->user()->can('update', $activityLog->actor))
|
||||
->url(fn (ActivityLog $activityLog) => EditUser::getUrl(['record' => $activityLog->actor], panel: 'admin'))
|
||||
),
|
||||
DateTimePicker::make('timestamp'),
|
||||
KeyValue::make('properties')
|
||||
->label('Metadata')
|
||||
->formatStateUsing(fn ($state) => Arr::dot($state)),
|
||||
]),
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('event')
|
||||
->options(fn (Table $table) => $table->getQuery()->pluck('event', 'event')->unique()->sort())
|
||||
->searchable()
|
||||
->preload(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function canViewAny(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_ACTIVITY_READ, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function getEloquentQuery(): Builder
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return ActivityLog::whereHas('subjects', fn (Builder $query) => $query->where('subject_id', $server->id)->where('subject_type', $server->getMorphClass()))
|
||||
return ActivityLog::whereHas('subjects', fn (Builder $query) => $query->where('subject_id', $server->id))
|
||||
->whereNotIn('activity_logs.event', ActivityLog::DISABLED_EVENTS)
|
||||
->when(config('activity.hide_admin_activity'), function (Builder $builder) use ($server) {
|
||||
// We could do this with a query and a lot of joins, but that gets pretty
|
||||
@ -153,6 +51,11 @@ class ActivityResource extends Resource
|
||||
});
|
||||
}
|
||||
|
||||
public static function canViewAny(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_ACTIVITY_READ, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
|
@ -2,13 +2,114 @@
|
||||
|
||||
namespace App\Filament\Server\Resources\ActivityResource\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\UserResource\Pages\EditUser;
|
||||
use App\Filament\Server\Resources\ActivityResource;
|
||||
use App\Models\ActivityLog;
|
||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\KeyValue;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class ListActivities extends ListRecords
|
||||
{
|
||||
protected static string $resource = ActivityResource::class;
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $table
|
||||
->paginated([25, 50])
|
||||
->defaultPaginationPageOption(25)
|
||||
->columns([
|
||||
TextColumn::make('event')
|
||||
->html()
|
||||
->description(fn ($state) => $state)
|
||||
->icon(fn (ActivityLog $activityLog) => $activityLog->getIcon())
|
||||
->formatStateUsing(fn (ActivityLog $activityLog) => $activityLog->getLabel()),
|
||||
TextColumn::make('user')
|
||||
->state(function (ActivityLog $activityLog) use ($server) {
|
||||
if (!$activityLog->actor instanceof User) {
|
||||
return $activityLog->actor_id === null ? 'System' : 'Deleted user';
|
||||
}
|
||||
|
||||
$user = $activityLog->actor->username;
|
||||
|
||||
// Only show the email if the actor is the server owner/ a subuser or if the viewing user is an admin
|
||||
if (auth()->user()->isAdmin() || $server->owner_id === $activityLog->actor->id || $server->subusers->where('user_id', $activityLog->actor->id)->first()) {
|
||||
$user .= " ({$activityLog->actor->email})";
|
||||
}
|
||||
|
||||
return $user;
|
||||
})
|
||||
->tooltip(fn (ActivityLog $activityLog) => auth()->user()->can('seeIps activityLog') ? $activityLog->ip : '')
|
||||
->url(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User && auth()->user()->can('update user') ? EditUser::getUrl(['record' => $activityLog->actor], panel: 'admin') : '')
|
||||
->grow(false),
|
||||
DateTimeColumn::make('timestamp')
|
||||
->since()
|
||||
->sortable()
|
||||
->grow(false),
|
||||
])
|
||||
->defaultSort('timestamp', 'desc')
|
||||
->actions([
|
||||
ViewAction::make()
|
||||
//->visible(fn (ActivityLog $activityLog) => $activityLog->hasAdditionalMetadata())
|
||||
->form([
|
||||
Placeholder::make('event')
|
||||
->content(fn (ActivityLog $activityLog) => new HtmlString($activityLog->getLabel())),
|
||||
TextInput::make('user')
|
||||
->formatStateUsing(function (ActivityLog $activityLog) use ($server) {
|
||||
if (!$activityLog->actor instanceof User) {
|
||||
return $activityLog->actor_id === null ? 'System' : 'Deleted user';
|
||||
}
|
||||
|
||||
$user = $activityLog->actor->username;
|
||||
|
||||
// Only show the email if the actor is the server owner/ a subuser or if the viewing user is an admin
|
||||
if (auth()->user()->isAdmin() || $server->owner_id === $activityLog->actor->id || $server->subusers->where('user_id', $activityLog->actor->id)->first()) {
|
||||
$user .= " ({$activityLog->actor->email})";
|
||||
}
|
||||
|
||||
if (auth()->user()->can('seeIps activityLog')) {
|
||||
$user .= " - $activityLog->ip";
|
||||
}
|
||||
|
||||
return $user;
|
||||
})
|
||||
->hintAction(
|
||||
Action::make('edit')
|
||||
->label(trans('filament-actions::edit.single.label'))
|
||||
->icon('tabler-edit')
|
||||
->visible(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User && auth()->user()->can('update user'))
|
||||
->url(fn (ActivityLog $activityLog) => EditUser::getUrl(['record' => $activityLog->actor], panel: 'admin'))
|
||||
),
|
||||
DateTimePicker::make('timestamp'),
|
||||
KeyValue::make('properties')
|
||||
->label('Metadata')
|
||||
->formatStateUsing(fn ($state) => Arr::dot($state)),
|
||||
]),
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('event')
|
||||
->options(fn (Table $table) => $table->getQuery()->pluck('event', 'event')->unique()->sort())
|
||||
->searchable()
|
||||
->preload(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
return [];
|
||||
|
@ -2,18 +2,12 @@
|
||||
|
||||
namespace App\Filament\Server\Resources;
|
||||
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Server\Resources\AllocationResource\Pages;
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables\Actions\DetachAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\TextInputColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AllocationResource extends Resource
|
||||
@ -28,61 +22,6 @@ class AllocationResource extends Resource
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-network';
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('ip')
|
||||
->label('Address')
|
||||
->formatStateUsing(fn (Allocation $allocation) => $allocation->alias),
|
||||
TextColumn::make('alias')
|
||||
->hidden(),
|
||||
TextColumn::make('port'),
|
||||
TextInputColumn::make('notes')
|
||||
->visibleFrom('sm')
|
||||
->disabled(fn () => !auth()->user()->can(Permission::ACTION_ALLOCATION_UPDATE, $server))
|
||||
->label('Notes')
|
||||
->placeholder('No Notes'),
|
||||
IconColumn::make('primary')
|
||||
->icon(fn ($state) => match ($state) {
|
||||
true => 'tabler-star-filled',
|
||||
default => 'tabler-star',
|
||||
})
|
||||
->color(fn ($state) => match ($state) {
|
||||
true => 'warning',
|
||||
default => 'gray',
|
||||
})
|
||||
->action(function (Allocation $allocation) use ($server) {
|
||||
if (auth()->user()->can(PERMISSION::ACTION_ALLOCATION_UPDATE, $server)) {
|
||||
return $server->update(['allocation_id' => $allocation->id]);
|
||||
}
|
||||
})
|
||||
->default(fn (Allocation $allocation) => $allocation->id === $server->allocation_id)
|
||||
->label('Primary'),
|
||||
])
|
||||
->actions([
|
||||
DetachAction::make()
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_ALLOCATION_DELETE, $server))
|
||||
->label('Delete')
|
||||
->icon('tabler-trash')
|
||||
->hidden(fn (Allocation $allocation) => $allocation->id === $server->allocation_id)
|
||||
->action(function (Allocation $allocation) {
|
||||
Allocation::query()->where('id', $allocation->id)->update([
|
||||
'notes' => null,
|
||||
'server_id' => null,
|
||||
]);
|
||||
|
||||
Activity::event('server:allocation.delete')
|
||||
->subject($allocation)
|
||||
->property('allocation', $allocation->address)
|
||||
->log();
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
// TODO: find better way handle server conflict state
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
|
@ -4,24 +4,85 @@ namespace App\Filament\Server\Resources\AllocationResource\Pages;
|
||||
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Server\Resources\AllocationResource;
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Services\Allocations\FindAssignableAllocationService;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\DetachAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\TextInputColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ListAllocations extends ListRecords
|
||||
{
|
||||
protected static string $resource = AllocationResource::class;
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('ip')
|
||||
->label('Address')
|
||||
->formatStateUsing(fn (Allocation $allocation) => $allocation->alias),
|
||||
TextColumn::make('alias')
|
||||
->hidden(),
|
||||
TextColumn::make('port'),
|
||||
TextInputColumn::make('notes')
|
||||
->visibleFrom('sm')
|
||||
->disabled(fn () => !auth()->user()->can(Permission::ACTION_ALLOCATION_UPDATE, $server))
|
||||
->label('Notes')
|
||||
->placeholder('No Notes'),
|
||||
IconColumn::make('primary')
|
||||
->icon(fn ($state) => match ($state) {
|
||||
true => 'tabler-star-filled',
|
||||
default => 'tabler-star',
|
||||
})
|
||||
->color(fn ($state) => match ($state) {
|
||||
true => 'warning',
|
||||
default => 'gray',
|
||||
})
|
||||
->action(function (Allocation $allocation) use ($server) {
|
||||
if (auth()->user()->can(PERMISSION::ACTION_ALLOCATION_UPDATE, $server)) {
|
||||
return $server->update(['allocation_id' => $allocation->id]);
|
||||
}
|
||||
})
|
||||
->default(fn (Allocation $allocation) => $allocation->id === $server->allocation_id)
|
||||
->label('Primary'),
|
||||
])
|
||||
->actions([
|
||||
DetachAction::make()
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_ALLOCATION_DELETE, $server))
|
||||
->label('Delete')
|
||||
->icon('tabler-trash')
|
||||
->hidden(fn (Allocation $allocation) => $allocation->id === $server->allocation_id)
|
||||
->action(function (Allocation $allocation) {
|
||||
Allocation::query()->where('id', $allocation->id)->update([
|
||||
'notes' => null,
|
||||
'server_id' => null,
|
||||
]);
|
||||
|
||||
Activity::event('server:allocation.delete')
|
||||
->subject($allocation)
|
||||
->property('allocation', $allocation->toString())
|
||||
->log();
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return [
|
||||
Action::make('addAllocation')
|
||||
Actions\Action::make('addAllocation')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_ALLOCATION_CREATE, $server))
|
||||
->label(fn () => $server->allocations()->count() >= $server->allocation_limit ? 'Allocation limit reached' : 'Add Allocation')
|
||||
->hidden(fn () => !config('panel.client_features.allocations.enabled'))
|
||||
@ -32,7 +93,7 @@ class ListAllocations extends ListRecords
|
||||
|
||||
Activity::event('server:allocation.create')
|
||||
->subject($allocation)
|
||||
->property('allocation', $allocation->address)
|
||||
->property('allocation', $allocation->toString())
|
||||
->log();
|
||||
}),
|
||||
];
|
||||
|
@ -2,37 +2,13 @@
|
||||
|
||||
namespace App\Filament\Server\Resources;
|
||||
|
||||
use App\Enums\BackupStatus;
|
||||
use App\Enums\ServerState;
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Server\Resources\BackupResource\Pages;
|
||||
use App\Http\Controllers\Api\Client\Servers\BackupController;
|
||||
use App\Models\Backup;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonBackupRepository;
|
||||
use App\Services\Backups\DownloadLinkService;
|
||||
use App\Filament\Components\Tables\Columns\BytesColumn;
|
||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
||||
use App\Services\Backups\DeleteBackupService;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\ActionGroup;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BackupResource extends Resource
|
||||
{
|
||||
@ -68,138 +44,8 @@ class BackupResource extends Resource
|
||||
return null;
|
||||
}
|
||||
|
||||
return $count >= $limit ? 'danger' : ($count >= $limit * self::WARNING_THRESHOLD ? 'warning' : 'success');
|
||||
}
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label('Name')
|
||||
->columnSpanFull(),
|
||||
TextArea::make('ignored')
|
||||
->columnSpanFull()
|
||||
->label('Ignored Files & Directories'),
|
||||
Toggle::make('is_locked')
|
||||
->label('Lock?')
|
||||
->helperText('Prevents this backup from being deleted until explicitly unlocked.'),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->searchable(),
|
||||
BytesColumn::make('bytes')
|
||||
->label('Size'),
|
||||
DateTimeColumn::make('created_at')
|
||||
->label('Created')
|
||||
->since()
|
||||
->sortable(),
|
||||
TextColumn::make('status')
|
||||
->label('Status')
|
||||
->badge(),
|
||||
IconColumn::make('is_locked')
|
||||
->visibleFrom('md')
|
||||
->label('Lock Status')
|
||||
->trueIcon('tabler-lock')
|
||||
->falseIcon('tabler-lock-open'),
|
||||
])
|
||||
->actions([
|
||||
ActionGroup::make([
|
||||
Action::make('lock')
|
||||
->icon(fn (Backup $backup) => !$backup->is_locked ? 'tabler-lock' : 'tabler-lock-open')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DELETE, $server))
|
||||
->label(fn (Backup $backup) => !$backup->is_locked ? 'Lock' : 'Unlock')
|
||||
->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->toggleLock($request, $server, $backup))
|
||||
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
|
||||
Action::make('download')
|
||||
->color('primary')
|
||||
->icon('tabler-download')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DOWNLOAD, $server))
|
||||
->url(fn (DownloadLinkService $downloadLinkService, Backup $backup, Request $request) => $downloadLinkService->handle($backup, $request->user()), true)
|
||||
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
|
||||
Action::make('restore')
|
||||
->color('success')
|
||||
->icon('tabler-folder-up')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_RESTORE, $server))
|
||||
->form([
|
||||
Placeholder::make('')
|
||||
->helperText('Your server will be stopped. You will not be able to control the power state, access the file manager, or create additional backups until this process is completed.'),
|
||||
Checkbox::make('truncate')
|
||||
->label('Delete all files before restoring backup?'),
|
||||
])
|
||||
->action(function (Backup $backup, $data, DaemonBackupRepository $daemonRepository, DownloadLinkService $downloadLinkService) use ($server) {
|
||||
if (!is_null($server->status)) {
|
||||
return Notification::make()
|
||||
->danger()
|
||||
->title('Backup Restore Failed')
|
||||
->body('This server is not currently in a state that allows for a backup to be restored.')
|
||||
->send();
|
||||
}
|
||||
|
||||
if (!$backup->is_successful && is_null($backup->completed_at)) {
|
||||
return Notification::make()
|
||||
->danger()
|
||||
->title('Backup Restore Failed')
|
||||
->body('This backup cannot be restored at this time: not completed or failed.')
|
||||
->send();
|
||||
}
|
||||
|
||||
$log = Activity::event('server:backup.restore')
|
||||
->subject($backup)
|
||||
->property(['name' => $backup->name, 'truncate' => $data['truncate']]);
|
||||
|
||||
$log->transaction(function () use ($downloadLinkService, $daemonRepository, $backup, $server, $data) {
|
||||
// If the backup is for an S3 file we need to generate a unique Download link for
|
||||
// it that will allow daemon to actually access the file.
|
||||
if ($backup->disk === Backup::ADAPTER_AWS_S3) {
|
||||
$url = $downloadLinkService->handle($backup, auth()->user());
|
||||
}
|
||||
|
||||
// Update the status right away for the server so that we know not to allow certain
|
||||
// actions against it via the Panel API.
|
||||
$server->update(['status' => ServerState::RestoringBackup]);
|
||||
|
||||
$daemonRepository->setServer($server)->restore($backup, $url ?? null, $data['truncate']);
|
||||
});
|
||||
|
||||
return Notification::make()
|
||||
->title('Restoring Backup')
|
||||
->send();
|
||||
})
|
||||
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
|
||||
DeleteAction::make('delete')
|
||||
->disabled(fn (Backup $backup) => $backup->is_locked)
|
||||
->modalDescription(fn (Backup $backup) => 'Do you wish to delete ' . $backup->name . '?')
|
||||
->modalSubmitActionLabel('Delete Backup')
|
||||
->action(function (Backup $backup, DeleteBackupService $deleteBackupService) {
|
||||
try {
|
||||
$deleteBackupService->handle($backup);
|
||||
} catch (ConnectionException) {
|
||||
Notification::make()
|
||||
->title('Could not delete backup')
|
||||
->body('Connection to node failed')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Activity::event('server:backup.delete')
|
||||
->subject($backup)
|
||||
->property(['name' => $backup->name, 'failed' => !$backup->is_successful])
|
||||
->log();
|
||||
})
|
||||
->visible(fn (Backup $backup) => $backup->status !== BackupStatus::InProgress),
|
||||
]),
|
||||
]);
|
||||
return $count >= $limit ? 'danger'
|
||||
: ($count >= $limit * self::WARNING_THRESHOLD ? 'warning' : 'success');
|
||||
}
|
||||
|
||||
// TODO: find better way handle server conflict state
|
||||
|
@ -2,28 +2,165 @@
|
||||
|
||||
namespace App\Filament\Server\Resources\BackupResource\Pages;
|
||||
|
||||
use App\Enums\BackupStatus;
|
||||
use App\Enums\ServerState;
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Server\Resources\BackupResource;
|
||||
use App\Http\Controllers\Api\Client\Servers\BackupController;
|
||||
use App\Models\Backup;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonBackupRepository;
|
||||
use App\Services\Backups\DownloadLinkService;
|
||||
use App\Services\Backups\InitiateBackupService;
|
||||
use Filament\Actions\CreateAction;
|
||||
use App\Filament\Components\Tables\Columns\BytesColumn;
|
||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
||||
use Filament\Actions;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\ActionGroup;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
class ListBackups extends ListRecords
|
||||
{
|
||||
protected static string $resource = BackupResource::class;
|
||||
|
||||
protected static bool $canCreateAnother = false;
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label('Name')
|
||||
->columnSpanFull(),
|
||||
TextArea::make('ignored')
|
||||
->columnSpanFull()
|
||||
->label('Ignored Files & Directories'),
|
||||
Toggle::make('is_locked')
|
||||
->label('Lock?')
|
||||
->helperText('Prevents this backup from being deleted until explicitly unlocked.'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->searchable(),
|
||||
BytesColumn::make('bytes')
|
||||
->label('Size'),
|
||||
DateTimeColumn::make('created_at')
|
||||
->label('Created')
|
||||
->since()
|
||||
->sortable(),
|
||||
TextColumn::make('status')
|
||||
->label('Status')
|
||||
->badge(),
|
||||
IconColumn::make('is_locked')
|
||||
->visibleFrom('md')
|
||||
->label('Lock Status')
|
||||
->trueIcon('tabler-lock')
|
||||
->falseIcon('tabler-lock-open'),
|
||||
])
|
||||
->actions([
|
||||
ActionGroup::make([
|
||||
Action::make('lock')
|
||||
->icon(fn (Backup $backup) => !$backup->is_locked ? 'tabler-lock' : 'tabler-lock-open')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DELETE, $server))
|
||||
->label(fn (Backup $backup) => !$backup->is_locked ? 'Lock' : 'Unlock')
|
||||
->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->toggleLock($request, $server, $backup))
|
||||
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
|
||||
Action::make('download')
|
||||
->color('primary')
|
||||
->icon('tabler-download')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DOWNLOAD, $server))
|
||||
->url(fn (DownloadLinkService $downloadLinkService, Backup $backup, Request $request) => $downloadLinkService->handle($backup, $request->user()), true)
|
||||
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
|
||||
Action::make('restore')
|
||||
->color('success')
|
||||
->icon('tabler-folder-up')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_RESTORE, $server))
|
||||
->form([
|
||||
Placeholder::make('')
|
||||
->helperText('Your server will be stopped. You will not be able to control the power state, access the file manager, or create additional backups until this process is completed.'),
|
||||
Checkbox::make('truncate')
|
||||
->label('Delete all files before restoring backup?'),
|
||||
])
|
||||
->action(function (Backup $backup, $data, DaemonBackupRepository $daemonRepository, DownloadLinkService $downloadLinkService) use ($server) {
|
||||
if (!is_null($server->status)) {
|
||||
return Notification::make()
|
||||
->danger()
|
||||
->title('Backup Restore Failed')
|
||||
->body('This server is not currently in a state that allows for a backup to be restored.')
|
||||
->send();
|
||||
}
|
||||
|
||||
if (!$backup->is_successful && is_null($backup->completed_at)) { //TODO Change to Notifications
|
||||
return Notification::make()
|
||||
->danger()
|
||||
->title('Backup Restore Failed')
|
||||
->body('This backup cannot be restored at this time: not completed or failed.')
|
||||
->send();
|
||||
}
|
||||
|
||||
$log = Activity::event('server:backup.restore')
|
||||
->subject($backup)
|
||||
->property(['name' => $backup->name, 'truncate' => $data['truncate']]);
|
||||
|
||||
$log->transaction(function () use ($downloadLinkService, $daemonRepository, $backup, $server, $data) {
|
||||
// If the backup is for an S3 file we need to generate a unique Download link for
|
||||
// it that will allow daemon to actually access the file.
|
||||
if ($backup->disk === Backup::ADAPTER_AWS_S3) {
|
||||
$url = $downloadLinkService->handle($backup, auth()->user());
|
||||
}
|
||||
|
||||
// Update the status right away for the server so that we know not to allow certain
|
||||
// actions against it via the Panel API.
|
||||
$server->update(['status' => ServerState::RestoringBackup]);
|
||||
|
||||
$daemonRepository->setServer($server)->restore($backup, $url ?? null, $data['truncate']);
|
||||
});
|
||||
|
||||
return Notification::make()
|
||||
->title('Restoring Backup')
|
||||
->send();
|
||||
})
|
||||
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
|
||||
DeleteAction::make('delete')
|
||||
->disabled(fn (Backup $backup) => $backup->is_locked)
|
||||
->modalDescription(fn (Backup $backup) => 'Do you wish to delete, ' . $backup->name . '?')
|
||||
->modalSubmitActionLabel('Delete Backup')
|
||||
->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->delete($request, $server, $backup))
|
||||
->visible(fn (Backup $backup) => $backup->status !== BackupStatus::InProgress),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return [
|
||||
CreateAction::make()
|
||||
Actions\CreateAction::make()
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_CREATE, $server))
|
||||
->label(fn () => $server->backups()->count() >= $server->backup_limit ? 'Backup limit reached' : 'Create Backup')
|
||||
->disabled(fn () => $server->backups()->count() >= $server->backup_limit)
|
||||
|
@ -2,23 +2,13 @@
|
||||
|
||||
namespace App\Filament\Server\Resources;
|
||||
|
||||
use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction;
|
||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
||||
use App\Filament\Server\Resources\DatabaseResource\Pages;
|
||||
use App\Models\Database;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Services\Databases\DatabaseManagementService;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||
|
||||
class DatabaseResource extends Resource
|
||||
{
|
||||
@ -52,65 +42,9 @@ class DatabaseResource extends Resource
|
||||
return null;
|
||||
}
|
||||
|
||||
return $count >= $limit ? 'danger' : ($count >= $limit * self::WARNING_THRESHOLD ? 'warning' : 'success');
|
||||
}
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('host')
|
||||
->formatStateUsing(fn (Database $database) => $database->address())
|
||||
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null),
|
||||
TextInput::make('database')
|
||||
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null),
|
||||
TextInput::make('username')
|
||||
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null),
|
||||
TextInput::make('password')
|
||||
->password()->revealable()
|
||||
->hidden(fn () => !auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server))
|
||||
->hintAction(
|
||||
RotateDatabasePasswordAction::make()
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_DATABASE_UPDATE, $server))
|
||||
)
|
||||
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
|
||||
->formatStateUsing(fn (Database $database) => $database->password),
|
||||
TextInput::make('remote')
|
||||
->label('Connections From'),
|
||||
TextInput::make('max_connections')
|
||||
->formatStateUsing(fn (Database $database) => $database->max_connections === 0 ? $database->max_connections : 'Unlimited'),
|
||||
TextInput::make('jdbc')
|
||||
->label('JDBC Connection String')
|
||||
->password()->revealable()
|
||||
->hidden(!auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server))
|
||||
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
|
||||
->columnSpanFull()
|
||||
->formatStateUsing(fn (Database $database) => $database->jdbc),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('host')
|
||||
->state(fn (Database $database) => $database->address())
|
||||
->badge(),
|
||||
TextColumn::make('database'),
|
||||
TextColumn::make('username'),
|
||||
TextColumn::make('remote'),
|
||||
DateTimeColumn::make('created_at')
|
||||
->sortable(),
|
||||
])
|
||||
->actions([
|
||||
ViewAction::make()
|
||||
->modalHeading(fn (Database $database) => 'Viewing ' . $database->database),
|
||||
DeleteAction::make()
|
||||
->using(fn (Database $database, DatabaseManagementService $service) => $service->delete($database)),
|
||||
]);
|
||||
return $count >= $limit
|
||||
? 'danger'
|
||||
: ($count >= $limit * self::WARNING_THRESHOLD ? 'warning' : 'success');
|
||||
}
|
||||
|
||||
// TODO: find better way handle server conflict state
|
||||
|
@ -2,8 +2,12 @@
|
||||
|
||||
namespace App\Filament\Server\Resources\DatabaseResource\Pages;
|
||||
|
||||
use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction;
|
||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
||||
use App\Filament\Server\Resources\DatabaseResource;
|
||||
use App\Models\Database;
|
||||
use App\Models\DatabaseHost;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Services\Databases\DatabaseManagementService;
|
||||
use Filament\Actions\CreateAction;
|
||||
@ -11,12 +15,76 @@ use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||
|
||||
class ListDatabases extends ListRecords
|
||||
{
|
||||
protected static string $resource = DatabaseResource::class;
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('host')
|
||||
->formatStateUsing(fn (Database $database) => $database->address())
|
||||
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null),
|
||||
TextInput::make('database')
|
||||
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null),
|
||||
TextInput::make('username')
|
||||
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null),
|
||||
TextInput::make('password')
|
||||
->password()->revealable()
|
||||
->hidden(fn () => !auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server))
|
||||
->hintAction(
|
||||
RotateDatabasePasswordAction::make()
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_DATABASE_UPDATE, $server))
|
||||
)
|
||||
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
|
||||
->formatStateUsing(fn (Database $database) => $database->password),
|
||||
TextInput::make('remote')
|
||||
->label('Connections From'),
|
||||
TextInput::make('max_connections')
|
||||
->formatStateUsing(fn (Database $database) => $database->max_connections === 0 ? $database->max_connections : 'Unlimited'),
|
||||
TextInput::make('jdbc')
|
||||
->label('JDBC Connection String')
|
||||
->password()->revealable()
|
||||
->hidden(!auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server))
|
||||
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
|
||||
->columnSpanFull()
|
||||
->formatStateUsing(fn (Database $database) => $database->jdbc),
|
||||
]);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('host')
|
||||
->state(fn (Database $database) => $database->address())
|
||||
->badge(),
|
||||
TextColumn::make('database'),
|
||||
TextColumn::make('username'),
|
||||
TextColumn::make('remote'),
|
||||
DateTimeColumn::make('created_at')
|
||||
->sortable(),
|
||||
])
|
||||
->actions([
|
||||
ViewAction::make()
|
||||
->modalHeading(fn (Database $database) => 'Viewing ' . $database->database),
|
||||
DeleteAction::make()
|
||||
->using(fn (Database $database, DatabaseManagementService $service) => $service->delete($database)),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
/** @var Server $server */
|
||||
|
@ -314,9 +314,9 @@ class ListFiles extends ListRecords
|
||||
->label('')
|
||||
->icon('tabler-trash')
|
||||
->requiresConfirmation()
|
||||
->modalHeading(fn (File $file) => trans('filament-actions::delete.single.modal.heading', ['label' => $file->name . ' ' . ($file->is_directory ? 'folder' : 'file')]))
|
||||
->modalDescription(fn (File $file) => $file->name)
|
||||
->modalHeading('Delete file?')
|
||||
->action(function (File $file) {
|
||||
$this->deselectAllTableRecords();
|
||||
$this->getDaemonFileRepository()->deleteFiles($this->path, [$file->name]);
|
||||
|
||||
Activity::event('server:file.delete')
|
||||
|
@ -2,8 +2,6 @@
|
||||
|
||||
namespace App\Filament\Server\Resources;
|
||||
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
||||
use App\Filament\Server\Resources\ScheduleResource\Pages;
|
||||
use App\Filament\Server\Resources\ScheduleResource\RelationManagers\TasksRelationManager;
|
||||
use App\Helpers\Utilities;
|
||||
@ -25,12 +23,6 @@ use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ScheduleResource extends Resource
|
||||
@ -311,44 +303,6 @@ class ScheduleResource extends Resource
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->searchable(),
|
||||
TextColumn::make('cron')
|
||||
->state(fn (Schedule $schedule) => $schedule->cron_minute . ' ' . $schedule->cron_hour . ' ' . $schedule->cron_day_of_month . ' ' . $schedule->cron_month . ' ' . $schedule->cron_day_of_week),
|
||||
TextColumn::make('status')
|
||||
->state(fn (Schedule $schedule) => !$schedule->is_active ? 'Inactive' : ($schedule->is_processing ? 'Processing' : 'Active')),
|
||||
IconColumn::make('only_when_online')
|
||||
->boolean()
|
||||
->sortable(),
|
||||
DateTimeColumn::make('last_run_at')
|
||||
->label('Last run')
|
||||
->placeholder('Never')
|
||||
->since()
|
||||
->sortable(),
|
||||
DateTimeColumn::make('next_run_at')
|
||||
->label('Next run')
|
||||
->placeholder('Never')
|
||||
->since()
|
||||
->sortable()
|
||||
->state(fn (Schedule $schedule) => $schedule->is_active ? $schedule->next_run_at : null),
|
||||
])
|
||||
->actions([
|
||||
ViewAction::make(),
|
||||
EditAction::make(),
|
||||
DeleteAction::make()
|
||||
->after(function (Schedule $schedule) {
|
||||
Activity::event('server:schedule.delete')
|
||||
->subject($schedule)
|
||||
->property('name', $schedule->name)
|
||||
->log();
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
|
@ -2,19 +2,65 @@
|
||||
|
||||
namespace App\Filament\Server\Resources\ScheduleResource\Pages;
|
||||
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Server\Resources\ScheduleResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use App\Models\Schedule;
|
||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ListSchedules extends ListRecords
|
||||
{
|
||||
protected static string $resource = ScheduleResource::class;
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->searchable(),
|
||||
TextColumn::make('cron')
|
||||
->state(fn (Schedule $schedule) => $schedule->cron_minute . ' ' . $schedule->cron_hour . ' ' . $schedule->cron_day_of_month . ' ' . $schedule->cron_month . ' ' . $schedule->cron_day_of_week),
|
||||
TextColumn::make('status')
|
||||
->state(fn (Schedule $schedule) => !$schedule->is_active ? 'Inactive' : ($schedule->is_processing ? 'Processing' : 'Active')),
|
||||
IconColumn::make('only_when_online')
|
||||
->boolean()
|
||||
->sortable(),
|
||||
DateTimeColumn::make('last_run_at')
|
||||
->label('Last run')
|
||||
->placeholder('Never')
|
||||
->since()
|
||||
->sortable(),
|
||||
DateTimeColumn::make('next_run_at')
|
||||
->label('Next run')
|
||||
->placeholder('Never')
|
||||
->since()
|
||||
->sortable()
|
||||
->state(fn (Schedule $schedule) => $schedule->is_active ? $schedule->next_run_at : null),
|
||||
])
|
||||
->actions([
|
||||
ViewAction::make(),
|
||||
EditAction::make(),
|
||||
DeleteAction::make()
|
||||
->after(function (Schedule $schedule) {
|
||||
Activity::event('server:schedule.delete')
|
||||
->subject($schedule)
|
||||
->property('name', $schedule->name)
|
||||
->log();
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make()
|
||||
->label('New Schedule'),
|
||||
Actions\CreateAction::make()->label('New Schedule'),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -7,8 +7,7 @@ use App\Filament\Server\Resources\ScheduleResource;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Schedule;
|
||||
use App\Services\Schedules\ProcessScheduleService;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
@ -19,7 +18,7 @@ class ViewSchedule extends ViewRecord
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('runNow')
|
||||
Actions\Action::make('runNow')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_SCHEDULE_UPDATE, Filament::getTenant()))
|
||||
->label(fn (Schedule $schedule) => $schedule->tasks->count() === 0 ? 'No tasks' : ($schedule->is_processing ? 'Processing' : 'Run now'))
|
||||
->color(fn (Schedule $schedule) => $schedule->tasks->count() === 0 || $schedule->is_processing ? 'warning' : 'primary')
|
||||
@ -34,7 +33,7 @@ class ViewSchedule extends ViewRecord
|
||||
|
||||
$this->fillForm();
|
||||
}),
|
||||
EditAction::make(),
|
||||
Actions\EditAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -6,10 +6,8 @@ use App\Enums\ContainerStatus;
|
||||
use App\Filament\Server\Components\SmallStatBlock;
|
||||
use App\Models\Server;
|
||||
use Carbon\CarbonInterface;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Widgets\StatsOverviewWidget;
|
||||
use Illuminate\Support\Number;
|
||||
use Livewire\Attributes\On;
|
||||
|
||||
class ServerOverview extends StatsOverviewWidget
|
||||
{
|
||||
@ -21,10 +19,14 @@ class ServerOverview extends StatsOverviewWidget
|
||||
{
|
||||
return [
|
||||
SmallStatBlock::make('Name', $this->server->name)
|
||||
->copyOnClick(fn () => request()->isSecure()),
|
||||
->extraAttributes([
|
||||
'class' => 'overflow-x-auto',
|
||||
]),
|
||||
SmallStatBlock::make('Status', $this->status()),
|
||||
SmallStatBlock::make('Address', $this->server->allocation->address)
|
||||
->copyOnClick(fn () => request()->isSecure()),
|
||||
->extraAttributes([
|
||||
'class' => 'overflow-x-auto',
|
||||
]),
|
||||
SmallStatBlock::make('CPU', $this->cpuUsage()),
|
||||
SmallStatBlock::make('Memory', $this->memoryUsage()),
|
||||
SmallStatBlock::make('Disk', $this->diskUsage()),
|
||||
@ -91,16 +93,4 @@ class ServerOverview extends StatsOverviewWidget
|
||||
|
||||
return $used . ($this->server->disk > 0 ? ' / ' . $total : ' / ∞');
|
||||
}
|
||||
|
||||
#[On('copyClick')]
|
||||
public function copyClick(string $value): void
|
||||
{
|
||||
$this->js("window.navigator.clipboard.writeText('{$value}');");
|
||||
|
||||
Notification::make()
|
||||
->title('Copied to clipboard')
|
||||
->body($value)
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ class NetworkAllocationController extends ClientApiController
|
||||
if ($original !== $allocation->notes) {
|
||||
Activity::event('server:allocation.notes')
|
||||
->subject($allocation)
|
||||
->property(['allocation' => $allocation->address, 'old' => $original, 'new' => $allocation->notes])
|
||||
->property(['allocation' => $allocation->toString(), 'old' => $original, 'new' => $allocation->notes])
|
||||
->log();
|
||||
}
|
||||
|
||||
@ -87,7 +87,7 @@ class NetworkAllocationController extends ClientApiController
|
||||
|
||||
Activity::event('server:allocation.primary')
|
||||
->subject($allocation)
|
||||
->property('allocation', $allocation->address)
|
||||
->property('allocation', $allocation->toString())
|
||||
->log();
|
||||
|
||||
return $this->fractal->item($allocation)
|
||||
@ -114,7 +114,7 @@ class NetworkAllocationController extends ClientApiController
|
||||
|
||||
Activity::event('server:allocation.create')
|
||||
->subject($allocation)
|
||||
->property('allocation', $allocation->address)
|
||||
->property('allocation', $allocation->toString())
|
||||
->log();
|
||||
|
||||
return $this->fractal->item($allocation)
|
||||
@ -148,7 +148,7 @@ class NetworkAllocationController extends ClientApiController
|
||||
|
||||
Activity::event('server:allocation.delete')
|
||||
->subject($allocation)
|
||||
->property('allocation', $allocation->address)
|
||||
->property('allocation', $allocation->toString())
|
||||
->log();
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
|
@ -4,7 +4,6 @@ namespace App\Http\Controllers\Api\Client\Servers;
|
||||
|
||||
use App\Facades\Activity;
|
||||
use App\Http\Controllers\Api\Client\ClientApiController;
|
||||
use App\Http\Requests\Api\Client\Servers\Settings\DescriptionServerRequest;
|
||||
use App\Http\Requests\Api\Client\Servers\Settings\ReinstallServerRequest;
|
||||
use App\Http\Requests\Api\Client\Servers\Settings\RenameServerRequest;
|
||||
use App\Http\Requests\Api\Client\Servers\Settings\SetDockerImageRequest;
|
||||
@ -35,36 +34,24 @@ class SettingsController extends ClientApiController
|
||||
public function rename(RenameServerRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$name = $request->input('name');
|
||||
$description = $request->has('description') ? (string) $request->input('description') : $server->description;
|
||||
|
||||
$server->update(['name' => $name]);
|
||||
|
||||
if ($server->wasChanged('name')) {
|
||||
if ($server->name !== $name) {
|
||||
Activity::event('server:settings.rename')
|
||||
->property(['old' => $server->getOriginal('name'), 'new' => $name])
|
||||
->property(['old' => $server->name, 'new' => $name])
|
||||
->log();
|
||||
$server->name = $name;
|
||||
}
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update server description
|
||||
*/
|
||||
public function description(DescriptionServerRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
if (!config('panel.editable_server_descriptions')) {
|
||||
return new JsonResponse([], Response::HTTP_FORBIDDEN);
|
||||
}
|
||||
|
||||
$description = $request->input('description');
|
||||
$server->update(['description' => $description ?? '']);
|
||||
|
||||
if ($server->wasChanged('description')) {
|
||||
if ($server->description !== $description && config('panel.editable_server_descriptions')) {
|
||||
Activity::event('server:settings.description')
|
||||
->property(['old' => $server->getOriginal('description'), 'new' => $description])
|
||||
->property(['old' => $server->description, 'new' => $description])
|
||||
->log();
|
||||
$server->description = $description;
|
||||
}
|
||||
|
||||
$server->save();
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
@ -93,7 +80,7 @@ class SettingsController extends ClientApiController
|
||||
*/
|
||||
public function dockerImage(SetDockerImageRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
if (!in_array($server->image, $server->egg->docker_images)) {
|
||||
if (!in_array($server->image, array_values($server->egg->docker_images))) {
|
||||
throw new BadRequestHttpException('This server\'s Docker image has been manually set by an administrator and cannot be updated.');
|
||||
}
|
||||
|
||||
|
@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Client\Servers\Settings;
|
||||
|
||||
use App\Contracts\Http\ClientPermissionsRequest;
|
||||
use App\Http\Requests\Api\Client\ClientApiRequest;
|
||||
use App\Models\Permission;
|
||||
|
||||
class DescriptionServerRequest extends ClientApiRequest implements ClientPermissionsRequest
|
||||
{
|
||||
/**
|
||||
* Returns the permissions string indicating which permission should be used to
|
||||
* validate that the authenticated user has permission to perform this action against
|
||||
* the given resource (server).
|
||||
*/
|
||||
public function permission(): string
|
||||
{
|
||||
return Permission::ACTION_SETTINGS_DESCRIPTION;
|
||||
}
|
||||
|
||||
/**
|
||||
* The rules to apply when validating this request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'description' => 'string|nullable',
|
||||
];
|
||||
}
|
||||
}
|
@ -26,6 +26,7 @@ class RenameServerRequest extends ClientApiRequest implements ClientPermissionsR
|
||||
{
|
||||
return [
|
||||
'name' => Server::getRules()['name'],
|
||||
'description' => 'string|nullable',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ abstract class SubuserRequest extends ClientApiRequest
|
||||
$server = $this->route()->parameter('server');
|
||||
|
||||
// If we are an admin or the server owner, no need to perform these checks.
|
||||
if ($user->can('update', $server) || $user->id === $server->owner_id) {
|
||||
if ($user->can('update server', $server) || $user->id === $server->owner_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
35
app/Jobs/NodeStatistics.php
Normal file
35
app/Jobs/NodeStatistics.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Node;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class NodeStatistics implements ShouldBeUnique, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
foreach (Node::all() as $node) {
|
||||
$stats = $node->statistics();
|
||||
$timestamp = now()->getTimestamp();
|
||||
|
||||
foreach ($stats as $key => $value) {
|
||||
$cacheKey = "nodes.{$node->id}.$key";
|
||||
$data = cache()->get($cacheKey, []);
|
||||
|
||||
// Add current timestamp and value to the data array
|
||||
$data[$timestamp] = $value;
|
||||
|
||||
// Update the cache with the new data, expires in 1 minute
|
||||
cache()->put($cacheKey, $data, now()->addMinute());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ namespace App\Listeners\Auth;
|
||||
|
||||
use App\Facades\Activity;
|
||||
use Illuminate\Auth\Events\Failed;
|
||||
use Illuminate\Auth\Events\Login;
|
||||
use App\Events\Auth\DirectLogin;
|
||||
|
||||
class AuthenticationListener
|
||||
{
|
||||
@ -12,10 +12,9 @@ class AuthenticationListener
|
||||
* Handles an authentication event by logging the user and information about
|
||||
* the request.
|
||||
*/
|
||||
public function handle(Failed|Login $event): void
|
||||
public function handle(Failed|DirectLogin $event): void
|
||||
{
|
||||
$activity = Activity::withRequestMetadata();
|
||||
|
||||
if ($event->user) {
|
||||
$activity = $activity->subject($event->user);
|
||||
}
|
||||
|
@ -2,14 +2,22 @@
|
||||
|
||||
namespace App\Listeners\Auth;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Facades\Activity;
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
|
||||
class PasswordResetListener
|
||||
{
|
||||
protected Request $request;
|
||||
|
||||
public function __construct(Request $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function handle(PasswordReset $event): void
|
||||
{
|
||||
Activity::event('auth:password-reset')
|
||||
Activity::event('event:password-reset')
|
||||
->withRequestMetadata()
|
||||
->subject($event->user)
|
||||
->log();
|
||||
|
@ -1,64 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\View\View;
|
||||
use Livewire\Component;
|
||||
|
||||
class ServerEntry extends Component
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.server-entry');
|
||||
}
|
||||
|
||||
public function placeholder(): string
|
||||
{
|
||||
return <<<'HTML'
|
||||
<div class="relative">
|
||||
<div
|
||||
class="absolute left-0 top-1 bottom-0 w-1 rounded-lg"
|
||||
style="background-color: #D97706;">
|
||||
</div>
|
||||
|
||||
<div class="flex-1 dark:bg-gray-800 dark:text-white rounded-lg overflow-hidden p-3">
|
||||
<div class="flex items-center mb-5 gap-2">
|
||||
<x-filament::loading-indicator class="h-5 w-5" />
|
||||
<h2 class="text-xl font-bold">
|
||||
{{ $server->name }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between text-center">
|
||||
<div>
|
||||
<p class="text-sm dark:text-gray-400">CPU</p>
|
||||
<p class="text-md font-semibold">{{ Number::format(0, precision: 2, locale: auth()->user()->language ?? 'en') . '%' }}</p>
|
||||
<hr class="p-0.5">
|
||||
<p class="text-xs dark:text-gray-400">{{ $server->formatResource('cpu', type: \App\Enums\ServerResourceType::Percentage, limit: true) }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm dark:text-gray-400">Memory</p>
|
||||
<p class="text-md font-semibold">{{ convert_bytes_to_readable(0, decimals: 2) }}</p>
|
||||
<hr class="p-0.5">
|
||||
<p class="text-xs dark:text-gray-400">{{ $server->formatResource('memory', limit: true) }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm dark:text-gray-400">Disk</p>
|
||||
<p class="text-md font-semibold">{{ convert_bytes_to_readable(0, decimals: 2) }}</p>
|
||||
<hr class="p-0.5">
|
||||
<p class="text-xs dark:text-gray-400">{{ $server->formatResource('disk', limit: true) }}</p>
|
||||
</div>
|
||||
<div class="hidden sm:block">
|
||||
<p class="text-sm dark:text-gray-400">Network</p>
|
||||
<hr class="p-0.5">
|
||||
<p class="text-md font-semibold">{{ $server->allocation->address }} </p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
}
|
@ -121,6 +121,11 @@ class Allocation extends Model
|
||||
);
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
return $this->address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets information for the server associated with this allocation.
|
||||
*/
|
||||
|
@ -38,7 +38,6 @@ use Symfony\Component\Yaml\Yaml;
|
||||
* @property string $daemon_token_id
|
||||
* @property string $daemon_token
|
||||
* @property int $daemon_listen
|
||||
* @property int $daemon_connect
|
||||
* @property int $daemon_sftp
|
||||
* @property string|null $daemon_sftp_alias
|
||||
* @property string $daemon_base
|
||||
@ -84,7 +83,7 @@ class Node extends Model implements Validatable
|
||||
'memory', 'memory_overallocate', 'disk',
|
||||
'disk_overallocate', 'cpu', 'cpu_overallocate',
|
||||
'upload_size', 'daemon_base',
|
||||
'daemon_sftp', 'daemon_sftp_alias', 'daemon_listen', 'daemon_connect',
|
||||
'daemon_sftp', 'daemon_sftp_alias', 'daemon_listen',
|
||||
'description', 'maintenance_mode', 'tags',
|
||||
];
|
||||
|
||||
@ -106,7 +105,6 @@ class Node extends Model implements Validatable
|
||||
'daemon_sftp' => ['required', 'numeric', 'between:1,65535'],
|
||||
'daemon_sftp_alias' => ['nullable', 'string'],
|
||||
'daemon_listen' => ['required', 'numeric', 'between:1,65535'],
|
||||
'daemon_connect' => ['required', 'numeric', 'between:1,65535'],
|
||||
'maintenance_mode' => ['boolean'],
|
||||
'upload_size' => ['int', 'between:1,1024'],
|
||||
'tags' => ['array'],
|
||||
@ -127,7 +125,6 @@ class Node extends Model implements Validatable
|
||||
'daemon_base' => '/var/lib/pelican/volumes',
|
||||
'daemon_sftp' => 2022,
|
||||
'daemon_listen' => 8080,
|
||||
'daemon_connect' => 8080,
|
||||
'maintenance_mode' => false,
|
||||
'tags' => '[]',
|
||||
];
|
||||
@ -139,7 +136,6 @@ class Node extends Model implements Validatable
|
||||
'disk' => 'integer',
|
||||
'cpu' => 'integer',
|
||||
'daemon_listen' => 'integer',
|
||||
'daemon_connect' => 'integer',
|
||||
'daemon_sftp' => 'integer',
|
||||
'daemon_token' => 'encrypted',
|
||||
'behind_proxy' => 'boolean',
|
||||
@ -175,7 +171,7 @@ class Node extends Model implements Validatable
|
||||
*/
|
||||
public function getConnectionAddress(): string
|
||||
{
|
||||
return "$this->scheme://$this->fqdn:$this->daemon_connect";
|
||||
return "$this->scheme://$this->fqdn:$this->daemon_listen";
|
||||
}
|
||||
|
||||
/**
|
||||
@ -333,6 +329,26 @@ class Node extends Model implements Validatable
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array-key, mixed>
|
||||
*/
|
||||
public function serverStatuses(): array
|
||||
{
|
||||
$statuses = [];
|
||||
try {
|
||||
$statuses = Http::daemon($this)->connectTimeout(1)->timeout(1)->get('/api/servers')->json() ?? [];
|
||||
} catch (Exception $exception) {
|
||||
report($exception);
|
||||
}
|
||||
|
||||
foreach ($statuses as $status) {
|
||||
$uuid = fluent($status)->get('configuration.uuid');
|
||||
cache()->remember("servers.$uuid.container.status", now()->addMinute(), fn () => fluent($status)->get('state'));
|
||||
}
|
||||
|
||||
return $statuses;
|
||||
}
|
||||
|
||||
/** @return array{
|
||||
* memory_total: int, memory_used: int,
|
||||
* swap_total: int, swap_used: int,
|
||||
|
@ -97,8 +97,6 @@ class Permission extends Model implements Validatable
|
||||
|
||||
public const ACTION_SETTINGS_RENAME = 'settings.rename';
|
||||
|
||||
public const ACTION_SETTINGS_DESCRIPTION = 'settings.description';
|
||||
|
||||
public const ACTION_SETTINGS_REINSTALL = 'settings.reinstall';
|
||||
|
||||
public const ACTION_ACTIVITY_READ = 'activity.read';
|
||||
@ -178,7 +176,7 @@ class Permission extends Model implements Validatable
|
||||
[
|
||||
'name' => 'settings',
|
||||
'icon' => 'tabler-settings',
|
||||
'permissions' => ['rename', 'description', 'reinstall'],
|
||||
'permissions' => ['rename', 'reinstall'],
|
||||
],
|
||||
[
|
||||
'name' => 'activity',
|
||||
|
@ -435,24 +435,25 @@ class Server extends Model implements Validatable
|
||||
|
||||
public function retrieveStatus(): ContainerStatus
|
||||
{
|
||||
return cache()->remember("servers.$this->uuid.status", now()->addSeconds(15), function () {
|
||||
// @phpstan-ignore myCustomRules.forbiddenGlobalFunctions
|
||||
$details = app(DaemonServerRepository::class)->setServer($this)->getDetails();
|
||||
$status = cache()->get("servers.$this->uuid.container.status");
|
||||
|
||||
return ContainerStatus::tryFrom(Arr::get($details, 'state')) ?? ContainerStatus::Missing;
|
||||
});
|
||||
if ($status === null) {
|
||||
$this->node->serverStatuses();
|
||||
|
||||
$status = cache()->get("servers.$this->uuid.container.status");
|
||||
}
|
||||
|
||||
return ContainerStatus::tryFrom($status) ?? ContainerStatus::Missing;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public function retrieveResources(): array
|
||||
public function resources(): array
|
||||
{
|
||||
return cache()->remember("servers.$this->uuid.resources", now()->addSeconds(15), function () {
|
||||
return cache()->remember("resources:$this->uuid", now()->addSeconds(15), function () {
|
||||
// @phpstan-ignore myCustomRules.forbiddenGlobalFunctions
|
||||
$details = app(DaemonServerRepository::class)->setServer($this)->getDetails();
|
||||
|
||||
return Arr::get($details, 'utilization', []);
|
||||
return Arr::get(app(DaemonServerRepository::class)->setServer($this)->getDetails(), 'utilization', []);
|
||||
});
|
||||
}
|
||||
|
||||
@ -460,7 +461,7 @@ class Server extends Model implements Validatable
|
||||
{
|
||||
$resourceAmount = $this->{$resourceKey} ?? 0;
|
||||
if (!$limit) {
|
||||
$resourceAmount = $this->retrieveResources()[$resourceKey] ?? 0;
|
||||
$resourceAmount = $this->resources()[$resourceKey] ?? 0;
|
||||
}
|
||||
|
||||
if ($type === ServerResourceType::Time) {
|
||||
@ -493,7 +494,7 @@ class Server extends Model implements Validatable
|
||||
public function condition(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => $this->status ?? $this->retrieveStatus(),
|
||||
get: fn () => $this->isSuspended() ? ServerState::Suspended : $this->status ?? $this->retrieveStatus(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -265,13 +265,8 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
*/
|
||||
public function accessibleServers(): Builder
|
||||
{
|
||||
if ($this->canned('viewAny', Server::class)) {
|
||||
return Server::select('servers.*')
|
||||
->leftJoin('subusers', 'subusers.server_id', '=', 'servers.id')
|
||||
->where(function (Builder $builder) {
|
||||
$builder->where('servers.owner_id', $this->id)->orWhere('subusers.user_id', $this->id)->orWhereIn('servers.node_id', $this->accessibleNodes()->pluck('id'));
|
||||
})
|
||||
->distinct('servers.id');
|
||||
if ($this->canned('viewList server')) {
|
||||
return Server::query();
|
||||
}
|
||||
|
||||
return $this->directAccessibleServers();
|
||||
@ -283,7 +278,8 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
*/
|
||||
public function directAccessibleServers(): Builder
|
||||
{
|
||||
return Server::select('servers.*')
|
||||
return Server::query()
|
||||
->select('servers.*')
|
||||
->leftJoin('subusers', 'subusers.server_id', '=', 'servers.id')
|
||||
->where(function (Builder $builder) {
|
||||
$builder->where('servers.owner_id', $this->id)->orWhere('subusers.user_id', $this->id);
|
||||
@ -318,12 +314,12 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
|
||||
protected function checkPermission(Server $server, string $permission = ''): bool
|
||||
{
|
||||
if ($this->canned('update', $server) || $server->owner_id === $this->id) {
|
||||
if ($this->canned('update server', $server) || $server->owner_id === $this->id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the user only has "view" permissions allow viewing the console
|
||||
if ($permission === Permission::ACTION_WEBSOCKET_CONNECT && $this->canned('view', $server)) {
|
||||
if ($permission === Permission::ACTION_WEBSOCKET_CONNECT && $this->canned('view server', $server)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -438,7 +434,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
public function canAccessTenant(Model $tenant): bool
|
||||
{
|
||||
if ($tenant instanceof Server) {
|
||||
if ($this->canned('view', $tenant) || $tenant->owner_id === $this->id) {
|
||||
if ($this->canned('view server', $tenant) || $tenant->owner_id === $this->id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,7 @@ class AccountCreated extends Notification implements ShouldQueue
|
||||
->line('Email: ' . $notifiable->email);
|
||||
|
||||
if (!is_null($this->token)) {
|
||||
return $message->action('Setup Your Account', Filament::getPanel('app')->getResetPasswordUrl($this->token, $notifiable));
|
||||
return $message->action('Setup Your Account', Filament::getResetPasswordUrl($this->token, $notifiable));
|
||||
}
|
||||
|
||||
return $message;
|
||||
|
@ -21,6 +21,11 @@ class ServerPolicy
|
||||
return null;
|
||||
}
|
||||
|
||||
// Make sure user can target node of the server
|
||||
if (!$user->canTarget($server->node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Owner has full server permissions
|
||||
if ($server->owner_id === $user->id) {
|
||||
return true;
|
||||
@ -32,11 +37,6 @@ class ServerPolicy
|
||||
return true;
|
||||
}
|
||||
|
||||
// Make sure user can target node of the server
|
||||
if (!$user->canTarget($server->node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Return null to let default policies take over
|
||||
return null;
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ class ServerPanelProvider extends PanelProvider
|
||||
->navigationItems([
|
||||
NavigationItem::make('Open in Admin')
|
||||
->url(fn () => EditServer::getUrl(['record' => Filament::getTenant()], panel: 'admin'))
|
||||
->visible(fn () => auth()->user()->canAccessPanel(Filament::getPanel('admin')) && auth()->user()->can('view server', Filament::getTenant()))
|
||||
->visible(fn () => auth()->user()->can('view server', Filament::getTenant()))
|
||||
->icon('tabler-arrow-back')
|
||||
->sort(99),
|
||||
])
|
||||
|
@ -19,7 +19,7 @@ class DaemonServerRepository extends DaemonRepository
|
||||
public function getDetails(): array
|
||||
{
|
||||
try {
|
||||
return $this->getHttpClient()->connectTimeout(1)->timeout(1)->get("/api/servers/{$this->server->uuid}")->throw()->json();
|
||||
return $this->getHttpClient()->get("/api/servers/{$this->server->uuid}")->throw()->json();
|
||||
} catch (RequestException $exception) {
|
||||
$cfId = $exception->response->header('Cf-Ray');
|
||||
$cfCache = $exception->response->header('Cf-Cache-Status');
|
||||
|
@ -6,11 +6,12 @@ use App\Extensions\Filesystem\S3Filesystem;
|
||||
use Aws\S3\S3Client;
|
||||
use Illuminate\Http\Response;
|
||||
use App\Models\Backup;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use App\Extensions\Backups\BackupManager;
|
||||
use App\Repositories\Daemon\DaemonBackupRepository;
|
||||
use App\Exceptions\Service\Backup\BackupLockedException;
|
||||
use Exception;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
|
||||
class DeleteBackupService
|
||||
{
|
||||
@ -47,10 +48,12 @@ class DeleteBackupService
|
||||
$this->connection->transaction(function () use ($backup) {
|
||||
try {
|
||||
$this->daemonBackupRepository->setServer($backup->server)->delete($backup);
|
||||
} catch (Exception $exception) {
|
||||
} catch (ConnectionException $exception) {
|
||||
$previous = $exception->getPrevious();
|
||||
|
||||
// Don't fail the request if the Daemon responds with a 404, just assume the backup
|
||||
// doesn't actually exist and remove its reference from the Panel as well.
|
||||
if ($exception->getCode() !== Response::HTTP_NOT_FOUND) {
|
||||
if (!$previous instanceof ClientException || $previous->getResponse()->getStatusCode() !== Response::HTTP_NOT_FOUND) {
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
@ -16,8 +16,8 @@ class GetUserPermissionsService
|
||||
*/
|
||||
public function handle(Server $server, User $user): array
|
||||
{
|
||||
if ($user->isAdmin() && ($user->can('view', $server) || $user->can('update', $server))) {
|
||||
$permissions = $user->can('update', $server) ? ['*'] : ['websocket.connect', 'backup.read'];
|
||||
if ($user->isAdmin() && ($user->can('view server', $server) || $user->can('update server', $server))) {
|
||||
$permissions = $user->can('update server', $server) ? ['*'] : ['websocket.connect', 'backup.read'];
|
||||
|
||||
$permissions[] = 'admin.websocket.errors';
|
||||
$permissions[] = 'admin.websocket.install';
|
||||
|
@ -47,7 +47,7 @@ class TransferServerService
|
||||
|
||||
// Check if the node is viable for the transfer.
|
||||
$node = Node::query()
|
||||
->select(['nodes.id', 'nodes.fqdn', 'nodes.scheme', 'nodes.daemon_token', 'nodes.daemon_connect', 'nodes.memory', 'nodes.disk', 'nodes.cpu', 'nodes.memory_overallocate', 'nodes.disk_overallocate', 'nodes.cpu_overallocate'])
|
||||
->select(['nodes.id', 'nodes.fqdn', 'nodes.scheme', 'nodes.daemon_token', 'nodes.daemon_listen', 'nodes.memory', 'nodes.disk', 'nodes.cpu', 'nodes.memory_overallocate', 'nodes.disk_overallocate', 'nodes.cpu_overallocate'])
|
||||
->withSum('servers', 'disk')
|
||||
->withSum('servers', 'memory')
|
||||
->withSum('servers', 'cpu')
|
||||
|
@ -2,20 +2,54 @@
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use Illuminate\Support\Env;
|
||||
use RuntimeException;
|
||||
use Exception;
|
||||
|
||||
trait EnvironmentWriterTrait
|
||||
{
|
||||
/**
|
||||
* Escapes an environment value by looking for any characters that could
|
||||
* reasonably cause environment parsing issues. Those values are then wrapped
|
||||
* in quotes before being returned.
|
||||
*/
|
||||
public function escapeEnvironmentValue(string $value): string
|
||||
{
|
||||
if (!preg_match('/^\"(.*)\"$/', $value) && preg_match('/([^\w.\-+\/])+/', $value)) {
|
||||
return sprintf('"%s"', addcslashes($value, '\\"'));
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the .env file for the application using the passed in values.
|
||||
*
|
||||
* @param array<string, mixed> $values
|
||||
*
|
||||
* @throws RuntimeException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function writeToEnvironment(array $values = []): void
|
||||
{
|
||||
Env::writeVariables($values, base_path('.env'), true);
|
||||
$path = base_path('.env');
|
||||
if (!file_exists($path)) {
|
||||
throw new Exception('Cannot locate .env file, was this software installed correctly?');
|
||||
}
|
||||
|
||||
$saveContents = file_get_contents($path);
|
||||
if ($saveContents === false) {
|
||||
$saveContents = '';
|
||||
}
|
||||
|
||||
collect($values)->each(function ($value, $key) use (&$saveContents) {
|
||||
$key = strtoupper($key);
|
||||
$saveValue = sprintf('%s=%s', $key, $this->escapeEnvironmentValue($value ?? ''));
|
||||
|
||||
if (preg_match_all('/^' . $key . '=(.*)$/m', $saveContents) < 1) {
|
||||
$saveContents = $saveContents . PHP_EOL . $saveValue;
|
||||
} else {
|
||||
$saveContents = preg_replace('/^' . $key . '=(.*)$/m', $saveValue, $saveContents);
|
||||
}
|
||||
});
|
||||
|
||||
file_put_contents($path, $saveContents);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
{
|
||||
"name": "pelican-dev/panel",
|
||||
"description": "The free, open-source game management panel. Supporting Minecraft, Spigot, BungeeCord, and SRCDS servers.",
|
||||
"license": "AGPL-3.0-only",
|
||||
"require": {
|
||||
"php": "^8.2 || ^8.3 || ^8.4",
|
||||
"ext-intl": "*",
|
||||
@ -10,17 +9,18 @@
|
||||
"ext-pdo": "*",
|
||||
"ext-zip": "*",
|
||||
"abdelhamiderrahmouni/filament-monaco-editor": "^0.2.5",
|
||||
"aws/aws-sdk-php": "^3.344",
|
||||
"aws/aws-sdk-php": "^3.343",
|
||||
"aymanalhattami/filament-context-menu": "^1.0",
|
||||
"calebporzio/sushi": "^2.5",
|
||||
"chillerlan/php-qrcode": "^5.0.2",
|
||||
"dedoc/scramble": "^0.12.10",
|
||||
"doctrine/dbal": "~3.6.0",
|
||||
"filament/filament": "^3.3",
|
||||
"guzzlehttp/guzzle": "^7.9",
|
||||
"laravel/framework": "^12.18",
|
||||
"laravel/framework": "^12.13",
|
||||
"laravel/helpers": "^1.7",
|
||||
"laravel/sanctum": "^4.1",
|
||||
"laravel/socialite": "^5.21",
|
||||
"laravel/socialite": "^5.20",
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"laravel/ui": "^4.6",
|
||||
"lcobucci/jwt": "~4.3.0",
|
||||
@ -37,7 +37,7 @@
|
||||
"spatie/laravel-data": "^4.15",
|
||||
"spatie/laravel-fractal": "^6.3",
|
||||
"spatie/laravel-health": "^1.34",
|
||||
"spatie/laravel-permission": "^6.19",
|
||||
"spatie/laravel-permission": "^6.17",
|
||||
"spatie/laravel-query-builder": "^6.3",
|
||||
"spatie/temporary-directory": "^2.3",
|
||||
"symfony/http-client": "^7.2",
|
||||
@ -50,7 +50,7 @@
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-ide-helper": "^3.5",
|
||||
"fakerphp/faker": "^1.23.1",
|
||||
"larastan/larastan": "^3.4",
|
||||
"larastan/larastan": "3.x-dev#5bd1c40edb43a727584081e74e9a1a2a201ea2ee",
|
||||
"laravel/pail": "^1.2.2",
|
||||
"laravel/pint": "^1.15.3",
|
||||
"laravel/sail": "^1.41",
|
||||
|
826
composer.lock
generated
826
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -38,7 +38,6 @@ class NodeFactory extends Factory
|
||||
'daemon_token_id' => Str::random(Node::DAEMON_TOKEN_ID_LENGTH),
|
||||
'daemon_token' => Str::random(Node::DAEMON_TOKEN_LENGTH),
|
||||
'daemon_listen' => 8080,
|
||||
'daemon_connect' => 8080,
|
||||
'daemon_sftp' => 2022,
|
||||
'daemon_base' => '/var/lib/panel/volumes',
|
||||
'maintenance_mode' => false,
|
||||
|
@ -4,7 +4,7 @@
|
||||
"version": "PLCN_v1",
|
||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/rust\/egg-rust.json"
|
||||
},
|
||||
"exported_at": "2025-06-06T11:57:17+00:00",
|
||||
"exported_at": "2025-03-18T12:36:48+00:00",
|
||||
"name": "Rust",
|
||||
"author": "panel@example.com",
|
||||
"uuid": "bace2dfb-209c-452a-9459-7d6f340b07ae",
|
||||
@ -20,7 +20,7 @@
|
||||
"ghcr.io\/parkervcp\/games:rust": "ghcr.io\/parkervcp\/games:rust"
|
||||
},
|
||||
"file_denylist": [],
|
||||
"startup": ".\/RustDedicated -batchmode +server.port {{SERVER_PORT}} +server.queryport {{QUERY_PORT}} +server.identity \"rust\" +rcon.port {{RCON_PORT}} +rcon.web true +server.hostname \\\"{{SERVER_HOSTNAME}}\\\" +server.level \\\"{{LEVEL}}\\\" +server.description \\\"{{DESCRIPTION}}\\\" +server.url \\\"{{SERVER_URL}}\\\" +server.headerimage \\\"{{SERVER_IMG}}\\\" +server.logoimage \\\"{{SERVER_LOGO}}\\\" +server.maxplayers {{MAX_PLAYERS}} +rcon.password \\\"{{RCON_PASS}}\\\" +server.saveinterval {{SAVEINTERVAL}} +app.port {{APP_PORT}} $( [ -z ${MAP_URL} ] && printf %s \"+server.worldsize \\\"{{WORLD_SIZE}}\\\" +server.seed \\\"{{WORLD_SEED}}\\\"\" || printf %s \"+server.levelurl {{MAP_URL}}\" ) {{ADDITIONAL_ARGS}}",
|
||||
"startup": ".\/RustDedicated -batchmode +server.port {{SERVER_PORT}} +server.queryport {{QUERY_PORT}} +server.identity \"rust\" +rcon.port {{RCON_PORT}} +rcon.web true +server.hostname \\\"{{HOSTNAME}}\\\" +server.level \\\"{{LEVEL}}\\\" +server.description \\\"{{DESCRIPTION}}\\\" +server.url \\\"{{SERVER_URL}}\\\" +server.headerimage \\\"{{SERVER_IMG}}\\\" +server.logoimage \\\"{{SERVER_LOGO}}\\\" +server.maxplayers {{MAX_PLAYERS}} +rcon.password \\\"{{RCON_PASS}}\\\" +server.saveinterval {{SAVEINTERVAL}} +app.port {{APP_PORT}} $( [ -z ${MAP_URL} ] && printf %s \"+server.worldsize \\\"{{WORLD_SIZE}}\\\" +server.seed \\\"{{WORLD_SEED}}\\\"\" || printf %s \"+server.levelurl {{MAP_URL}}\" ) {{ADDITIONAL_ARGS}}",
|
||||
"config": {
|
||||
"files": "{}",
|
||||
"startup": "{\r\n \"done\": \"Server startup complete\"\r\n}",
|
||||
@ -38,7 +38,7 @@
|
||||
{
|
||||
"name": "Server Name",
|
||||
"description": "The name of your server in the public server list.",
|
||||
"env_variable": "SERVER_HOSTNAME",
|
||||
"env_variable": "HOSTNAME",
|
||||
"default_value": "A Rust Server",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
|
@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('nodes', function (Blueprint $table) {
|
||||
$table->smallInteger('daemon_connect')->unsigned()->default(8080);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('nodes', function (Blueprint $table) {
|
||||
$table->dropColumn('daemon_connect');
|
||||
});
|
||||
}
|
||||
};
|
@ -45,10 +45,6 @@ return [
|
||||
'port' => 'Port',
|
||||
'ports' => 'Ports',
|
||||
'port_help' => 'If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.',
|
||||
'connect_port' => 'Connection Port',
|
||||
'connect_port_help' => 'Connections to wings will use this port. If you are using a reverse proxy this can differ from the listen port. When using Cloudflare proxy you should use 8443.',
|
||||
'listen_port' => 'Listening Port',
|
||||
'listen_port_help' => 'Wings will listen on this port.',
|
||||
'display_name' => 'Display Name',
|
||||
'ssl' => 'Communicate over SSL',
|
||||
'panel_on_ssl' => 'Your Panel is using a secure SSL connection,<br>so your Daemon must too.',
|
||||
|
@ -16,7 +16,6 @@ return [
|
||||
'failed' => 'These credentials do not match our records.',
|
||||
'failed-two-factor' => 'Incorrect 2FA Code',
|
||||
'two-factor-code' => 'Two Factor Code',
|
||||
'two-factor-hint' => 'You may use backup codes if you lost access to your device.',
|
||||
'password' => 'The provided password is incorrect.',
|
||||
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
|
||||
'2fa_must_be_enabled' => 'The administrator has required that 2-Factor Authentication must be enabled for your account in order to use the Panel.',
|
||||
|
@ -36,7 +36,6 @@ return [
|
||||
'cpu_overallocate' => 'Enter the amount of cpu to over allocate by, -1 will disable checking and 0 will prevent creating new server',
|
||||
'upload_size' => "'Enter the maximum filesize upload",
|
||||
'daemonListen' => 'Enter the daemon listening port',
|
||||
'daemonConnect' => 'Enter the daemon connecting port (can be same as listen port)',
|
||||
'daemonSFTP' => 'Enter the daemon SFTP listening port',
|
||||
'daemonSFTPAlias' => 'Enter the daemon SFTP alias (can be empty)',
|
||||
'daemonBase' => 'Enter the base folder',
|
||||
|
@ -16,8 +16,7 @@ return [
|
||||
'startup_update' => 'Allows a user to modify the startup variables for the server.',
|
||||
'startup_docker_image' => 'Allows a user to modify the Docker image used when running the server.',
|
||||
'settings_reinstall' => 'Allows a user to trigger a reinstall of this server.',
|
||||
'settings_rename' => 'Allows a user to rename this server.',
|
||||
'settings_description' => 'Allows a user to change the description of this server.',
|
||||
'settings_rename' => 'Allows a user to rename this server and change the description of it.',
|
||||
'activity_read' => 'Allows a user to view the activity logs for the server.',
|
||||
'websocket_*' => 'Allows a user access to the websocket for this server.',
|
||||
'control_console' => 'Allows a user to send data to the server console.',
|
||||
|
File diff suppressed because one or more lines are too long
@ -5,7 +5,7 @@
|
||||
$userFontSize = auth()->user()->getCustomization()['console_font_size'] ?? 14;
|
||||
$userRows = auth()->user()->getCustomization()['console_rows'] ?? 30;
|
||||
@endphp
|
||||
@if($userFont !== "monospace")
|
||||
@if($userFont)
|
||||
<link rel="preload" href="{{ asset("storage/fonts/{$userFont}.ttf") }}" as="font" crossorigin>
|
||||
<style>
|
||||
@font-face {
|
||||
|
@ -0,0 +1,10 @@
|
||||
<div class="fi-wi-stats-overview-stat relative rounded-lg bg-white p-4 shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10">
|
||||
<div class="grid grid-flow-row">
|
||||
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
{{ $getLabel() }}
|
||||
</span>
|
||||
<div class="text-xl font-semibold text-gray-950 dark:text-white">
|
||||
{{ $getValue() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,14 +1,9 @@
|
||||
<div class="fi-small-stat-block grid grid-flow-row w-full p-3 rounded-lg bg-white shadow-sm overflow-hidden overflow-x-auto ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10">
|
||||
@if ($shouldCopyOnClick())
|
||||
<span class="cursor-pointer" wire:click="copyClick('{{ $getValue() }}')">
|
||||
@else
|
||||
<div
|
||||
class="grid grid-flow-row w-full p-3 rounded-lg bg-white shadow-sm overflow-hidden ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10">
|
||||
<span>
|
||||
@endif
|
||||
<span class="text-md font-medium text-gray-500 dark:text-gray-400">
|
||||
{{ $getLabel() }}
|
||||
</span>
|
||||
<span class="text-md font-semibold">
|
||||
{{ $getValue() }}
|
||||
</span>
|
||||
<span class="text-md font-semibold">{{ $getValue() }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -176,5 +176,5 @@
|
||||
x-text="monacoPlaceholderText"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</x-dynamic-component>
|
||||
|
@ -1,8 +0,0 @@
|
||||
@php
|
||||
/** @var \App\Models\Server $server */
|
||||
$server = $getRecord();
|
||||
@endphp
|
||||
|
||||
<div class="w-full">
|
||||
@livewire('server-entry', ['server' => $server, 'lazy' => true], key($server->id))
|
||||
</div>
|
@ -1,11 +1,25 @@
|
||||
<div wire:poll.15s class="relative">
|
||||
<a href="{{ \App\Filament\Server\Pages\Console::getUrl(panel: 'server', tenant: $server) }}" wire:navigate>
|
||||
@php
|
||||
use App\Enums\ServerResourceType;
|
||||
|
||||
/** @var \App\Models\Server $server */
|
||||
$server = $getRecord();
|
||||
@endphp
|
||||
|
||||
<head>
|
||||
<style>
|
||||
hr {
|
||||
border-color: #9ca3af;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<div class="w-full">
|
||||
<div class="relative">
|
||||
<div
|
||||
class="absolute left-0 top-1 bottom-0 w-1 rounded-lg"
|
||||
style="background-color: {{ $server->condition->getColor(true) }};">
|
||||
</div>
|
||||
|
||||
<div class="flex-1 dark:bg-gray-800 dark:text-white rounded-lg overflow-hidden p-3">
|
||||
<div class="flex-1 bg-gray-800 dark:text-white rounded-lg overflow-hidden p-3">
|
||||
<div class="flex items-center mb-5 gap-2">
|
||||
<x-filament::icon-button
|
||||
:icon="$server->condition->getIcon()"
|
||||
@ -15,16 +29,16 @@
|
||||
/>
|
||||
<h2 class="text-xl font-bold">
|
||||
{{ $server->name }}
|
||||
<span class="dark:text-gray-400">({{ $server->formatResource('uptime', type: \App\Enums\ServerResourceType::Time) }})</span>
|
||||
<span class="dark:text-gray-400">({{ $server->formatResource('uptime', type: ServerResourceType::Time) }})</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between text-center">
|
||||
<div>
|
||||
<p class="text-sm dark:text-gray-400">CPU</p>
|
||||
<p class="text-md font-semibold">{{ $server->formatResource('cpu_absolute', type: \App\Enums\ServerResourceType::Percentage) }}</p>
|
||||
<p class="text-md font-semibold">{{ $server->formatResource('cpu_absolute', type: ServerResourceType::Percentage) }}</p>
|
||||
<hr class="p-0.5">
|
||||
<p class="text-xs dark:text-gray-400">{{ $server->formatResource('cpu', type: \App\Enums\ServerResourceType::Percentage, limit: true) }}</p>
|
||||
<p class="text-xs dark:text-gray-400">{{ $server->formatResource('cpu', type: ServerResourceType::Percentage, limit: true) }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm dark:text-gray-400">Memory</p>
|
||||
@ -45,11 +59,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<x-filament-tables::actions
|
||||
:actions="\App\Filament\App\Resources\ServerResource\Pages\ListServers::getPowerActions()"
|
||||
:alignment="\Filament\Support\Enums\Alignment::Center"
|
||||
:record="$server"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
@ -129,7 +129,6 @@ Route::prefix('/servers/{server:uuid}')->middleware([ServerSubject::class, Authe
|
||||
|
||||
Route::prefix('/settings')->group(function () {
|
||||
Route::post('/rename', [Client\Servers\SettingsController::class, 'rename']);
|
||||
Route::post('/description', [Client\Servers\SettingsController::class, 'description']);
|
||||
Route::post('/reinstall', [Client\Servers\SettingsController::class, 'reinstall']);
|
||||
Route::put('/docker-image', [Client\Servers\SettingsController::class, 'dockerImage']);
|
||||
});
|
||||
|
0
storage/app/packs/.githold
Executable file
0
storage/app/packs/.githold
Executable file
@ -21,9 +21,11 @@ class SettingsControllerTest extends ClientApiIntegrationTestCase
|
||||
/** @var \App\Models\Server $server */
|
||||
[$user, $server] = $this->generateTestAccount($permissions);
|
||||
$originalName = $server->name;
|
||||
$originalDescription = $server->description;
|
||||
|
||||
$response = $this->actingAs($user)->postJson("/api/client/servers/$server->uuid/settings/rename", [
|
||||
'name' => '',
|
||||
'description' => '',
|
||||
]);
|
||||
|
||||
$response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
@ -31,15 +33,18 @@ class SettingsControllerTest extends ClientApiIntegrationTestCase
|
||||
|
||||
$server = $server->refresh();
|
||||
$this->assertSame($originalName, $server->name);
|
||||
$this->assertSame($originalDescription, $server->description);
|
||||
|
||||
$this->actingAs($user)
|
||||
->postJson("/api/client/servers/$server->uuid/settings/rename", [
|
||||
'name' => 'Test Server Name',
|
||||
'description' => 'This is a test server.',
|
||||
])
|
||||
->assertStatus(Response::HTTP_NO_CONTENT);
|
||||
|
||||
$server = $server->refresh();
|
||||
$this->assertSame('Test Server Name', $server->name);
|
||||
$this->assertSame('This is a test server.', $server->description);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2,8 +2,10 @@
|
||||
|
||||
namespace App\Tests\Integration\Services\Backups;
|
||||
|
||||
use GuzzleHttp\Psr7\Request;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use App\Models\Backup;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use App\Extensions\Backups\BackupManager;
|
||||
use App\Extensions\Filesystem\S3Filesystem;
|
||||
use App\Services\Backups\DeleteBackupService;
|
||||
@ -52,7 +54,7 @@ class DeleteBackupServiceTest extends IntegrationTestCase
|
||||
$backup = Backup::factory()->create(['server_id' => $server->id]);
|
||||
|
||||
$mock = $this->mock(DaemonBackupRepository::class);
|
||||
$mock->expects('setServer->delete')->with($backup)->andThrow(new ConnectionException(code: 404));
|
||||
$mock->expects('setServer->delete')->with($backup)->andThrow(new ConnectionException(previous: new ClientException('', new Request('DELETE', '/'), new Response(404))));
|
||||
|
||||
$this->app->make(DeleteBackupService::class)->handle($backup);
|
||||
|
||||
@ -67,7 +69,7 @@ class DeleteBackupServiceTest extends IntegrationTestCase
|
||||
$backup = Backup::factory()->create(['server_id' => $server->id]);
|
||||
|
||||
$mock = $this->mock(DaemonBackupRepository::class);
|
||||
$mock->expects('setServer->delete')->with($backup)->andThrow(new ConnectionException(code: 500));
|
||||
$mock->expects('setServer->delete')->with($backup)->andThrow(new ConnectionException(previous: new ClientException('', new Request('DELETE', '/'), new Response(500))));
|
||||
|
||||
$this->expectException(ConnectionException::class);
|
||||
|
||||
|
43
tests/Unit/Helpers/EnvironmentWriterTraitTest.php
Normal file
43
tests/Unit/Helpers/EnvironmentWriterTraitTest.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Unit\Helpers;
|
||||
|
||||
use App\Tests\TestCase;
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
|
||||
class EnvironmentWriterTraitTest extends TestCase
|
||||
{
|
||||
#[DataProvider('variableDataProvider')]
|
||||
public function test_variable_is_escaped_properly($input, $expected): void
|
||||
{
|
||||
$output = (new FooClass())->escapeEnvironmentValue($input);
|
||||
|
||||
$this->assertSame($expected, $output);
|
||||
}
|
||||
|
||||
public static function variableDataProvider(): array
|
||||
{
|
||||
return [
|
||||
['foo', 'foo'],
|
||||
['abc123', 'abc123'],
|
||||
['val"ue', '"val\"ue"'],
|
||||
['val\'ue', '"val\'ue"'],
|
||||
['my test value', '"my test value"'],
|
||||
['mysql_p@assword', '"mysql_p@assword"'],
|
||||
['mysql_p#assword', '"mysql_p#assword"'],
|
||||
['mysql p@$$word', '"mysql p@$$word"'],
|
||||
['mysql p%word', '"mysql p%word"'],
|
||||
['mysql p#word', '"mysql p#word"'],
|
||||
['abc_@#test', '"abc_@#test"'],
|
||||
['test 123 $$$', '"test 123 $$$"'],
|
||||
['#password%', '"#password%"'],
|
||||
['$pass ', '"$pass "'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class FooClass
|
||||
{
|
||||
use EnvironmentWriterTrait;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user