From 7dad2d0e42cc9d89ac4f0f2e6f1c561765d2abc2 Mon Sep 17 00:00:00 2001 From: notCharles Date: Sun, 7 Jul 2024 19:33:25 -0400 Subject: [PATCH 01/43] Fix #464 --- .../components/dashboard/forms/UpdateEmailAddressForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/scripts/components/dashboard/forms/UpdateEmailAddressForm.tsx b/resources/scripts/components/dashboard/forms/UpdateEmailAddressForm.tsx index 2258e67b95..7980f0d7a9 100644 --- a/resources/scripts/components/dashboard/forms/UpdateEmailAddressForm.tsx +++ b/resources/scripts/components/dashboard/forms/UpdateEmailAddressForm.tsx @@ -70,7 +70,7 @@ export default () => { id={'confirm_password'} type={'password'} name={'password'} - label={t('confirm_password', { ns: 'strings' })} + label={t('current_password', { ns: 'strings' })} />
From 1c1c8c0cc617fd65953771624c9eec6adc90e364 Mon Sep 17 00:00:00 2001 From: Exotical Date: Wed, 10 Jul 2024 06:30:12 +0200 Subject: [PATCH 02/43] Fix client Activity tab issues; fixes #465 (#466) * Remove deploy.locations from validator * Change location data to optional for backwards compat * Better styling * Add back comma to follow coding style * Remove EventServiceProvider from providers file Fixes duplicated auth messages in the client Activity tab. * Add null check on $model->actor Prevents the client Activity tab page from breaking when an authentication attempt has failed. * Proper type checking on $model->actor Chose instanceof as it seems to be the best in terms of type safety. Co-authored-by: MartinOscar <40749467+RMartinOscar@users.noreply.github.com> * Revert removal of EventServiceProvider * Remove subscription of AuthenticationListener * Remove subscriptions for auth events * Remove unused import Dispatcher * Remove unused import AuthenticationListener --------- Co-authored-by: MartinOscar <40749467+RMartinOscar@users.noreply.github.com> --- app/Listeners/Auth/AuthenticationListener.php | 7 ------- app/Providers/EventServiceProvider.php | 5 ----- app/Transformers/Api/Client/ActivityLogTransformer.php | 2 +- bootstrap/providers.php | 1 - 4 files changed, 1 insertion(+), 14 deletions(-) diff --git a/app/Listeners/Auth/AuthenticationListener.php b/app/Listeners/Auth/AuthenticationListener.php index 01a8e35c0f..b06428bd2b 100644 --- a/app/Listeners/Auth/AuthenticationListener.php +++ b/app/Listeners/Auth/AuthenticationListener.php @@ -5,7 +5,6 @@ use App\Facades\Activity; use Illuminate\Auth\Events\Failed; use App\Events\Auth\DirectLogin; -use Illuminate\Events\Dispatcher; class AuthenticationListener { @@ -28,10 +27,4 @@ public function handle(Failed|DirectLogin $event): void $activity->event($event instanceof Failed ? 'auth:fail' : 'auth:success')->log(); } - - public function subscribe(Dispatcher $events): void - { - $events->listen(Failed::class, self::class); - $events->listen(DirectLogin::class, self::class); - } } diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 88b8a3d012..e8b9a9a0d9 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -10,7 +10,6 @@ use App\Observers\ServerObserver; use App\Observers\SubuserObserver; use App\Observers\EggVariableObserver; -use App\Listeners\Auth\AuthenticationListener; use App\Events\Server\Installed as ServerInstalledEvent; use App\Notifications\ServerInstalled as ServerInstalledNotification; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; @@ -24,10 +23,6 @@ class EventServiceProvider extends ServiceProvider ServerInstalledEvent::class => [ServerInstalledNotification::class], ]; - protected $subscribe = [ - AuthenticationListener::class, - ]; - /** * Register any events for your application. */ diff --git a/app/Transformers/Api/Client/ActivityLogTransformer.php b/app/Transformers/Api/Client/ActivityLogTransformer.php index af666beb69..488ad1c954 100644 --- a/app/Transformers/Api/Client/ActivityLogTransformer.php +++ b/app/Transformers/Api/Client/ActivityLogTransformer.php @@ -55,7 +55,7 @@ protected function properties(ActivityLog $model): object $properties = $model->properties ->mapWithKeys(function ($value, $key) use ($model) { - if ($key === 'ip' && !$model->actor->is($this->request->user())) { + if ($key === 'ip' && $model->actor instanceof User && !$model->actor->is($this->request->user())) { return [$key => '[hidden]']; } diff --git a/bootstrap/providers.php b/bootstrap/providers.php index 8c37bb7b53..a20d8785c2 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -8,6 +8,5 @@ App\Providers\Filament\AdminPanelProvider::class, App\Providers\RouteServiceProvider::class, App\Providers\ViewComposerServiceProvider::class, - SocialiteProviders\Manager\ServiceProvider::class, ]; From 447e889a4fa9b1665ec0bc32c7cc06e36270ca24 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Wed, 10 Jul 2024 08:36:24 +0200 Subject: [PATCH 03/43] Fix default timestamp for activity logs (#468) * fix default timestamp for activity logs * fix phpstan --- app/Models/ActivityLog.php | 4 +++ ...948_fix-activity-log-timestamp-default.php | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 database/migrations/2024_07_08_112948_fix-activity-log-timestamp-default.php diff --git a/app/Models/ActivityLog.php b/app/Models/ActivityLog.php index 06fd2d108b..bb6149c142 100644 --- a/app/Models/ActivityLog.php +++ b/app/Models/ActivityLog.php @@ -140,6 +140,10 @@ protected static function boot() { parent::boot(); + static::creating(function (self $model) { + $model->timestamp = Carbon::now(); + }); + static::created(function (self $model) { Event::dispatch(new ActivityLogged($model)); }); diff --git a/database/migrations/2024_07_08_112948_fix-activity-log-timestamp-default.php b/database/migrations/2024_07_08_112948_fix-activity-log-timestamp-default.php new file mode 100644 index 0000000000..892ca18268 --- /dev/null +++ b/database/migrations/2024_07_08_112948_fix-activity-log-timestamp-default.php @@ -0,0 +1,28 @@ +timestamp('timestamp')->default(null)->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('activity_logs', function (Blueprint $table) { + $table->timestamp('timestamp')->useCurrent()->change(); + }); + } +}; From bb7c0e0e66845802af577ecc488c3ccbae72eb50 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Wed, 10 Jul 2024 09:25:15 +0200 Subject: [PATCH 04/43] Add "Delete files" task (#470) * started "delete files" task * add logic to DeleteFilesService * add frontend * make nicer * move description to right place --- .../Servers/Schedules/StoreTaskRequest.php | 2 +- app/Jobs/Schedule/RunTaskJob.php | 7 +++- app/Models/Task.php | 1 + app/Services/Files/DeleteFilesService.php | 41 +++++++++++++++++++ .../server/schedules/ScheduleTaskRow.tsx | 6 +++ .../server/schedules/TaskDetailsModal.tsx | 15 ++++++- 6 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 app/Services/Files/DeleteFilesService.php diff --git a/app/Http/Requests/Api/Client/Servers/Schedules/StoreTaskRequest.php b/app/Http/Requests/Api/Client/Servers/Schedules/StoreTaskRequest.php index a510c1b5fc..190d3e54f8 100644 --- a/app/Http/Requests/Api/Client/Servers/Schedules/StoreTaskRequest.php +++ b/app/Http/Requests/Api/Client/Servers/Schedules/StoreTaskRequest.php @@ -19,7 +19,7 @@ public function permission(): string public function rules(): array { return [ - 'action' => 'required|in:command,power,backup', + 'action' => 'required|in:command,power,backup,delete_files', 'payload' => 'required_unless:action,backup|string|nullable', 'time_offset' => 'required|numeric|min:0|max:900', 'sequence_id' => 'sometimes|required|numeric|min:1', diff --git a/app/Jobs/Schedule/RunTaskJob.php b/app/Jobs/Schedule/RunTaskJob.php index 0d2b255a6d..6a660e5c05 100644 --- a/app/Jobs/Schedule/RunTaskJob.php +++ b/app/Jobs/Schedule/RunTaskJob.php @@ -12,6 +12,7 @@ use App\Services\Backups\InitiateBackupService; use App\Repositories\Daemon\DaemonPowerRepository; use App\Exceptions\Http\Connection\DaemonConnectionException; +use App\Services\Files\DeleteFilesService; class RunTaskJob extends Job implements ShouldQueue { @@ -34,7 +35,8 @@ public function __construct(public Task $task, public bool $manualRun = false) */ public function handle( InitiateBackupService $backupService, - DaemonPowerRepository $powerRepository + DaemonPowerRepository $powerRepository, + DeleteFilesService $deleteFilesService ): void { // Do not process a task that is not set to active, unless it's been manually triggered. if (!$this->task->schedule->is_active && !$this->manualRun) { @@ -67,6 +69,9 @@ public function handle( case Task::ACTION_BACKUP: $backupService->setIgnoredFiles(explode(PHP_EOL, $this->task->payload))->handle($server, null, true); break; + case Task::ACTION_DELETE_FILES: + $deleteFilesService->handle($server, explode(PHP_EOL, $this->task->payload)); + break; default: throw new \InvalidArgumentException('Invalid task action provided: ' . $this->task->action); } diff --git a/app/Models/Task.php b/app/Models/Task.php index 6545ab5f5f..254d38ece8 100644 --- a/app/Models/Task.php +++ b/app/Models/Task.php @@ -33,6 +33,7 @@ class Task extends Model public const ACTION_POWER = 'power'; public const ACTION_COMMAND = 'command'; public const ACTION_BACKUP = 'backup'; + public const ACTION_DELETE_FILES = 'delete_files'; /** * The table associated with the model. diff --git a/app/Services/Files/DeleteFilesService.php b/app/Services/Files/DeleteFilesService.php new file mode 100644 index 0000000000..2b1be5fda1 --- /dev/null +++ b/app/Services/Files/DeleteFilesService.php @@ -0,0 +1,41 @@ +daemonFileRepository->setServer($server)->getDirectory($path))->each(function ($item) use ($path, $pattern, $filesToDelete) { + if (Str::is($pattern, $item['name'])) { + $filesToDelete->push($path . '/' . $item['name']); + } + }); + } + + if ($filesToDelete->isNotEmpty()) { + $this->daemonFileRepository->setServer($server)->deleteFiles('/', $filesToDelete->toArray()); + } + } +} diff --git a/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx b/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx index 36db1fff9c..ee638e3f91 100644 --- a/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx +++ b/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx @@ -9,6 +9,7 @@ import { faPencilAlt, faToggleOn, faTrashAlt, + faTrash, } from '@fortawesome/free-solid-svg-icons'; import deleteScheduleTask from '@/api/server/schedules/deleteScheduleTask'; import { httpErrorToHuman } from '@/api/http'; @@ -35,6 +36,8 @@ const getActionDetails = (action: string): [string, any] => { return ['Send Power Action', faToggleOn]; case 'backup': return ['Create Backup', faFileArchive]; + case 'delete_files': + return ['Delete Files', faTrash]; default: return ['Unknown Action', faCode]; } @@ -94,6 +97,9 @@ export default ({ schedule, task }: Props) => { {task.action === 'backup' && (

Ignoring files & folders:

)} + {task.action === 'delete_files' && ( +

Files to delete:

+ )}
diff --git a/resources/scripts/components/server/schedules/TaskDetailsModal.tsx b/resources/scripts/components/server/schedules/TaskDetailsModal.tsx index 0af8fb441e..4903df9319 100644 --- a/resources/scripts/components/server/schedules/TaskDetailsModal.tsx +++ b/resources/scripts/components/server/schedules/TaskDetailsModal.tsx @@ -34,7 +34,7 @@ interface Values { } const schema = object().shape({ - action: string().required().oneOf(['command', 'power', 'backup']), + action: string().required().oneOf(['command', 'power', 'backup', 'delete_files']), payload: string().when('action', { is: (v) => v !== 'backup', then: string().required('A task payload must be provided.'), @@ -131,6 +131,7 @@ const TaskDetailsModal = ({ schedule, task }: Props) => { +
@@ -166,7 +167,7 @@ const TaskDetailsModal = ({ schedule, task }: Props) => {
- ) : ( + ) : values.action === 'backup' ? (
{
+ ) : ( +
+ + + + +
)}
From 1fdff43ae7fbbfdefa46ba48b4700be4f791b6c7 Mon Sep 17 00:00:00 2001 From: Charles Date: Sun, 14 Jul 2024 16:48:14 -0400 Subject: [PATCH 05/43] Add Node CPU/Memory Graphs (#459) * Update Node Stats Soon TM * Update * Make these smaller * Change graphs * Remove this. Didn't work anyways. * Update Graphs * Use User TZ and config var * Fix math * Change to per thread. --- app/Console/Kernel.php | 3 + .../Resources/NodeResource/Pages/EditNode.php | 45 ++++++++-- .../NodeResource/Widgets/NodeCpuChart.php | 81 +++++++++++++++++ .../NodeResource/Widgets/NodeMemoryChart.php | 87 +++++++++++-------- .../NodeResource/Widgets/NodeStorageChart.php | 4 +- app/Jobs/NodeStatistics.php | 46 ++++++++++ .../components/node-cpu-chart.blade.php | 3 + .../components/node-memory-chart.blade.php | 3 + .../components/node-storage-chart.blade.php | 3 + 9 files changed, 228 insertions(+), 47 deletions(-) create mode 100644 app/Filament/Resources/NodeResource/Widgets/NodeCpuChart.php create mode 100644 app/Jobs/NodeStatistics.php create mode 100644 resources/views/filament/components/node-cpu-chart.blade.php create mode 100644 resources/views/filament/components/node-memory-chart.blade.php create mode 100644 resources/views/filament/components/node-storage-chart.blade.php diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 1dd9986467..5b85059702 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -2,6 +2,7 @@ namespace App\Console; +use App\Jobs\NodeStatistics; use App\Models\ActivityLog; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Database\Console\PruneCommand; @@ -32,6 +33,8 @@ protected function schedule(Schedule $schedule): void $schedule->command(ProcessRunnableCommand::class)->everyMinute()->withoutOverlapping(); $schedule->command(CleanServiceBackupFilesCommand::class)->daily(); + $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(); diff --git a/app/Filament/Resources/NodeResource/Pages/EditNode.php b/app/Filament/Resources/NodeResource/Pages/EditNode.php index 94ea3a8a40..c6fd0e451f 100644 --- a/app/Filament/Resources/NodeResource/Pages/EditNode.php +++ b/app/Filament/Resources/NodeResource/Pages/EditNode.php @@ -3,12 +3,11 @@ namespace App\Filament\Resources\NodeResource\Pages; use App\Filament\Resources\NodeResource; -use App\Filament\Resources\NodeResource\Widgets\NodeMemoryChart; -use App\Filament\Resources\NodeResource\Widgets\NodeStorageChart; use App\Models\Node; use App\Services\Nodes\NodeUpdateService; use Filament\Actions; use Filament\Forms; +use Filament\Forms\Components\Fieldset; use Filament\Forms\Components\Grid; use Filament\Forms\Components\Placeholder; use Filament\Forms\Components\Tabs; @@ -17,6 +16,7 @@ use Filament\Forms\Components\Textarea; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\ToggleButtons; +use Filament\Forms\Components\View; use Filament\Forms\Get; use Filament\Forms\Set; use Filament\Notifications\Notification; @@ -41,6 +41,32 @@ public function form(Forms\Form $form): Forms\Form ->persistTabInQueryString() ->columnSpanFull() ->tabs([ + Tab::make('') + ->label('Overview') + ->icon('tabler-chart-area-line-filled') + ->columns(6) + ->schema([ + Fieldset::make() + ->label('Node Information') + ->columns(4) + ->schema([ + Placeholder::make('') + ->label('Wings Version') + ->content(fn (Node $node) => $node->systemInformation()['version']), + Placeholder::make('') + ->label('CPU Threads') + ->content(fn (Node $node) => $node->systemInformation()['cpu_count']), + Placeholder::make('') + ->label('Architecture') + ->content(fn (Node $node) => $node->systemInformation()['architecture']), + Placeholder::make('') + ->label('Kernel') + ->content(fn (Node $node) => $node->systemInformation()['kernel_version']), + ]), + View::make('filament.components.node-cpu-chart')->columnSpan(3), + View::make('filament.components.node-memory-chart')->columnSpan(3), + // TODO: Make purdy View::make('filament.components.node-storage-chart')->columnSpan(3), + ]), Tab::make('Basic Settings') ->icon('tabler-server') ->schema([ @@ -437,16 +463,17 @@ protected function getHeaderActions(): array ]; } - protected function getFooterWidgets(): array + protected function afterSave(): void { - return [ - NodeStorageChart::class, - NodeMemoryChart::class, - ]; + $this->fillForm(); } - protected function afterSave(): void + protected function getColumnSpan() { - $this->fillForm(); + return null; + } + protected function getColumnStart() + { + return null; } } diff --git a/app/Filament/Resources/NodeResource/Widgets/NodeCpuChart.php b/app/Filament/Resources/NodeResource/Widgets/NodeCpuChart.php new file mode 100644 index 0000000000..ecc6bc5926 --- /dev/null +++ b/app/Filament/Resources/NodeResource/Widgets/NodeCpuChart.php @@ -0,0 +1,81 @@ +record; + $threads = $node->systemInformation()['cpu_count']; + + $cpu = collect(cache()->get("nodes.$node->id.cpu_percent")) + ->slice(-10) + ->map(fn ($value, $key) => [ + 'cpu' => number_format($value * $threads, 2), + 'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'), + ]) + ->all(); + + return [ + 'datasets' => [ + [ + 'data' => array_column($cpu, 'cpu'), + 'backgroundColor' => [ + 'rgba(96, 165, 250, 0.3)', + ], + 'tension' => '0.3', + 'fill' => true, + ], + ], + 'labels' => array_column($cpu, 'timestamp'), + ]; + } + + protected function getType(): string + { + return 'line'; + } + + protected function getOptions(): RawJs + { + return RawJs::make(<<<'JS' + { + scales: { + y: { + min: 0, + }, + }, + plugins: { + legend: { + display: false, + } + } + } + JS); + } + + public function getHeading(): string + { + /** @var Node $node */ + $node = $this->record; + $threads = $node->systemInformation()['cpu_count']; + + $cpu = number_format(collect(cache()->get("nodes.$node->id.cpu_percent"))->last() * $threads, 2); + $max = number_format($threads * 100) . '%'; + + return 'CPU - ' . $cpu . '% Of ' . $max; + } +} diff --git a/app/Filament/Resources/NodeResource/Widgets/NodeMemoryChart.php b/app/Filament/Resources/NodeResource/Widgets/NodeMemoryChart.php index 8ed87046a0..3d87624472 100644 --- a/app/Filament/Resources/NodeResource/Widgets/NodeMemoryChart.php +++ b/app/Filament/Resources/NodeResource/Widgets/NodeMemoryChart.php @@ -3,66 +3,83 @@ namespace App\Filament\Resources\NodeResource\Widgets; use App\Models\Node; +use Carbon\Carbon; +use Filament\Support\RawJs; use Filament\Widgets\ChartWidget; use Illuminate\Database\Eloquent\Model; class NodeMemoryChart extends ChartWidget { - protected static ?string $heading = 'Memory'; - - protected static ?string $pollingInterval = '60s'; + protected static ?string $pollingInterval = '5s'; + protected static ?string $maxHeight = '300px'; public ?Model $record = null; - protected static ?array $options = [ - 'scales' => [ - 'x' => [ - 'grid' => [ - 'display' => false, - ], - 'ticks' => [ - 'display' => false, - ], - ], - 'y' => [ - 'grid' => [ - 'display' => false, - ], - 'ticks' => [ - 'display' => false, - ], - ], - ], - ]; - protected function getData(): array { /** @var Node $node */ $node = $this->record; - $total = ($node->statistics()['memory_total'] ?? 0) / 1024 / 1024 / 1024; - $used = ($node->statistics()['memory_used'] ?? 0) / 1024 / 1024 / 1024; - $unused = $total - $used; + $memUsed = collect(cache()->get("nodes.$node->id.memory_used"))->slice(-10) + ->map(fn ($value, $key) => [ + 'memory' => config('panel.use_binary_prefix') ? $value / 1024 / 1024 / 1024 : $value / 1000 / 1000 / 1000, + 'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'), + ]) + ->all(); return [ 'datasets' => [ [ - 'label' => 'Data Cool', - 'data' => [$used, $unused], + 'data' => array_column($memUsed, 'memory'), 'backgroundColor' => [ - 'rgb(255, 99, 132)', - 'rgb(54, 162, 235)', - 'rgb(255, 205, 86)', + 'rgba(96, 165, 250, 0.3)', ], + 'tension' => '0.3', + 'fill' => true, ], - // 'backgroundColor' => [], ], - 'labels' => ['Used', 'Unused'], + 'labels' => array_column($memUsed, 'timestamp'), ]; } protected function getType(): string { - return 'pie'; + return 'line'; + } + + protected function getOptions(): RawJs + { + return RawJs::make(<<<'JS' + { + scales: { + y: { + min: 0, + }, + }, + plugins: { + legend: { + display: false, + } + } + } + JS); + } + + public function getHeading(): string + { + /** @var Node $node */ + $node = $this->record; + $latestMemoryUsed = collect(cache()->get("nodes.$node->id.memory_used"))->last(); + $totalMemory = collect(cache()->get("nodes.$node->id.memory_total"))->last(); + + $used = config('panel.use_binary_prefix') + ? number_format($latestMemoryUsed / 1024 / 1024 / 1024, 2) .' GiB' + : number_format($latestMemoryUsed / 1000 / 1000 / 1000, 2) . ' GB'; + + $total = config('panel.use_binary_prefix') + ? number_format($totalMemory / 1024 / 1024 / 1024, 2) .' GiB' + : number_format($totalMemory / 1000 / 1000 / 1000, 2) . ' GB'; + + return 'Memory - ' . $used . ' Of ' . $total; } } diff --git a/app/Filament/Resources/NodeResource/Widgets/NodeStorageChart.php b/app/Filament/Resources/NodeResource/Widgets/NodeStorageChart.php index bcfbfcf4fd..b841d84ef0 100644 --- a/app/Filament/Resources/NodeResource/Widgets/NodeStorageChart.php +++ b/app/Filament/Resources/NodeResource/Widgets/NodeStorageChart.php @@ -9,8 +9,8 @@ class NodeStorageChart extends ChartWidget { protected static ?string $heading = 'Storage'; - protected static ?string $pollingInterval = '60s'; + protected static ?string $maxHeight = '300px'; public ?Model $record = null; @@ -47,7 +47,6 @@ protected function getData(): array return [ 'datasets' => [ [ - 'label' => 'Data Cool', 'data' => [$used, $unused], 'backgroundColor' => [ 'rgb(255, 99, 132)', @@ -55,7 +54,6 @@ protected function getData(): array 'rgb(255, 205, 86)', ], ], - // 'backgroundColor' => [], ], 'labels' => ['Used', 'Unused'], ]; diff --git a/app/Jobs/NodeStatistics.php b/app/Jobs/NodeStatistics.php new file mode 100644 index 0000000000..19fae9b9db --- /dev/null +++ b/app/Jobs/NodeStatistics.php @@ -0,0 +1,46 @@ +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()); + } + } + } + +} diff --git a/resources/views/filament/components/node-cpu-chart.blade.php b/resources/views/filament/components/node-cpu-chart.blade.php new file mode 100644 index 0000000000..d2627c42d6 --- /dev/null +++ b/resources/views/filament/components/node-cpu-chart.blade.php @@ -0,0 +1,3 @@ + + @livewire(\App\Filament\Resources\NodeResource\Widgets\NodeCpuChart::class, ['record'=> $getRecord()]) + diff --git a/resources/views/filament/components/node-memory-chart.blade.php b/resources/views/filament/components/node-memory-chart.blade.php new file mode 100644 index 0000000000..cb934d0077 --- /dev/null +++ b/resources/views/filament/components/node-memory-chart.blade.php @@ -0,0 +1,3 @@ + + @livewire(\App\Filament\Resources\NodeResource\Widgets\NodeMemoryChart::class, ['record'=> $getRecord()]) + diff --git a/resources/views/filament/components/node-storage-chart.blade.php b/resources/views/filament/components/node-storage-chart.blade.php new file mode 100644 index 0000000000..ea7b5358cc --- /dev/null +++ b/resources/views/filament/components/node-storage-chart.blade.php @@ -0,0 +1,3 @@ + + @livewire(\App\Filament\Resources\NodeResource\Widgets\NodeStorageChart::class, ['record'=> $getRecord()]) + From 833ae30e593ef32032103ac47b0b4998a2339386 Mon Sep 17 00:00:00 2001 From: Charles Date: Mon, 15 Jul 2024 19:09:52 -0400 Subject: [PATCH 06/43] Add timeouts (#483) * Add timeouts Add Timeouts to github call. * use config value --- app/Services/Helpers/SoftwareVersionService.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/Services/Helpers/SoftwareVersionService.php b/app/Services/Helpers/SoftwareVersionService.php index c66d859040..cb7021a74c 100644 --- a/app/Services/Helpers/SoftwareVersionService.php +++ b/app/Services/Helpers/SoftwareVersionService.php @@ -89,13 +89,23 @@ protected function cacheVersionData(): array $versionData = []; try { - $response = $this->client->request('GET', 'https://api.github.com/repos/pelican-dev/panel/releases/latest'); + $response = $this->client->request('GET', 'https://api.github.com/repos/pelican-dev/panel/releases/latest', + [ + 'timeout' => config('panel.guzzle.timeout'), + 'connect_timeout' => config('panel.guzzle.connect_timeout'), + ] + ); if ($response->getStatusCode() === 200) { $panelData = json_decode($response->getBody(), true); $versionData['panel'] = trim($panelData['tag_name'], 'v'); } - $response = $this->client->request('GET', 'https://api.github.com/repos/pelican-dev/wings/releases/latest'); + $response = $this->client->request('GET', 'https://api.github.com/repos/pelican-dev/wings/releases/latest', + [ + 'timeout' => config('panel.guzzle.timeout'), + 'connect_timeout' => config('panel.guzzle.connect_timeout'), + ] + ); if ($response->getStatusCode() === 200) { $wingsData = json_decode($response->getBody(), true); $versionData['daemon'] = trim($wingsData['tag_name'], 'v'); From 8a3d67ada06500025537e142b500b38b64b6ab8c Mon Sep 17 00:00:00 2001 From: Boy132 Date: Wed, 17 Jul 2024 13:00:54 +0200 Subject: [PATCH 07/43] Fix update egg from url (#492) --- app/Filament/Resources/EggResource/Pages/EditEgg.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Filament/Resources/EggResource/Pages/EditEgg.php b/app/Filament/Resources/EggResource/Pages/EditEgg.php index c2282ea802..3fdf0854e7 100644 --- a/app/Filament/Resources/EggResource/Pages/EditEgg.php +++ b/app/Filament/Resources/EggResource/Pages/EditEgg.php @@ -249,9 +249,9 @@ protected function getHeaderActions(): array Tab::make('From URL') ->icon('tabler-world-upload') ->schema([ - TextInput::make('update_url') + TextInput::make('url') ->label('URL') - ->formatStateUsing(fn (Egg $egg): string => $egg->update_url) + ->default(fn (Egg $egg): string => $egg->update_url) ->hint('Link to the egg file (eg. minecraft.json)') ->url(), ]), From a04937d6984bff8a0636c4da70f9ef81f2898d7d Mon Sep 17 00:00:00 2001 From: Boy132 Date: Wed, 17 Jul 2024 13:01:13 +0200 Subject: [PATCH 08/43] Fix `PORT_FLOOR` check and `CIDR_MAX_BITS` in AssignmentService (#491) * fix max cidr * fix port floor --- app/Services/Allocations/AssignmentService.php | 6 +++--- lang/en/exceptions.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Services/Allocations/AssignmentService.php b/app/Services/Allocations/AssignmentService.php index 3788d6bf89..3fd0dc27cb 100644 --- a/app/Services/Allocations/AssignmentService.php +++ b/app/Services/Allocations/AssignmentService.php @@ -14,7 +14,7 @@ class AssignmentService { - public const CIDR_MAX_BITS = 27; + public const CIDR_MAX_BITS = 25; public const CIDR_MIN_BITS = 32; public const PORT_FLOOR = 1024; public const PORT_CEIL = 65535; @@ -74,7 +74,7 @@ public function handle(Node $node, array $data): array throw new TooManyPortsInRangeException(); } - if ((int) $matches[1] <= self::PORT_FLOOR || (int) $matches[2] > self::PORT_CEIL) { + if ((int) $matches[1] < self::PORT_FLOOR || (int) $matches[2] > self::PORT_CEIL) { throw new PortOutOfRangeException(); } @@ -88,7 +88,7 @@ public function handle(Node $node, array $data): array ]; } } else { - if ((int) $port <= self::PORT_FLOOR || (int) $port > self::PORT_CEIL) { + if ((int) $port < self::PORT_FLOOR || (int) $port > self::PORT_CEIL) { throw new PortOutOfRangeException(); } diff --git a/lang/en/exceptions.php b/lang/en/exceptions.php index 3c9adf4c90..9ec18f9b27 100644 --- a/lang/en/exceptions.php +++ b/lang/en/exceptions.php @@ -11,7 +11,7 @@ 'too_many_ports' => 'Adding more than 1000 ports in a single range at once is not supported.', 'invalid_mapping' => 'The mapping provided for :port was invalid and could not be processed.', 'cidr_out_of_range' => 'CIDR notation only allows masks between /25 and /32.', - 'port_out_of_range' => 'Ports in an allocation must be greater than 1024 and less than or equal to 65535.', + 'port_out_of_range' => 'Ports in an allocation must be greater than or equal to 1024 and less than or equal to 65535.', ], 'egg' => [ 'delete_has_servers' => 'An Egg with active servers attached to it cannot be deleted from the Panel.', From 10806d6d6bcad8a875bdfc7fec7ecce2c6bced6c Mon Sep 17 00:00:00 2001 From: Boy132 Date: Wed, 17 Jul 2024 14:43:04 +0200 Subject: [PATCH 09/43] Fix SQLite foreign keys (#478) * start migration to fix sqlite foreign keys * add remaining foreign keys * add ".sqlite.backup" files to gitignore --- database/.gitignore | 1 + ...095213_fix_missing_sqlite_foreign_keys.php | 284 ++++++++++++++++++ 2 files changed, 285 insertions(+) create mode 100644 database/migrations/2024_07_12_095213_fix_missing_sqlite_foreign_keys.php diff --git a/database/.gitignore b/database/.gitignore index 9b1dffd90f..0c40e2f77e 100644 --- a/database/.gitignore +++ b/database/.gitignore @@ -1 +1,2 @@ *.sqlite +*.sqlite.backup diff --git a/database/migrations/2024_07_12_095213_fix_missing_sqlite_foreign_keys.php b/database/migrations/2024_07_12_095213_fix_missing_sqlite_foreign_keys.php new file mode 100644 index 0000000000..47c78a09ae --- /dev/null +++ b/database/migrations/2024_07_12_095213_fix_missing_sqlite_foreign_keys.php @@ -0,0 +1,284 @@ +getDriverName() !== 'sqlite') { + return; + } + + // Disable foreign checks + // legacy_alter_table needs to be 'ON' so existing foreign key table references aren't renamed when renaming the table, see https://www.sqlite.org/lang_altertable.html + DB::statement('PRAGMA foreign_keys = OFF'); + DB::statement('PRAGMA legacy_alter_table = ON'); + + DB::transaction(function () { + // api_keys_user_id_foreign + DB::statement('ALTER TABLE api_keys RENAME TO _api_keys_old'); + DB::statement('CREATE TABLE api_keys + ("id" integer primary key autoincrement not null, + "token" text not null, + "allowed_ips" text not null, + "created_at" datetime, + "updated_at" datetime, + "user_id" integer not null, + "memo" text, + "r_servers" integer not null default \'0\', + "r_nodes" integer not null default \'0\', + "r_allocations" integer not null default \'0\', + "r_users" integer not null default \'0\', + "r_eggs" integer not null default \'0\', + "r_database_hosts" integer not null default \'0\', + "r_server_databases" integer not null default \'0\', + "identifier" varchar, + "key_type" integer not null default \'0\', + "last_used_at" datetime, + "expires_at" datetime, + "r_mounts" integer not null default \'0\', + foreign key("user_id") references "users"("id") on delete cascade)'); + DB::statement('INSERT INTO api_keys SELECT * FROM _api_keys_old'); + DB::statement('DROP TABLE _api_keys_old'); + DB::statement('CREATE UNIQUE INDEX "api_keys_identifier_unique" on "api_keys" ("identifier")'); + + // database_hosts_node_id_foreign + DB::statement('ALTER TABLE database_hosts RENAME TO _database_hosts_old'); + DB::statement('CREATE TABLE database_hosts + ("id" integer primary key autoincrement not null, + "name" varchar not null, + "host" varchar not null, + "port" integer not null, + "username" varchar not null, + "password" text not null, + "max_databases" integer, + "node_id" integer, + "created_at" datetime, + "updated_at" datetime, + foreign key("node_id") references "nodes"("id") on delete set null)'); + DB::statement('INSERT INTO database_hosts SELECT * FROM _database_hosts_old'); + DB::statement('DROP TABLE _database_hosts_old'); + + // mount_node_node_id_foreign + // mount_node_mount_id_foreign + DB::statement('ALTER TABLE mount_node RENAME TO _mount_node_old'); + DB::statement('CREATE TABLE mount_node + ("node_id" integer not null, + "mount_id" integer not null, + foreign key("node_id") references "nodes"("id") on delete cascade on update cascade, + foreign key("mount_id") references "mounts"("id") on delete cascade on update cascade)'); + DB::statement('INSERT INTO mount_node SELECT * FROM _mount_node_old'); + DB::statement('DROP TABLE _mount_node_old'); + DB::statement('CREATE UNIQUE INDEX "mount_node_node_id_mount_id_unique" on "mount_node" ("node_id", "mount_id")'); + + // servers_node_id_foreign + // servers_owner_id_foreign + // servers_egg_id_foreign + // servers_allocation_id_foreign + DB::statement('ALTER TABLE servers RENAME TO _servers_old'); + DB::statement('CREATE TABLE servers + ("id" integer primary key autoincrement not null, + "uuid" varchar not null, + "uuid_short" varchar not null, + "node_id" integer not null, + "name" varchar not null, + "owner_id" integer not null, + "memory" integer not null, + "swap" integer not null, + "disk" integer not null, + "io" integer not null, + "cpu" integer not null, + "egg_id" integer not null, + "startup" text not null, + "created_at" datetime, + "updated_at" datetime, + "allocation_id" integer not null, + "image" varchar not null, + "description" text not null, + "skip_scripts" tinyint(1) not null default \'0\', + "external_id" varchar, + "database_limit" integer default \'0\', + "allocation_limit" integer, + "threads" varchar, + "backup_limit" integer not null default \'0\', + "status" varchar, + "installed_at" datetime, + "oom_killer" integer not null default \'0\', + "docker_labels" text, + foreign key("node_id") references "nodes"("id"), + foreign key("owner_id") references "users"("id"), + foreign key("egg_id") references "eggs"("id"), + foreign key("allocation_id") references "allocations"("id"))'); + DB::statement('INSERT INTO servers SELECT * FROM _servers_old'); + DB::statement('DROP TABLE _servers_old'); + DB::statement('CREATE UNIQUE INDEX "servers_allocation_id_unique" on "servers" ("allocation_id")'); + DB::statement('CREATE UNIQUE INDEX "servers_external_id_unique" on "servers" ("external_id")'); + DB::statement('CREATE UNIQUE INDEX "servers_uuid_unique" on "servers" ("uuid")'); + DB::statement('CREATE UNIQUE INDEX "servers_uuidshort_unique" on "servers" ("uuid_short")'); + + // databases_server_id_foreign + // databases_database_host_id_foreign + DB::statement('ALTER TABLE databases RENAME TO _databases_old'); + DB::statement('CREATE TABLE databases + ("id" integer primary key autoincrement not null, + "server_id" integer not null, + "database_host_id" integer not null, + "database" varchar not null, + "username" varchar not null, + "remote" varchar not null default \'%\', + "password" text not null, + "created_at" datetime, + "updated_at" datetime, + "max_connections" integer default \'0\', + foreign key("server_id") references "servers"("id"), + foreign key("database_host_id") references "database_hosts"("id"))'); + DB::statement('INSERT INTO databases SELECT * FROM _databases_old'); + DB::statement('DROP TABLE _databases_old'); + DB::statement('CREATE UNIQUE INDEX "databases_database_host_id_server_id_database_unique" on "databases" ("database_host_id", "server_id", "database")'); + DB::statement('CREATE UNIQUE INDEX "databases_database_host_id_username_unique" on "databases" ("database_host_id", "username")'); + + // allocations_node_id_foreign + // allocations_server_id_foreign + DB::statement('ALTER TABLE allocations RENAME TO _allocations_old'); + DB::statement('CREATE TABLE allocations + ("id" integer primary key autoincrement not null, + "node_id" integer not null, + "ip" varchar not null, + "port" integer not null, + "server_id" integer, + "created_at" datetime, + "updated_at" datetime, + "ip_alias" text, + "notes" varchar, + foreign key("node_id") references "nodes"("id") on delete cascade, + foreign key("server_id") references "servers"("id") on delete cascade on update set null)'); + DB::statement('INSERT INTO allocations SELECT * FROM _allocations_old'); + DB::statement('DROP TABLE _allocations_old'); + DB::statement('CREATE UNIQUE INDEX "allocations_node_id_ip_port_unique" on "allocations" ("node_id", "ip", "port")'); + + // eggs_config_from_foreign + // eggs_copy_script_from_foreign + DB::statement('ALTER TABLE eggs RENAME TO _eggs_old'); + DB::statement('CREATE TABLE eggs + ("id" integer primary key autoincrement not null, + "name" varchar not null, + "description" text, + "created_at" datetime, + "updated_at" datetime, + "startup" text, + "config_from" integer, + "config_stop" varchar, + "config_logs" text, + "config_startup" text, + "config_files" text, + "script_install" text, + "script_is_privileged" tinyint(1) not null default \'1\', + "script_entry" varchar not null default \'ash\', + "script_container" varchar not null default \'alpine:3.4\', + "copy_script_from" integer, + "uuid" varchar not null, + "author" varchar not null, + "features" text, + "docker_images" text, + "update_url" text, + "file_denylist" text, + "force_outgoing_ip" tinyint(1) not null default \'0\', + "tags" text not null, + foreign key("config_from") references "eggs"("id") on delete set null, + foreign key("copy_script_from") references "eggs"("id") on delete set null)'); + DB::statement('INSERT INTO eggs SELECT * FROM _eggs_old'); + DB::statement('DROP TABLE _eggs_old'); + DB::statement('CREATE UNIQUE INDEX "service_options_uuid_unique" on "eggs" ("uuid")'); + + // egg_mount_mount_id_foreign + // egg_mount_egg_id_foreign + DB::statement('ALTER TABLE egg_mount RENAME TO _egg_mount_old'); + DB::statement('CREATE TABLE egg_mount + ("egg_id" integer not null, + "mount_id" integer not null, + foreign key("egg_id") references "eggs"("id") on delete cascade on update cascade, + foreign key("mount_id") references "mounts"("id") on delete cascade on update cascade)'); + DB::statement('INSERT INTO egg_mount SELECT * FROM _egg_mount_old'); + DB::statement('DROP TABLE _egg_mount_old'); + DB::statement('CREATE UNIQUE INDEX "egg_mount_egg_id_mount_id_unique" on "egg_mount" ("egg_id", "mount_id")'); + + // service_variables_egg_id_foreign + DB::statement('ALTER TABLE egg_variables RENAME TO _egg_variables_old'); + DB::statement('CREATE TABLE egg_variables + ("id" integer primary key autoincrement not null, + "egg_id" integer not null, + "name" varchar not null, + "description" text not null, + "env_variable" varchar not null, + "default_value" text not null, + "user_viewable" integer not null, + "user_editable" integer not null, + "rules" text not null, + "created_at" datetime, + "updated_at" datetime, + "sort" integer, + foreign key("egg_id") references "eggs"("id") on delete cascade)'); + DB::statement('INSERT INTO egg_variables SELECT * FROM _egg_variables_old'); + DB::statement('DROP TABLE _egg_variables_old'); + + // mount_server_server_id_foreign + // mount_server_mount_id_foreign + DB::statement('ALTER TABLE mount_server RENAME TO _mount_server_old'); + DB::statement('CREATE TABLE mount_server + ("server_id" integer not null, + "mount_id" integer not null, + foreign key("server_id") references "servers"("id") on delete cascade on update cascade, + foreign key("mount_id") references "mounts"("id") on delete cascade on update cascade)'); + DB::statement('INSERT INTO mount_server SELECT * FROM _mount_server_old'); + DB::statement('DROP TABLE _mount_server_old'); + DB::statement('CREATE UNIQUE INDEX "mount_server_server_id_mount_id_unique" on "mount_server" ("server_id", "mount_id")'); + + // server_variables_variable_id_foreign + DB::statement('ALTER TABLE server_variables RENAME TO _server_variables_old'); + DB::statement('CREATE TABLE server_variables + ("id" integer primary key autoincrement not null, + "server_id" integer not null, + "variable_id" integer not null, + "variable_value" text not null, + "created_at" datetime, + "updated_at" datetime, + foreign key("server_id") references "servers"("id") on delete cascade, + foreign key("variable_id") references "egg_variables"("id") on delete cascade)'); + DB::statement('INSERT INTO server_variables SELECT * FROM _server_variables_old'); + DB::statement('DROP TABLE _server_variables_old'); + + // subusers_user_id_foreign + // subusers_server_id_foreign + DB::statement('ALTER TABLE subusers RENAME TO _subusers_old'); + DB::statement('CREATE TABLE subusers + ("id" integer primary key autoincrement not null, + "user_id" integer not null, + "server_id" integer not null, + "created_at" datetime, + "updated_at" datetime, + "permissions" text, + foreign key("user_id") references "users"("id") on delete cascade, + foreign key("server_id") references "servers"("id") on delete cascade)'); + DB::statement('INSERT INTO subusers SELECT * FROM _subusers_old'); + DB::statement('DROP TABLE _subusers_old'); + }); + + DB::statement('PRAGMA foreign_keys = ON'); + DB::statement('PRAGMA legacy_alter_table = OFF'); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // Reverse not needed + } +}; From 56b4938dc2b3b38dc731d929e37c869f0c21ab4b Mon Sep 17 00:00:00 2001 From: MartinOscar <40749467+RMartinOscar@users.noreply.github.com> Date: Wed, 17 Jul 2024 22:22:12 +0200 Subject: [PATCH 10/43] Fix #489 (#490) * Fix #489 * Update app/Filament/Resources/NodeResource/Pages/EditNode.php Co-authored-by: Boy132 * Update app/Filament/Resources/NodeResource/Pages/EditNode.php Co-authored-by: Boy132 * Update app/Filament/Resources/NodeResource/Pages/EditNode.php Co-authored-by: Boy132 --------- Co-authored-by: Boy132 --- app/Filament/Resources/NodeResource/Pages/EditNode.php | 8 ++++---- .../Resources/NodeResource/Widgets/NodeCpuChart.php | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/Filament/Resources/NodeResource/Pages/EditNode.php b/app/Filament/Resources/NodeResource/Pages/EditNode.php index c6fd0e451f..d72435fd2a 100644 --- a/app/Filament/Resources/NodeResource/Pages/EditNode.php +++ b/app/Filament/Resources/NodeResource/Pages/EditNode.php @@ -52,16 +52,16 @@ public function form(Forms\Form $form): Forms\Form ->schema([ Placeholder::make('') ->label('Wings Version') - ->content(fn (Node $node) => $node->systemInformation()['version']), + ->content(fn (Node $node) => $node->systemInformation()['version'] ?? 'Unknown'), Placeholder::make('') ->label('CPU Threads') - ->content(fn (Node $node) => $node->systemInformation()['cpu_count']), + ->content(fn (Node $node) => $node->systemInformation()['cpu_count'] ?? 0), Placeholder::make('') ->label('Architecture') - ->content(fn (Node $node) => $node->systemInformation()['architecture']), + ->content(fn (Node $node) => $node->systemInformation()['architecture'] ?? 'Unknown'), Placeholder::make('') ->label('Kernel') - ->content(fn (Node $node) => $node->systemInformation()['kernel_version']), + ->content(fn (Node $node) => $node->systemInformation()['kernel_version'] ?? 'Unknown'), ]), View::make('filament.components.node-cpu-chart')->columnSpan(3), View::make('filament.components.node-memory-chart')->columnSpan(3), diff --git a/app/Filament/Resources/NodeResource/Widgets/NodeCpuChart.php b/app/Filament/Resources/NodeResource/Widgets/NodeCpuChart.php index ecc6bc5926..45d18fc90d 100644 --- a/app/Filament/Resources/NodeResource/Widgets/NodeCpuChart.php +++ b/app/Filament/Resources/NodeResource/Widgets/NodeCpuChart.php @@ -19,7 +19,7 @@ protected function getData(): array { /** @var Node $node */ $node = $this->record; - $threads = $node->systemInformation()['cpu_count']; + $threads = $node->systemInformation()['cpu_count'] ?? 0; $cpu = collect(cache()->get("nodes.$node->id.cpu_percent")) ->slice(-10) @@ -71,7 +71,7 @@ public function getHeading(): string { /** @var Node $node */ $node = $this->record; - $threads = $node->systemInformation()['cpu_count']; + $threads = $node->systemInformation()['cpu_count'] ?? 0; $cpu = number_format(collect(cache()->get("nodes.$node->id.cpu_percent"))->last() * $threads, 2); $max = number_format($threads * 100) . '%'; From 56484a2282b5cf68c441b3154e3e411b20ef334b Mon Sep 17 00:00:00 2001 From: Boy132 Date: Sat, 20 Jul 2024 17:18:45 +0200 Subject: [PATCH 11/43] Increase guzzle timeout when running tests (#485) * increase guzzle timeout when running tests * catch correct exception --- .github/workflows/ci.yaml | 6 ++++++ app/Services/Helpers/SoftwareVersionService.php | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 43a644a996..92fea58bda 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -41,6 +41,8 @@ jobs: DB_HOST: 127.0.0.1 DB_DATABASE: testing DB_USERNAME: root + GUZZLE_TIMEOUT: 60 + GUZZLE_CONNECT_TIMEOUT: 60 steps: - name: Code Checkout uses: actions/checkout@v4 @@ -113,6 +115,8 @@ jobs: DB_HOST: 127.0.0.1 DB_DATABASE: testing DB_USERNAME: root + GUZZLE_TIMEOUT: 60 + GUZZLE_CONNECT_TIMEOUT: 60 steps: - name: Code Checkout uses: actions/checkout@v4 @@ -173,6 +177,8 @@ jobs: QUEUE_CONNECTION: sync DB_CONNECTION: sqlite DB_DATABASE: testing.sqlite + GUZZLE_TIMEOUT: 60 + GUZZLE_CONNECT_TIMEOUT: 60 steps: - name: Code Checkout uses: actions/checkout@v4 diff --git a/app/Services/Helpers/SoftwareVersionService.php b/app/Services/Helpers/SoftwareVersionService.php index cb7021a74c..7d50203bf1 100644 --- a/app/Services/Helpers/SoftwareVersionService.php +++ b/app/Services/Helpers/SoftwareVersionService.php @@ -4,7 +4,7 @@ use GuzzleHttp\Client; use Carbon\CarbonImmutable; -use GuzzleHttp\Exception\ClientException; +use GuzzleHttp\Exception\GuzzleException; use Illuminate\Support\Arr; use Illuminate\Contracts\Cache\Repository as CacheRepository; @@ -110,7 +110,7 @@ protected function cacheVersionData(): array $wingsData = json_decode($response->getBody(), true); $versionData['daemon'] = trim($wingsData['tag_name'], 'v'); } - } catch (ClientException $e) { + } catch (GuzzleException $e) { } $versionData['discord'] = 'https://pelican.dev/discord'; From dfba8e3993aa93bff98c294fd169ddd74296b362 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Sat, 20 Jul 2024 17:23:03 +0200 Subject: [PATCH 12/43] Command to cleanup docker images (#495) * add command to cleanup docker images * automatically cleanup images daily * fix request * fix empty check * run pint --- .../Maintenance/PruneImagesCommand.php | 60 +++++++++++++++++++ app/Console/Kernel.php | 3 + 2 files changed, 63 insertions(+) create mode 100644 app/Console/Commands/Maintenance/PruneImagesCommand.php diff --git a/app/Console/Commands/Maintenance/PruneImagesCommand.php b/app/Console/Commands/Maintenance/PruneImagesCommand.php new file mode 100644 index 0000000000..23f613fa38 --- /dev/null +++ b/app/Console/Commands/Maintenance/PruneImagesCommand.php @@ -0,0 +1,60 @@ +argument('node'); + + if (empty($node)) { + $nodes = Node::all(); + /** @var Node $node */ + foreach ($nodes as $node) { + $this->cleanupImages($node); + } + } else { + $this->cleanupImages((int) $node); + } + } + + private function cleanupImages(int|Node $node): void + { + if (!$node instanceof Node) { + $node = Node::query()->findOrFail($node); + } + + try { + $response = Http::daemon($node) + ->connectTimeout(5) + ->timeout(30) + ->delete('/api/system/docker/image/prune') + ->json() ?? []; + + if (empty($response) || $response['ImagesDeleted'] === null) { + $this->warn("Node {$node->id}: No images to clean up."); + + return; + } + + $count = count($response['ImagesDeleted']); + + $useBinaryPrefix = config('panel.use_binary_prefix'); + $space = round($useBinaryPrefix ? $response['SpaceReclaimed'] / 1024 / 1024 : $response['SpaceReclaimed'] / 1000 / 1000, 2) . ($useBinaryPrefix ? ' MiB' : ' MB'); + + $this->info("Node {$node->id}: Cleaned up {$count} dangling docker images. ({$space})"); + } catch (Exception $exception) { + $this->error($exception->getMessage()); + } + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 5b85059702..4a9bbee17d 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -10,6 +10,7 @@ use App\Console\Commands\Schedule\ProcessRunnableCommand; use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand; use App\Console\Commands\Maintenance\CleanServiceBackupFilesCommand; +use App\Console\Commands\Maintenance\PruneImagesCommand; class Kernel extends ConsoleKernel { @@ -31,7 +32,9 @@ protected function schedule(Schedule $schedule): void // Execute scheduled commands for servers every minute, as if there was a normal cron running. $schedule->command(ProcessRunnableCommand::class)->everyMinute()->withoutOverlapping(); + $schedule->command(CleanServiceBackupFilesCommand::class)->daily(); + $schedule->command(PruneImagesCommand::class)->daily(); $schedule->job(new NodeStatistics())->everyFiveSeconds()->withoutOverlapping(); From acf43f28267cab6a269272a9a938f49840616dbf Mon Sep 17 00:00:00 2001 From: MartinOscar <40749467+RMartinOscar@users.noreply.github.com> Date: Sat, 20 Jul 2024 17:38:34 +0200 Subject: [PATCH 13/43] Ability to create allocations on EditServer page (#494) * Ability to create allocation on edit page + Ability to assign allocation to server on creation * Disable dehydrate for readonly * set these to false --------- Co-authored-by: notCharles --- .../ServerResource/Pages/EditServer.php | 6 +- .../AllocationsRelationManager.php | 88 ++++++++++++++++++- .../UserResource/Pages/EditProfile.php | 2 + .../Allocations/AssignmentService.php | 7 +- 4 files changed, 95 insertions(+), 8 deletions(-) diff --git a/app/Filament/Resources/ServerResource/Pages/EditServer.php b/app/Filament/Resources/ServerResource/Pages/EditServer.php index 7c6ee83a90..c229f658c7 100644 --- a/app/Filament/Resources/ServerResource/Pages/EditServer.php +++ b/app/Filament/Resources/ServerResource/Pages/EditServer.php @@ -124,7 +124,8 @@ public function form(Form $form): Form 'md' => 2, 'lg' => 3, ]) - ->readOnly(), + ->readOnly() + ->dehydrated(false), Forms\Components\TextInput::make('uuid_short') ->label('Short UUID') ->hintAction(CopyAction::make()) @@ -134,7 +135,8 @@ public function form(Form $form): Form 'md' => 2, 'lg' => 3, ]) - ->readOnly(), + ->readOnly() + ->dehydrated(false), Forms\Components\TextInput::make('external_id') ->label('External ID') ->columnSpan([ diff --git a/app/Filament/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php b/app/Filament/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php index d2615a4819..cbac9bf92c 100644 --- a/app/Filament/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php +++ b/app/Filament/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php @@ -4,11 +4,15 @@ use App\Models\Allocation; use App\Models\Server; -use Filament\Forms; +use App\Services\Allocations\AssignmentService; +use Filament\Forms\Components\TagsInput; +use Filament\Forms\Components\TextInput; +use Filament\Forms\Set; use Filament\Forms\Form; use Filament\Resources\RelationManagers\RelationManager; use Filament\Tables; use Filament\Tables\Table; +use Illuminate\Support\HtmlString; /** * @method Server getOwnerRecord() @@ -21,7 +25,7 @@ public function form(Form $form): Form { return $form ->schema([ - Forms\Components\TextInput::make('ip') + TextInput::make('ip') ->required() ->maxLength(255), ]); @@ -62,9 +66,87 @@ public function table(Table $table): Table ->label(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id ? '' : 'Make Primary'), ]) ->headerActions([ - //TODO Tables\Actions\CreateAction::make()->label('Create Allocation'), + Tables\Actions\CreateAction::make()->label('Create Allocation') + ->createAnother(false) + ->form(fn () => [ + TextInput::make('allocation_ip') + ->datalist($this->getOwnerRecord()->node->ipAddresses()) + ->label('IP Address') + ->inlineLabel() + ->ipv4() + ->helperText("Usually your machine's public IP unless you are port forwarding.") + ->required(), + TextInput::make('allocation_alias') + ->label('Alias') + ->inlineLabel() + ->default(null) + ->helperText('Optional display name to help you remember what these are.') + ->required(false), + TagsInput::make('allocation_ports') + ->placeholder('Examples: 27015, 27017-27019') + ->helperText(new HtmlString(' + These are the ports that users can connect to this Server through. +
+ You would have to port forward these on your home network. + ')) + ->label('Ports') + ->inlineLabel() + ->live() + ->afterStateUpdated(function ($state, Set $set) { + $ports = collect(); + $update = false; + foreach ($state as $portEntry) { + if (!str_contains($portEntry, '-')) { + if (is_numeric($portEntry)) { + $ports->push((int) $portEntry); + + continue; + } + + // Do not add non numerical ports + $update = true; + + continue; + } + + $update = true; + [$start, $end] = explode('-', $portEntry); + if (!is_numeric($start) || !is_numeric($end)) { + continue; + } + + $start = max((int) $start, 0); + $end = min((int) $end, 2 ** 16 - 1); + foreach (range($start, $end) as $i) { + $ports->push($i); + } + } + + $uniquePorts = $ports->unique()->values(); + if ($ports->count() > $uniquePorts->count()) { + $update = true; + $ports = $uniquePorts; + } + + $sortedPorts = $ports->sort()->values(); + if ($sortedPorts->all() !== $ports->all()) { + $update = true; + $ports = $sortedPorts; + } + + $ports = $ports->filter(fn ($port) => $port > 1024 && $port < 65535)->values(); + + if ($update) { + $set('allocation_ports', $ports->all()); + } + }) + ->splitKeys(['Tab', ' ', ',']) + ->required(), + ]) + ->action(fn (array $data) => resolve(AssignmentService::class)->handle($this->getOwnerRecord()->node, $data, $this->getOwnerRecord())), Tables\Actions\AssociateAction::make() ->multiple() + ->associateAnother(false) ->preloadRecordSelect() ->recordSelectOptionsQuery(fn ($query) => $query->whereBelongsTo($this->getOwnerRecord()->node)) ->label('Add Allocation'), diff --git a/app/Filament/Resources/UserResource/Pages/EditProfile.php b/app/Filament/Resources/UserResource/Pages/EditProfile.php index 7cc2dfde96..d8eb7274f0 100644 --- a/app/Filament/Resources/UserResource/Pages/EditProfile.php +++ b/app/Filament/Resources/UserResource/Pages/EditProfile.php @@ -53,6 +53,7 @@ protected function getForms(): array ->label(trans('strings.username')) ->disabled() ->readOnly() + ->dehydrated(false) ->maxLength(255) ->unique(ignoreRecord: true) ->autofocus(), @@ -119,6 +120,7 @@ protected function getForms(): array ->hidden(fn () => !cache()->get("users.{$this->getUser()->id}.2fa.tokens")) ->rows(10) ->readOnly() + ->dehydrated(false) ->formatStateUsing(fn () => cache()->get("users.{$this->getUser()->id}.2fa.tokens")) ->helperText('These will not be shown again!') ->label('Backup Tokens:'), diff --git a/app/Services/Allocations/AssignmentService.php b/app/Services/Allocations/AssignmentService.php index 3fd0dc27cb..33ed4aa748 100644 --- a/app/Services/Allocations/AssignmentService.php +++ b/app/Services/Allocations/AssignmentService.php @@ -5,6 +5,7 @@ use App\Models\Allocation; use IPTools\Network; use App\Models\Node; +use App\Models\Server; use Illuminate\Database\ConnectionInterface; use App\Exceptions\DisplayException; use App\Exceptions\Service\Allocation\CidrOutOfRangeException; @@ -37,7 +38,7 @@ public function __construct(protected ConnectionInterface $connection) * @throws \App\Exceptions\Service\Allocation\PortOutOfRangeException * @throws \App\Exceptions\Service\Allocation\TooManyPortsInRangeException */ - public function handle(Node $node, array $data): array + public function handle(Node $node, array $data, Server $server = null): array { $explode = explode('/', $data['allocation_ip']); if (count($explode) !== 1) { @@ -84,7 +85,7 @@ public function handle(Node $node, array $data): array 'ip' => $ip->__toString(), 'port' => (int) $unit, 'ip_alias' => array_get($data, 'allocation_alias'), - 'server_id' => null, + 'server_id' => $server->id ?? null, ]; } } else { @@ -97,7 +98,7 @@ public function handle(Node $node, array $data): array 'ip' => $ip->__toString(), 'port' => (int) $port, 'ip_alias' => array_get($data, 'allocation_alias'), - 'server_id' => null, + 'server_id' => $server->id ?? null, ]; } From 8662806dfd0f4d889ed5d7e5c87fdb8e227610d1 Mon Sep 17 00:00:00 2001 From: notCharles Date: Sat, 20 Jul 2024 18:51:38 -0400 Subject: [PATCH 14/43] Fix 500 if update url is blank --- app/Filament/Resources/EggResource/Pages/EditEgg.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Filament/Resources/EggResource/Pages/EditEgg.php b/app/Filament/Resources/EggResource/Pages/EditEgg.php index 3fdf0854e7..b4af8bc0ae 100644 --- a/app/Filament/Resources/EggResource/Pages/EditEgg.php +++ b/app/Filament/Resources/EggResource/Pages/EditEgg.php @@ -251,7 +251,7 @@ protected function getHeaderActions(): array ->schema([ TextInput::make('url') ->label('URL') - ->default(fn (Egg $egg): string => $egg->update_url) + ->default(fn (Egg $egg): ?string => $egg->update_url) ->hint('Link to the egg file (eg. minecraft.json)') ->url(), ]), From fcef8d69aee83367f4a61c538b437593dadfd012 Mon Sep 17 00:00:00 2001 From: notCharles Date: Sat, 20 Jul 2024 19:15:01 -0400 Subject: [PATCH 15/43] Remove breadcrumbs --- app/Providers/Filament/AdminPanelProvider.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 60339c7440..923a3a77d3 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -37,6 +37,7 @@ public function panel(Panel $panel): Panel ->path('admin') ->topNavigation(config('panel.filament.top-navigation', true)) ->login() + ->breadcrumbs(false) ->homeUrl('/') ->favicon(config('app.favicon', '/pelican.ico')) ->brandName(config('app.name', 'Pelican')) From 2c2e52b18a4244ef571310994bfb45ab42ff966b Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 23 Jul 2024 11:32:32 +0200 Subject: [PATCH 16/43] fix phpstan (#503) --- app/Models/Egg.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Models/Egg.php b/app/Models/Egg.php index 40e20d22a9..e8c672064f 100644 --- a/app/Models/Egg.php +++ b/app/Models/Egg.php @@ -17,7 +17,7 @@ * @property array|null $features * @property string $docker_image -- deprecated, use $docker_images * @property array $docker_images - * @property string $update_url + * @property string|null $update_url * @property bool $force_outgoing_ip * @property array|null $file_denylist * @property string|null $config_files From 465a03bf0e5b0049662e13d5bd24b6ea2745196a Mon Sep 17 00:00:00 2001 From: Lance Pioch Date: Wed, 24 Jul 2024 20:10:45 -0400 Subject: [PATCH 17/43] Update readme.md --- readme.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/readme.md b/readme.md index 74b0c7998b..210802d90f 100644 --- a/readme.md +++ b/readme.md @@ -5,9 +5,6 @@ ![Total Downloads](https://img.shields.io/github/downloads/pelican-dev/panel/total?style=flat&label=Total%20Downloads&labelColor=rgba(0%2C%2070%2C%20114%2C%201)&color=rgba(255%2C%20255%2C%20255%2C%201)) ![Latest Release](https://img.shields.io/github/v/release/pelican-dev/panel?style=flat&label=Latest%20Release&labelColor=rgba(0%2C%2070%2C%20114%2C%201)&color=rgba(255%2C%20255%2C%20255%2C%201)) - -Subscribe on Polar - Pelican Panel is an open-source, web-based application designed for easy management of game servers. It offers a user-friendly interface for deploying, configuring, and managing servers, with features like real-time resource monitoring, Docker container isolation, and extensive customization options. Ideal for both individual gamers and hosting companies, it simplifies server administration without requiring deep technical knowledge. @@ -21,7 +18,7 @@ Fly High, Game On: Pelican's pledge for unrivaled game servers. * [Discord](https://discord.gg/pelican-panel) * [Wings](https://github.com/pelican-dev/wings) -### Supported Games and Servers +## Supported Games and Servers Pelican supports a wide variety of games by utilizing Docker containers to isolate each instance. This gives you the power to run game servers without bloating machines with a host of additional dependencies. @@ -44,4 +41,7 @@ Some of our popular eggs include: | [Storage](https://github.com/pelican-eggs/storage) | S3 | SFTP Share | | | | [Monitoring](https://github.com/pelican-eggs/monitoring) | Prometheus | Loki | | | +## Repository Activity +![Stats](https://repobeats.axiom.co/api/embed/4d8cc7012b325141e6fae9c34a22b3669ad5753b.svg "Repobeats analytics image") + *Copyright Pelican® 2024* From e1bdf95971f8c07109d419f2cc7fda78566f01ae Mon Sep 17 00:00:00 2001 From: MartinOscar <40749467+RMartinOscar@users.noreply.github.com> Date: Mon, 29 Jul 2024 11:58:20 +0200 Subject: [PATCH 18/43] Update SetupTOTPDialog.tsx (#476) --- .../scripts/components/dashboard/forms/SetupTOTPDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/scripts/components/dashboard/forms/SetupTOTPDialog.tsx b/resources/scripts/components/dashboard/forms/SetupTOTPDialog.tsx index b1635a32d7..f68127bd2d 100644 --- a/resources/scripts/components/dashboard/forms/SetupTOTPDialog.tsx +++ b/resources/scripts/components/dashboard/forms/SetupTOTPDialog.tsx @@ -127,7 +127,7 @@ const ConfigureTwoFactorForm = ({ onTokens }: Props) => { }; export default asDialog({ - title: i18n.t('dashboard/account:two_factor.setup.title') ?? 'Enable Two-Step Verification', + title: 'Enable Two-Step Verification', description: "Help protect your account from unauthorized access. You'll be prompted for a verification code each time you sign in.", })(ConfigureTwoFactorForm); From bddd6af8af93c2eb482475dd7ae57e39a0a4ccc4 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 29 Jul 2024 12:13:08 +0200 Subject: [PATCH 19/43] Fix user deletion in no interactive mode (#506) --- app/Console/Commands/User/DeleteUserCommand.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/Console/Commands/User/DeleteUserCommand.php b/app/Console/Commands/User/DeleteUserCommand.php index a6810feee5..8c85510ed8 100644 --- a/app/Console/Commands/User/DeleteUserCommand.php +++ b/app/Console/Commands/User/DeleteUserCommand.php @@ -15,7 +15,7 @@ class DeleteUserCommand extends Command public function handle(): int { $search = $this->option('user') ?? $this->ask(trans('command/messages.user.search_users')); - Assert::notEmpty($search, 'Search term should be an email address, got: %s.'); + Assert::notEmpty($search, 'Search term should not be empty.'); $results = User::query() ->where('id', 'LIKE', "$search%") @@ -42,6 +42,8 @@ public function handle(): int if (!$deleteUser = $this->ask(trans('command/messages.user.select_search_user'))) { return $this->handle(); } + + $deleteUser = User::query()->findOrFail($deleteUser); } else { if (count($results) > 1) { $this->error(trans('command/messages.user.multiple_found')); @@ -53,8 +55,7 @@ public function handle(): int } if ($this->confirm(trans('command/messages.user.confirm_delete')) || !$this->input->isInteractive()) { - $user = User::query()->findOrFail($deleteUser); - $user->delete(); + $deleteUser->delete(); $this->info(trans('command/messages.user.deleted')); } From d89af243a8b288bd0717b72f061a582836f277c4 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 29 Jul 2024 12:13:29 +0200 Subject: [PATCH 20/43] Fix user search on "create server" (#508) --- app/Filament/Resources/ServerResource/Pages/CreateServer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Filament/Resources/ServerResource/Pages/CreateServer.php b/app/Filament/Resources/ServerResource/Pages/CreateServer.php index 572499aa04..bfe5fcbdd1 100644 --- a/app/Filament/Resources/ServerResource/Pages/CreateServer.php +++ b/app/Filament/Resources/ServerResource/Pages/CreateServer.php @@ -80,7 +80,7 @@ public function form(Form $form): Form 'lg' => 3, ]) ->relationship('user', 'username') - ->searchable(['user', 'username', 'email']) + ->searchable(['username', 'email']) ->getOptionLabelFromRecordUsing(fn (User $user) => "$user->email | $user->username " . ($user->root_admin ? '(admin)' : '')) ->createOptionForm([ Forms\Components\TextInput::make('username') From a58e15947815bf2f9bca78296ee5be62c99f83c4 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 29 Jul 2024 12:14:24 +0200 Subject: [PATCH 21/43] Settings page (#486) * remove old settings stuff * add basic settings page * add some settings * add "test mail" button * fix mail fields not updating * fix phpstan * fix default for "top navigation" * force toggle buttons to be bool * force toggle to be bool * add class to view to allow customization * add mailgun settings * add notification settings * add timeout settings * organize tabs into sub-functions * add more settings * add backup settings * add sections to mail settings * add setting for trusted_proxies * fix unsaved data alert not showing * fix clear action * Fix clear action v2 TagsInput expects an array, not a string, fails on saving when using `''` * Add App favicon * Remove defaults, collapse misc sections * Move Save btn, Add API rate limit * small cleanup --------- Co-authored-by: notCharles --- .env.example | 1 - .github/workflows/ci.yaml | 3 - .../Environment/AppSettingsCommand.php | 9 +- app/Filament/Clusters/Settings.php | 10 - app/Filament/Pages/Settings.php | 570 ++++++++++++++++++ .../Admin/Settings/AdvancedController.php | 56 -- .../Admin/Settings/IndexController.php | 56 -- .../Admin/Settings/MailController.php | 82 --- app/Models/Setting.php | 58 -- app/Providers/AppServiceProvider.php | 5 - app/Providers/SettingsServiceProvider.php | 112 ---- config/app.php | 1 + config/panel.php | 12 - lang/en/commands.php | 1 - .../views/admin/settings/advanced.blade.php | 127 ---- .../views/admin/settings/index.blade.php | 75 --- resources/views/admin/settings/mail.blade.php | 202 ------- .../views/filament/pages/settings.blade.php | 15 + resources/views/layouts/admin.blade.php | 5 - .../partials/admin/settings/nav.blade.php | 16 - .../partials/admin/settings/notice.blade.php | 11 - routes/admin.php | 20 - 22 files changed, 587 insertions(+), 860 deletions(-) delete mode 100644 app/Filament/Clusters/Settings.php create mode 100644 app/Filament/Pages/Settings.php delete mode 100644 app/Http/Controllers/Admin/Settings/AdvancedController.php delete mode 100644 app/Http/Controllers/Admin/Settings/IndexController.php delete mode 100644 app/Http/Controllers/Admin/Settings/MailController.php delete mode 100644 app/Providers/SettingsServiceProvider.php delete mode 100644 resources/views/admin/settings/advanced.blade.php delete mode 100644 resources/views/admin/settings/index.blade.php delete mode 100644 resources/views/admin/settings/mail.blade.php create mode 100644 resources/views/filament/pages/settings.blade.php delete mode 100644 resources/views/partials/admin/settings/nav.blade.php delete mode 100644 resources/views/partials/admin/settings/notice.blade.php diff --git a/.env.example b/.env.example index ae71e78e69..95607b2e37 100644 --- a/.env.example +++ b/.env.example @@ -4,7 +4,6 @@ APP_KEY= APP_TIMEZONE=UTC APP_URL=http://panel.test APP_LOCALE=en -APP_ENVIRONMENT_ONLY=true LOG_CHANNEL=daily LOG_STACK=single diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 92fea58bda..5e2ee88471 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -32,7 +32,6 @@ jobs: APP_KEY: ThisIsARandomStringForTests12345 APP_TIMEZONE: UTC APP_URL: http://localhost/ - APP_ENVIRONMENT_ONLY: "true" CACHE_DRIVER: array MAIL_MAILER: array SESSION_DRIVER: array @@ -106,7 +105,6 @@ jobs: APP_KEY: ThisIsARandomStringForTests12345 APP_TIMEZONE: UTC APP_URL: http://localhost/ - APP_ENVIRONMENT_ONLY: "true" CACHE_DRIVER: array MAIL_MAILER: array SESSION_DRIVER: array @@ -170,7 +168,6 @@ jobs: APP_KEY: ThisIsARandomStringForTests12345 APP_TIMEZONE: UTC APP_URL: http://localhost/ - APP_ENVIRONMENT_ONLY: "true" CACHE_DRIVER: array MAIL_MAILER: array SESSION_DRIVER: array diff --git a/app/Console/Commands/Environment/AppSettingsCommand.php b/app/Console/Commands/Environment/AppSettingsCommand.php index 4f2e931196..a480e9b6f5 100644 --- a/app/Console/Commands/Environment/AppSettingsCommand.php +++ b/app/Console/Commands/Environment/AppSettingsCommand.php @@ -38,8 +38,7 @@ class AppSettingsCommand extends Command {--queue= : The queue driver backend to use.} {--redis-host= : Redis host to use for connections.} {--redis-pass= : Password used to connect to redis.} - {--redis-port= : Port to connect to redis over.} - {--settings-ui= : Enable or disable the settings UI.}'; + {--redis-port= : Port to connect to redis over.}'; protected array $variables = []; @@ -87,12 +86,6 @@ public function handle(): int array_key_exists($selected, self::QUEUE_DRIVERS) ? $selected : null ); - if (!is_null($this->option('settings-ui'))) { - $this->variables['APP_ENVIRONMENT_ONLY'] = $this->option('settings-ui') == 'true' ? 'false' : 'true'; - } else { - $this->variables['APP_ENVIRONMENT_ONLY'] = $this->confirm(__('commands.appsettings.comment.settings_ui'), true) ? 'false' : 'true'; - } - // Make sure session cookies are set as "secure" when using HTTPS if (str_starts_with($this->variables['APP_URL'], 'https://')) { $this->variables['SESSION_SECURE_COOKIE'] = 'true'; diff --git a/app/Filament/Clusters/Settings.php b/app/Filament/Clusters/Settings.php deleted file mode 100644 index 0ac8254c82..0000000000 --- a/app/Filament/Clusters/Settings.php +++ /dev/null @@ -1,10 +0,0 @@ -form->fill(); + } + + protected function getFormSchema(): array + { + return [ + Tabs::make('Tabs') + ->columns() + ->persistTabInQueryString() + ->tabs([ + Tab::make('general') + ->label('General') + ->icon('tabler-home') + ->schema($this->generalSettings()), + Tab::make('recaptcha') + ->label('reCAPTCHA') + ->icon('tabler-shield') + ->schema($this->recaptchaSettings()), + Tab::make('mail') + ->label('Mail') + ->icon('tabler-mail') + ->schema($this->mailSettings()), + Tab::make('backup') + ->label('Backup') + ->icon('tabler-box') + ->schema($this->backupSettings()), + Tab::make('misc') + ->label('Misc') + ->icon('tabler-tool') + ->schema($this->miscSettings()), + ]), + ]; + } + + private function generalSettings(): array + { + return [ + TextInput::make('APP_NAME') + ->label('App Name') + ->required() + ->default(env('APP_NAME', 'Pelican')), + TextInput::make('APP_FAVICON') + ->label('App Favicon') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('Favicons should be placed in the public folder, located in the root panel directory.') + ->required() + ->default(env('APP_FAVICON', './pelican.ico')), + Toggle::make('APP_DEBUG') + ->label('Enable Debug Mode?') + ->inline(false) + ->onIcon('tabler-check') + ->offIcon('tabler-x') + ->onColor('success') + ->offColor('danger') + ->formatStateUsing(fn ($state): bool => (bool) $state) + ->afterStateUpdated(fn ($state, Set $set) => $set('APP_DEBUG', (bool) $state)) + ->default(env('RECAPTCHA_ENABLED', config('recaptcha.enabled'))), + ToggleButtons::make('FILAMENT_TOP_NAVIGATION') + ->label('Navigation') + ->grouped() + ->options([ + false => 'Sidebar', + true => 'Topbar', + ]) + ->formatStateUsing(fn ($state): bool => (bool) $state) + ->afterStateUpdated(fn ($state, Set $set) => $set('FILAMENT_TOP_NAVIGATION', (bool) $state)) + ->default(env('FILAMENT_TOP_NAVIGATION', config('panel.filament.top-navigation'))), + ToggleButtons::make('PANEL_USE_BINARY_PREFIX') + ->label('Unit prefix') + ->grouped() + ->options([ + false => 'Decimal Prefix (MB/ GB)', + true => 'Binary Prefix (MiB/ GiB)', + ]) + ->formatStateUsing(fn ($state): bool => (bool) $state) + ->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_USE_BINARY_PREFIX', (bool) $state)) + ->default(env('PANEL_USE_BINARY_PREFIX', config('panel.use_binary_prefix'))), + ToggleButtons::make('APP_2FA_REQUIRED') + ->label('2FA Requirement') + ->grouped() + ->options([ + 0 => 'Not required', + 1 => 'Required for only Admins', + 2 => 'Required for all Users', + ]) + ->formatStateUsing(fn ($state): int => (int) $state) + ->afterStateUpdated(fn ($state, Set $set) => $set('APP_2FA_REQUIRED', (int) $state)) + ->default(env('APP_2FA_REQUIRED', config('panel.auth.2fa_required'))), + TagsInput::make('TRUSTED_PROXIES') + ->label('Trusted Proxies') + ->separator() + ->splitKeys(['Tab', ' ']) + ->placeholder('New IP or IP Range') + ->default(env('TRUSTED_PROXIES', config('trustedproxy.proxies'))) + ->hintActions([ + FormAction::make('clear') + ->label('Clear') + ->color('danger') + ->icon('tabler-trash') + ->requiresConfirmation() + ->action(fn (Set $set) => $set('TRUSTED_PROXIES', [])), + FormAction::make('cloudflare') + ->label('Set to Cloudflare IPs') + ->icon('tabler-brand-cloudflare') + ->action(fn (Set $set) => $set('TRUSTED_PROXIES', [ + '173.245.48.0/20', + '103.21.244.0/22', + '103.22.200.0/22', + '103.31.4.0/22', + '141.101.64.0/18', + '108.162.192.0/18', + '190.93.240.0/20', + '188.114.96.0/20', + '197.234.240.0/22', + '198.41.128.0/17', + '162.158.0.0/15', + '104.16.0.0/13', + '104.24.0.0/14', + '172.64.0.0/13', + '131.0.72.0/22', + ])), + ]), + ]; + } + + private function recaptchaSettings(): array + { + return [ + Toggle::make('RECAPTCHA_ENABLED') + ->label('Enable reCAPTCHA?') + ->inline(false) + ->onIcon('tabler-check') + ->offIcon('tabler-x') + ->onColor('success') + ->offColor('danger') + ->live() + ->formatStateUsing(fn ($state): bool => (bool) $state) + ->afterStateUpdated(fn ($state, Set $set) => $set('RECAPTCHA_ENABLED', (bool) $state)) + ->default(env('RECAPTCHA_ENABLED', config('recaptcha.enabled'))), + TextInput::make('RECAPTCHA_DOMAIN') + ->label('Domain') + ->required() + ->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED')) + ->default(env('RECAPTCHA_DOMAIN', config('recaptcha.domain'))), + TextInput::make('RECAPTCHA_WEBSITE_KEY') + ->label('Website Key') + ->required() + ->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED')) + ->default(env('RECAPTCHA_WEBSITE_KEY', config('recaptcha.website_key'))), + TextInput::make('RECAPTCHA_SECRET_KEY') + ->label('Secret Key') + ->required() + ->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED')) + ->default(env('RECAPTCHA_SECRET_KEY', config('recaptcha.secret_key'))), + ]; + } + + private function mailSettings(): array + { + return [ + ToggleButtons::make('MAIL_MAILER') + ->label('Mail Driver') + ->columnSpanFull() + ->grouped() + ->options([ + 'log' => 'Print mails to Log', + 'smtp' => 'SMTP Server', + 'sendmail' => 'sendmail Binary', + 'mailgun' => 'Mailgun', + 'mandrill' => 'Mandrill', + 'postmark' => 'Postmark', + ]) + ->live() + ->default(env('MAIL_MAILER', config('mail.default'))) + ->hintAction( + FormAction::make('test') + ->label('Send Test Mail') + ->icon('tabler-send') + ->hidden(fn (Get $get) => $get('MAIL_MAILER') === 'log') + ->action(function () { + try { + MailNotification::route('mail', auth()->user()->email) + ->notify(new MailTested(auth()->user())); + + Notification::make() + ->title('Test Mail sent') + ->success() + ->send(); + } catch (Exception $exception) { + Notification::make() + ->title('Test Mail failed') + ->body($exception->getMessage()) + ->danger() + ->send(); + } + }) + ), + Section::make('"From" Settings') + ->description('Set the Address and Name used as "From" in mails.') + ->columns() + ->schema([ + TextInput::make('MAIL_FROM_ADDRESS') + ->label('From Address') + ->required() + ->email() + ->default(env('MAIL_FROM_ADDRESS', config('mail.from.address'))), + TextInput::make('MAIL_FROM_NAME') + ->label('From Name') + ->required() + ->default(env('MAIL_FROM_NAME', config('mail.from.name'))), + ]), + Section::make('SMTP Configuration') + ->columns() + ->visible(fn (Get $get) => $get('MAIL_MAILER') === 'smtp') + ->schema([ + TextInput::make('MAIL_HOST') + ->label('SMTP Host') + ->required() + ->default(env('MAIL_HOST', config('mail.mailers.smtp.host'))), + TextInput::make('MAIL_PORT') + ->label('SMTP Port') + ->required() + ->numeric() + ->minValue(1) + ->maxValue(65535) + ->default(env('MAIL_PORT', config('mail.mailers.smtp.port'))), + TextInput::make('MAIL_USERNAME') + ->label('SMTP Username') + ->required() + ->default(env('MAIL_USERNAME', config('mail.mailers.smtp.username'))), + TextInput::make('MAIL_PASSWORD') + ->label('SMTP Password') + ->password() + ->revealable() + ->default(env('MAIL_PASSWORD')), + ToggleButtons::make('MAIL_ENCRYPTION') + ->label('SMTP encryption') + ->required() + ->grouped() + ->options(['tls' => 'TLS', 'ssl' => 'SSL', '' => 'None']) + ->default(env('MAIL_ENCRYPTION', config('mail.mailers.smtp.encryption', 'tls'))), + ]), + Section::make('Mailgun Configuration') + ->columns() + ->visible(fn (Get $get) => $get('MAIL_MAILER') === 'mailgun') + ->schema([ + TextInput::make('MAILGUN_DOMAIN') + ->label('Mailgun Domain') + ->required() + ->default(env('MAILGUN_DOMAIN', config('services.mailgun.domain'))), + TextInput::make('MAILGUN_SECRET') + ->label('Mailgun Secret') + ->required() + ->default(env('MAIL_USERNAME', config('services.mailgun.secret'))), + TextInput::make('MAILGUN_ENDPOINT') + ->label('Mailgun Endpoint') + ->required() + ->default(env('MAILGUN_ENDPOINT', config('services.mailgun.endpoint'))), + ]), + ]; + } + + private function backupSettings(): array + { + return [ + ToggleButtons::make('APP_BACKUP_DRIVER') + ->label('Backup Driver') + ->columnSpanFull() + ->grouped() + ->options([ + Backup::ADAPTER_DAEMON => 'Wings', + Backup::ADAPTER_AWS_S3 => 'S3', + ]) + ->live() + ->default(env('APP_BACKUP_DRIVER', config('backups.default'))), + Section::make('Throttles') + ->description('Configure how many backups can be created in a period. Set period to 0 to disable this throttle.') + ->columns() + ->schema([ + TextInput::make('BACKUP_THROTTLE_LIMIT') + ->label('Limit') + ->required() + ->numeric() + ->minValue(1) + ->default(config('backups.throttles.limit')), + TextInput::make('BACKUP_THROTTLE_PERIOD') + ->label('Period') + ->required() + ->numeric() + ->minValue(0) + ->suffix('Seconds') + ->default(config('backups.throttles.period')), + ]), + Section::make('S3 Configuration') + ->columns() + ->visible(fn (Get $get) => $get('APP_BACKUP_DRIVER') === Backup::ADAPTER_AWS_S3) + ->schema([ + TextInput::make('AWS_DEFAULT_REGION') + ->label('Default Region') + ->required() + ->default(config('backups.disks.s3.region')), + TextInput::make('AWS_ACCESS_KEY_ID') + ->label('Access Key ID') + ->required() + ->default(config('backups.disks.s3.key')), + TextInput::make('AWS_SECRET_ACCESS_KEY') + ->label('Secret Access Key') + ->required() + ->default(config('backups.disks.s3.secret')), + TextInput::make('AWS_BACKUPS_BUCKET') + ->label('Bucket') + ->required() + ->default(config('backups.disks.s3.bucket')), + TextInput::make('AWS_ENDPOINT') + ->label('Endpoint') + ->required() + ->default(config('backups.disks.s3.endpoint')), + Toggle::make('AWS_USE_PATH_STYLE_ENDPOINT') + ->label('Use path style endpoint?') + ->inline(false) + ->onIcon('tabler-check') + ->offIcon('tabler-x') + ->onColor('success') + ->offColor('danger') + ->live() + ->formatStateUsing(fn ($state): bool => (bool) $state) + ->afterStateUpdated(fn ($state, Set $set) => $set('AWS_USE_PATH_STYLE_ENDPOINT', (bool) $state)) + ->default(env('AWS_USE_PATH_STYLE_ENDPOINT', config('backups.disks.s3.use_path_style_endpoint'))), + ]), + ]; + } + + private function miscSettings(): array + { + return [ + Section::make('Automatic Allocation Creation') + ->description('Toggle if Users can create allocations via the client area.') + ->columns() + ->collapsible() + ->collapsed() + ->schema([ + Toggle::make('PANEL_CLIENT_ALLOCATIONS_ENABLED') + ->label('Allow Users to create allocations?') + ->onIcon('tabler-check') + ->offIcon('tabler-x') + ->onColor('success') + ->offColor('danger') + ->live() + ->columnSpanFull() + ->formatStateUsing(fn ($state): bool => (bool) $state) + ->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_CLIENT_ALLOCATIONS_ENABLED', (bool) $state)) + ->default(env('PANEL_CLIENT_ALLOCATIONS_ENABLED', config('panel.client_features.allocations.enabled'))), + TextInput::make('PANEL_CLIENT_ALLOCATIONS_RANGE_START') + ->label('Starting Port') + ->required() + ->numeric() + ->minValue(1024) + ->maxValue(65535) + ->visible(fn (Get $get) => $get('PANEL_CLIENT_ALLOCATIONS_ENABLED')) + ->default(env('PANEL_CLIENT_ALLOCATIONS_RANGE_START')), + TextInput::make('PANEL_CLIENT_ALLOCATIONS_RANGE_END') + ->label('Ending Port') + ->required() + ->numeric() + ->minValue(1024) + ->maxValue(65535) + ->visible(fn (Get $get) => $get('PANEL_CLIENT_ALLOCATIONS_ENABLED')) + ->default(env('PANEL_CLIENT_ALLOCATIONS_RANGE_END')), + ]), + Section::make('Mail Notifications') + ->description('Toggle which mail notifications should be sent to Users.') + ->columns() + ->collapsible() + ->collapsed() + ->schema([ + Toggle::make('PANEL_SEND_INSTALL_NOTIFICATION') + ->label('Server Installed') + ->onIcon('tabler-check') + ->offIcon('tabler-x') + ->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'))), + Toggle::make('PANEL_SEND_REINSTALL_NOTIFICATION') + ->label('Server Reinstalled') + ->onIcon('tabler-check') + ->offIcon('tabler-x') + ->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'))), + ]), + Section::make('Connections') + ->description('Timeouts used when making requests.') + ->columns() + ->collapsible() + ->collapsed() + ->schema([ + TextInput::make('GUZZLE_TIMEOUT') + ->label('Request Timeout') + ->required() + ->numeric() + ->minValue(15) + ->maxValue(60) + ->suffix('Seconds') + ->default(env('GUZZLE_TIMEOUT', config('panel.guzzle.timeout'))), + TextInput::make('GUZZLE_CONNECT_TIMEOUT') + ->label('Connect Timeout') + ->required() + ->numeric() + ->minValue(5) + ->maxValue(60) + ->suffix('Seconds') + ->default(env('GUZZLE_CONNECT_TIMEOUT', config('panel.guzzle.connect_timeout'))), + ]), + Section::make('Activity Logs') + ->description('Configure how often old activity logs should be pruned and whether admin activities should be logged.') + ->columns() + ->collapsible() + ->collapsed() + ->schema([ + TextInput::make('APP_ACTIVITY_PRUNE_DAYS') + ->label('Prune age') + ->required() + ->numeric() + ->minValue(1) + ->maxValue(365) + ->suffix('Days') + ->default(env('APP_ACTIVITY_PRUNE_DAYS', config('activity.prune_days'))), + Toggle::make('APP_ACTIVITY_HIDE_ADMIN') + ->label('Hide admin activities?') + ->inline(false) + ->onIcon('tabler-check') + ->offIcon('tabler-x') + ->onColor('success') + ->offColor('danger') + ->live() + ->formatStateUsing(fn ($state): bool => (bool) $state) + ->afterStateUpdated(fn ($state, Set $set) => $set('APP_ACTIVITY_HIDE_ADMIN', (bool) $state)) + ->default(env('APP_ACTIVITY_HIDE_ADMIN', config('activity.hide_admin_activity'))), + ]), + Section::make('API') + ->description('Defines the rate limit for the number of requests per minute that can be executed.') + ->columns() + ->collapsible() + ->collapsed() + ->schema([ + TextInput::make('APP_API_CLIENT_RATELIMIT') + ->label('Client API Rate Limit') + ->required() + ->numeric() + ->minValue(1) + ->suffix('Requests Per Minute') + ->default(env('APP_API_CLIENT_RATELIMIT', config('http.rate_limit.client'))), + TextInput::make('APP_API_APPLICATION_RATELIMIT') + ->label('Application API Rate Limit') + ->required() + ->numeric() + ->minValue(1) + ->suffix('Requests Per Minute') + ->default(env('APP_API_APPLICATION_RATELIMIT', config('http.rate_limit.application'))), + ]), + ]; + } + + protected function getFormStatePath(): ?string + { + return 'data'; + } + + protected function hasUnsavedDataChangesAlert(): bool + { + return true; + } + + public function save(): void + { + try { + $data = $this->form->getState(); + + $this->writeToEnvironment($data); + + Artisan::call('config:clear'); + Artisan::call('queue:restart'); + + $this->rememberData(); + + $this->redirect($this->getUrl()); + + Notification::make() + ->title('Settings saved') + ->success() + ->send(); + } catch (Exception $exception) { + Notification::make() + ->title('Save failed') + ->body($exception->getMessage()) + ->danger() + ->send(); + } + } + + protected function getHeaderActions(): array + { + return [ + Action::make('save') + ->action('save') + ->keyBindings(['mod+s']), + ]; + + } + protected function getFormActions(): array + { + return []; + } +} diff --git a/app/Http/Controllers/Admin/Settings/AdvancedController.php b/app/Http/Controllers/Admin/Settings/AdvancedController.php deleted file mode 100644 index def9124a8b..0000000000 --- a/app/Http/Controllers/Admin/Settings/AdvancedController.php +++ /dev/null @@ -1,56 +0,0 @@ - $showRecaptchaWarning, - ]); - } - - /** - * @throws \App\Exceptions\Model\DataValidationException - */ - public function update(AdvancedSettingsFormRequest $request): RedirectResponse - { - foreach ($request->normalize() as $key => $value) { - Setting::set('settings::' . $key, $value); - } - - $this->kernel->call('queue:restart'); - $this->alert->success('Advanced settings have been updated successfully and the queue worker was restarted to apply these changes.')->flash(); - - return redirect()->route('admin.settings.advanced'); - } -} diff --git a/app/Http/Controllers/Admin/Settings/IndexController.php b/app/Http/Controllers/Admin/Settings/IndexController.php deleted file mode 100644 index 47c5674582..0000000000 --- a/app/Http/Controllers/Admin/Settings/IndexController.php +++ /dev/null @@ -1,56 +0,0 @@ - $this->versionService, - 'languages' => $this->getAvailableLanguages(), - ]); - } - - /** - * Handle settings update. - * - * @throws \App\Exceptions\Model\DataValidationException - */ - public function update(BaseSettingsFormRequest $request): RedirectResponse - { - foreach ($request->normalize() as $key => $value) { - Setting::set('settings::' . $key, $value); - } - - $this->kernel->call('queue:restart'); - $this->alert->success('Panel settings have been updated successfully and the queue worker was restarted to apply these changes.')->flash(); - - return redirect()->route('admin.settings'); - } -} diff --git a/app/Http/Controllers/Admin/Settings/MailController.php b/app/Http/Controllers/Admin/Settings/MailController.php deleted file mode 100644 index 33aa5c31f2..0000000000 --- a/app/Http/Controllers/Admin/Settings/MailController.php +++ /dev/null @@ -1,82 +0,0 @@ - config('mail.default') !== 'smtp', - ]); - } - - /** - * Handle request to update SMTP mail settings. - * - * @throws DisplayException - * @throws \App\Exceptions\Model\DataValidationException - */ - public function update(MailSettingsFormRequest $request): Response - { - if (config('mail.default') !== 'smtp') { - throw new DisplayException('This feature is only available if SMTP is the selected email driver for the Panel.'); - } - - $values = $request->normalize(); - if (array_get($values, 'mail:mailers:smtp:password') === '!e') { - $values['mail:mailers:smtp:password'] = ''; - } - - foreach ($values as $key => $value) { - if (in_array($key, SettingsServiceProvider::getEncryptedKeys()) && !empty($value)) { - $value = encrypt($value); - } - - Setting::set('settings::' . $key, $value); - } - - $this->kernel->call('queue:restart'); - - return response('', 204); - } - - /** - * Submit a request to send a test mail message. - */ - public function test(Request $request): Response - { - try { - Notification::route('mail', $request->user()->email) - ->notify(new MailTested($request->user())); - } catch (\Exception $exception) { - return response($exception->getMessage(), 500); - } - - return response('', 204); - } -} diff --git a/app/Models/Setting.php b/app/Models/Setting.php index d25bd1b5d8..9efad2b080 100644 --- a/app/Models/Setting.php +++ b/app/Models/Setting.php @@ -24,62 +24,4 @@ class Setting extends Model 'key' => 'required|string|between:1,255', 'value' => 'string', ]; - - private static array $cache = []; - - private static array $databaseMiss = []; - - /** - * Store a new persistent setting in the database. - */ - public static function set(string $key, string $value = null): void - { - // Clear item from the cache. - self::clearCache($key); - - self::query()->updateOrCreate(['key' => $key], ['value' => $value ?? '']); - - self::$cache[$key] = $value; - } - - /** - * Retrieve a persistent setting from the database. - */ - public static function get(string $key, mixed $default = null): mixed - { - // If item has already been requested return it from the cache. If - // we already know it is missing, immediately return the default value. - if (array_key_exists($key, self::$cache)) { - return self::$cache[$key]; - } elseif (array_key_exists($key, self::$databaseMiss)) { - return value($default); - } - - $instance = self::query()->where('key', $key)->first(); - if (is_null($instance)) { - self::$databaseMiss[$key] = true; - - return value($default); - } - - return self::$cache[$key] = $instance->value; - } - - /** - * Remove a key from the database cache. - */ - public static function forget(string $key) - { - self::clearCache($key); - - return self::query()->where('key', $key)->delete(); - } - - /** - * Remove a key from the cache. - */ - private static function clearCache(string $key): void - { - unset(self::$cache[$key], self::$databaseMiss[$key]); - } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 7d5ba0c44b..384e4501d1 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -87,11 +87,6 @@ public function boot(): void */ public function register(): void { - // Only load the settings service provider if the environment is configured to allow it. - if (!config('panel.load_environment_only', false) && $this->app->environment() !== 'testing') { - $this->app->register(SettingsServiceProvider::class); - } - $this->app->singleton('extensions.themes', function () { return new Theme(); }); diff --git a/app/Providers/SettingsServiceProvider.php b/app/Providers/SettingsServiceProvider.php deleted file mode 100644 index 5c74124559..0000000000 --- a/app/Providers/SettingsServiceProvider.php +++ /dev/null @@ -1,112 +0,0 @@ -keys = array_merge($this->keys, $this->emailKeys); - } - - try { - $values = Setting::all()->mapWithKeys(function ($setting) { - return [$setting->key => $setting->value]; - })->toArray(); - } catch (QueryException $exception) { - $log->notice('A query exception was encountered while trying to load settings from the database: ' . $exception->getMessage()); - - return; - } - - foreach ($this->keys as $key) { - $value = array_get($values, 'settings::' . $key, config(str_replace(':', '.', $key))); - if (in_array($key, self::$encrypted)) { - try { - $value = decrypt($value); - } catch (Exception) { - // ignore - } - } - - switch (strtolower($value)) { - case 'true': - case '(true)': - $value = true; - break; - case 'false': - case '(false)': - $value = false; - break; - case 'empty': - case '(empty)': - $value = ''; - break; - case 'null': - case '(null)': - $value = null; - } - - config()->set(str_replace(':', '.', $key), $value); - } - } - - public static function getEncryptedKeys(): array - { - return self::$encrypted; - } -} diff --git a/config/app.php b/config/app.php index e8d4deb301..f9a4acc850 100644 --- a/config/app.php +++ b/config/app.php @@ -5,6 +5,7 @@ return [ 'name' => env('APP_NAME', 'Pelican'), + 'favicon' => env('APP_FAVICON', './pelican.ico'), 'version' => 'canary', diff --git a/config/panel.php b/config/panel.php index 7b049f1459..32b11bc3a2 100644 --- a/config/panel.php +++ b/config/panel.php @@ -1,18 +1,6 @@ (bool) env('APP_ENVIRONMENT_ONLY', false), - /* |-------------------------------------------------------------------------- | Authentication diff --git a/lang/en/commands.php b/lang/en/commands.php index a42ca228b4..3b9ab9f452 100644 --- a/lang/en/commands.php +++ b/lang/en/commands.php @@ -6,7 +6,6 @@ 'author' => 'Provide the email address that eggs exported by this Panel should be from. This should be a valid email address.', 'url' => 'The application URL MUST begin with https:// or http:// depending on if you are using SSL or not. If you do not include the scheme your emails and other content will link to the wrong location.', 'timezone' => "The timezone should match one of PHP\'s supported timezones. If you are unsure, please reference https://php.net/manual/en/timezones.php.", - 'settings_ui' => 'Enable UI based settings editor?', ], 'redis' => [ 'note' => 'You\'ve selected the Redis driver for one or more options, please provide valid connection information below. In most cases you can use the defaults provided unless you have modified your setup.', diff --git a/resources/views/admin/settings/advanced.blade.php b/resources/views/admin/settings/advanced.blade.php deleted file mode 100644 index dc5543ca9c..0000000000 --- a/resources/views/admin/settings/advanced.blade.php +++ /dev/null @@ -1,127 +0,0 @@ -@extends('layouts.admin') -@include('partials/admin.settings.nav', ['activeTab' => 'advanced']) - -@section('title') - Advanced Settings -@endsection - -@section('content-header') -

Advanced SettingsConfigure advanced settings for Panel.

- -@endsection - -@section('content') - @yield('settings::nav') -
-
-
-
-
-

reCAPTCHA

-
-
-
-
- -
- -

If enabled, login forms and password reset forms will do a silent captcha check and display a visible captcha if needed.

-
-
-
- -
- -
-
-
- -
- -

Used for communication between your site and Google. Be sure to keep it a secret.

-
-
-
- @if($showRecaptchaWarning) -
-
-
- You are currently using reCAPTCHA keys that were shipped with this Panel. For improved security it is recommended to generate new invisible reCAPTCHA keys that tied specifically to your website. -
-
-
- @endif -
-
-
-
-

HTTP Connections

-
-
-
-
- -
- -

The amount of time in seconds to wait for a connection to be opened before throwing an error.

-
-
-
- -
- -

The amount of time in seconds to wait for a request to be completed before throwing an error.

-
-
-
-
-
-
-
-

Automatic Allocation Creation

-
-
-
-
- -
- -

If enabled users will have the option to automatically create new allocations for their server via the frontend.

-
-
-
- -
- -

The starting port in the range that can be automatically allocated.

-
-
-
- -
- -

The ending port in the range that can be automatically allocated.

-
-
-
-
-
-
- -
-
-
-
-@endsection diff --git a/resources/views/admin/settings/index.blade.php b/resources/views/admin/settings/index.blade.php deleted file mode 100644 index 19356e8b8b..0000000000 --- a/resources/views/admin/settings/index.blade.php +++ /dev/null @@ -1,75 +0,0 @@ -@extends('layouts.admin') -@include('partials/admin.settings.nav', ['activeTab' => 'basic']) - -@section('title') - Settings -@endsection - -@section('content-header') -

Panel SettingsConfigure Panel to your liking.

- -@endsection - -@section('content') - @yield('settings::nav') -
-
-
-
-

Panel Settings

-
-
-
-
-
- -
- -

This is the name that is used throughout the panel and in emails sent to clients.

-
-
-
- -
-
- @php - $level = old('panel:auth:2fa_required', config('panel.auth.2fa_required')); - @endphp - - - -
-

If enabled, any account falling into the selected grouping will be required to have 2-Factor authentication enabled to use the Panel.

-
-
-
- -
- -

The default language to use when rendering UI components.

-
-
-
-
- -
-
-
-
-@endsection diff --git a/resources/views/admin/settings/mail.blade.php b/resources/views/admin/settings/mail.blade.php deleted file mode 100644 index 0488d1c7a0..0000000000 --- a/resources/views/admin/settings/mail.blade.php +++ /dev/null @@ -1,202 +0,0 @@ -@extends('layouts.admin') -@include('partials/admin.settings.nav', ['activeTab' => 'mail']) - -@section('title') - Mail Settings -@endsection - -@section('content-header') -

Mail SettingsConfigure how email sending should be handled.

- -@endsection - -@section('content') - @yield('settings::nav') -
-
-
-
-

Email Settings

-
- @if($disabled) -
-
-
-
- This interface is limited to instances using SMTP as the mail driver. Please either use php artisan p:environment:mail command to update your email settings, or set MAIL_DRIVER=smtp in your environment file. -
-
-
-
- @else -
-
-
-
- -
- -

Enter the SMTP server address that mail should be sent through.

-
-
-
- -
- -

Enter the SMTP server port that mail should be sent through.

-
-
-
- -
- @php - $encryption = old('mail:mailers:smtp:encryption', config('mail.mailers.smtp.encryption')); - @endphp - -

Select the type of encryption to use when sending mail.

-
-
-
- -
- -

The username to use when connecting to the SMTP server.

-
-
-
- -
- -

The password to use in conjunction with the SMTP username. Leave blank to continue using the existing password. To set the password to an empty value enter !e into the field.

-
-
-
-
-
-
- -
- -

Enter an email address that all outgoing emails will originate from.

-
-
-
- -
- -

The name that emails should appear to come from.

-
-
-
-
- -
- @endif -
-
-
-@endsection - -@section('footer-scripts') - @parent - - -@endsection diff --git a/resources/views/filament/pages/settings.blade.php b/resources/views/filament/pages/settings.blade.php new file mode 100644 index 0000000000..9f9b3f4391 --- /dev/null +++ b/resources/views/filament/pages/settings.blade.php @@ -0,0 +1,15 @@ + + + {{ $this->form }} + + + + diff --git a/resources/views/layouts/admin.blade.php b/resources/views/layouts/admin.blade.php index d48fad64bf..24b21db199 100644 --- a/resources/views/layouts/admin.blade.php +++ b/resources/views/layouts/admin.blade.php @@ -96,11 +96,6 @@
  • OTHER
  • -
  • - - Settings - -
  • Application API diff --git a/resources/views/partials/admin/settings/nav.blade.php b/resources/views/partials/admin/settings/nav.blade.php deleted file mode 100644 index 9f1ace7f31..0000000000 --- a/resources/views/partials/admin/settings/nav.blade.php +++ /dev/null @@ -1,16 +0,0 @@ -@include('partials/admin.settings.notice') - -@section('settings::nav') - @yield('settings::notice') - -@endsection diff --git a/resources/views/partials/admin/settings/notice.blade.php b/resources/views/partials/admin/settings/notice.blade.php deleted file mode 100644 index 980c5ef60d..0000000000 --- a/resources/views/partials/admin/settings/notice.blade.php +++ /dev/null @@ -1,11 +0,0 @@ -@section('settings::notice') - @if(config('panel.load_environment_only', false)) -
    -
    -
    - Your Panel is currently configured to read settings from the environment only. You will need to set APP_ENVIRONMENT_ONLY=false in your environment file in order to load settings dynamically. -
    -
    -
    - @endif -@endsection diff --git a/routes/admin.php b/routes/admin.php index 3786b876a1..60d047e01f 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -40,26 +40,6 @@ Route::delete('/view/{host:id}', [Admin\DatabaseController::class, 'delete']); }); -/* -|-------------------------------------------------------------------------- -| Settings Controller Routes -|-------------------------------------------------------------------------- -| -| Endpoint: /admin/settings -| -*/ -Route::prefix('settings')->group(function () { - Route::get('/', [Admin\Settings\IndexController::class, 'index'])->name('admin.settings'); - Route::get('/mail', [Admin\Settings\MailController::class, 'index'])->name('admin.settings.mail'); - Route::get('/advanced', [Admin\Settings\AdvancedController::class, 'index'])->name('admin.settings.advanced'); - - Route::post('/mail/test', [Admin\Settings\MailController::class, 'test'])->name('admin.settings.mail.test'); - - Route::patch('/', [Admin\Settings\IndexController::class, 'update']); - Route::patch('/mail', [Admin\Settings\MailController::class, 'update']); - Route::patch('/advanced', [Admin\Settings\AdvancedController::class, 'update']); -}); - /* |-------------------------------------------------------------------------- | User Controller Routes From 3f40256f8bc2e35265624f6eb69dda4805ceb86c Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 30 Jul 2024 16:07:20 +0200 Subject: [PATCH 22/43] Settings page followup (#514) * remove group for toggle buttons * fix default for APP_DEBUG * correctly handle bool values * fix pint * small cleanup for example .env --- .env.example | 6 +----- app/Filament/Pages/Settings.php | 17 ++++++++++------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/.env.example b/.env.example index 95607b2e37..6a128b13df 100644 --- a/.env.example +++ b/.env.example @@ -26,11 +26,7 @@ MAIL_FROM_ADDRESS=no-reply@example.com MAIL_FROM_NAME="Pelican Admin" # Set this to your domain to prevent it defaulting to 'localhost', causing mail servers such as Gmail to reject your mail # MAIL_EHLO_DOMAIN=panel.example.com + SESSION_ENCRYPT=false SESSION_PATH=/ SESSION_DOMAIN=null - -# Set this to true, and set start & end ports to auto create allocations. -PANEL_CLIENT_ALLOCATIONS_ENABLED=false -PANEL_CLIENT_ALLOCATIONS_RANGE_START= -PANEL_CLIENT_ALLOCATIONS_RANGE_END= diff --git a/app/Filament/Pages/Settings.php b/app/Filament/Pages/Settings.php index 06513b47a1..5ec862795e 100644 --- a/app/Filament/Pages/Settings.php +++ b/app/Filament/Pages/Settings.php @@ -102,10 +102,10 @@ private function generalSettings(): array ->offColor('danger') ->formatStateUsing(fn ($state): bool => (bool) $state) ->afterStateUpdated(fn ($state, Set $set) => $set('APP_DEBUG', (bool) $state)) - ->default(env('RECAPTCHA_ENABLED', config('recaptcha.enabled'))), + ->default(env('APP_DEBUG', config('app.debug'))), ToggleButtons::make('FILAMENT_TOP_NAVIGATION') ->label('Navigation') - ->grouped() + ->inline() ->options([ false => 'Sidebar', true => 'Topbar', @@ -115,7 +115,7 @@ private function generalSettings(): array ->default(env('FILAMENT_TOP_NAVIGATION', config('panel.filament.top-navigation'))), ToggleButtons::make('PANEL_USE_BINARY_PREFIX') ->label('Unit prefix') - ->grouped() + ->inline() ->options([ false => 'Decimal Prefix (MB/ GB)', true => 'Binary Prefix (MiB/ GiB)', @@ -125,7 +125,7 @@ private function generalSettings(): array ->default(env('PANEL_USE_BINARY_PREFIX', config('panel.use_binary_prefix'))), ToggleButtons::make('APP_2FA_REQUIRED') ->label('2FA Requirement') - ->grouped() + ->inline() ->options([ 0 => 'Not required', 1 => 'Required for only Admins', @@ -209,7 +209,7 @@ private function mailSettings(): array ToggleButtons::make('MAIL_MAILER') ->label('Mail Driver') ->columnSpanFull() - ->grouped() + ->inline() ->options([ 'log' => 'Print mails to Log', 'smtp' => 'SMTP Server', @@ -284,7 +284,7 @@ private function mailSettings(): array ToggleButtons::make('MAIL_ENCRYPTION') ->label('SMTP encryption') ->required() - ->grouped() + ->inline() ->options(['tls' => 'TLS', 'ssl' => 'SSL', '' => 'None']) ->default(env('MAIL_ENCRYPTION', config('mail.mailers.smtp.encryption', 'tls'))), ]), @@ -314,7 +314,7 @@ private function backupSettings(): array ToggleButtons::make('APP_BACKUP_DRIVER') ->label('Backup Driver') ->columnSpanFull() - ->grouped() + ->inline() ->options([ Backup::ADAPTER_DAEMON => 'Wings', Backup::ADAPTER_AWS_S3 => 'S3', @@ -532,6 +532,9 @@ public function save(): void try { $data = $this->form->getState(); + // Convert bools to a string, so they are correctly written to the .env file + $data = array_map(fn ($value) => is_bool($value) ? ($value ? 'true' : 'false') : $value, $data); + $this->writeToEnvironment($data); Artisan::call('config:clear'); From 686c4375bc823a3b4ec8a85d40d4e4366ae37ce3 Mon Sep 17 00:00:00 2001 From: Charles Date: Tue, 30 Jul 2024 10:43:24 -0400 Subject: [PATCH 23/43] Layout fix for mobile --- .../ServerResource/Pages/EditServer.php | 56 ++++++------------- 1 file changed, 16 insertions(+), 40 deletions(-) diff --git a/app/Filament/Resources/ServerResource/Pages/EditServer.php b/app/Filament/Resources/ServerResource/Pages/EditServer.php index c229f658c7..48665601d0 100644 --- a/app/Filament/Resources/ServerResource/Pages/EditServer.php +++ b/app/Filament/Resources/ServerResource/Pages/EditServer.php @@ -7,6 +7,8 @@ use App\Services\Databases\DatabasePasswordService; use Filament\Forms\Components\Actions\Action; use Filament\Forms\Components\Repeater; +use Filament\Forms\Get; +use Filament\Forms\Set; use LogicException; use App\Filament\Resources\ServerResource; use App\Http\Controllers\Admin\ServersController; @@ -36,22 +38,16 @@ class EditServer extends EditRecord public function form(Form $form): Form { return $form - ->columns([ - 'default' => 1, - 'sm' => 2, - 'md' => 2, - 'lg' => 4, - ]) ->schema([ Tabs::make('Tabs') ->persistTabInQueryString() - ->columnSpan(6) ->columns([ 'default' => 2, 'sm' => 2, 'md' => 4, 'lg' => 6, ]) + ->columnSpanFull() ->tabs([ Tabs\Tab::make('Information') ->icon('tabler-info-circle') @@ -161,12 +157,6 @@ public function form(Form $form): Form ->icon('tabler-brand-docker') ->schema([ Forms\Components\Fieldset::make('Resource Limits') - ->columnSpan([ - 'default' => 2, - 'sm' => 4, - 'md' => 4, - 'lg' => 6, - ]) ->columns([ 'default' => 1, 'sm' => 2, @@ -342,12 +332,6 @@ public function form(Form $form): Form Forms\Components\Fieldset::make('Feature Limits') ->inlineLabel() - ->columnSpan([ - 'default' => 2, - 'sm' => 4, - 'md' => 4, - 'lg' => 6, - ]) ->columns([ 'default' => 1, 'sm' => 2, @@ -372,12 +356,6 @@ public function form(Form $form): Form ->numeric(), ]), Forms\Components\Fieldset::make('Docker Settings') - ->columnSpan([ - 'default' => 2, - 'sm' => 4, - 'md' => 4, - 'lg' => 6, - ]) ->columns([ 'default' => 1, 'sm' => 2, @@ -440,10 +418,10 @@ public function form(Form $form): Form ->disabledOn('edit') ->prefixIcon('tabler-egg') ->columnSpan([ - 'default' => 1, + 'default' => 6, 'sm' => 3, 'md' => 3, - 'lg' => 5, + 'lg' => 4, ]) ->relationship('egg', 'name') ->searchable() @@ -452,6 +430,12 @@ public function form(Form $form): Form Forms\Components\ToggleButtons::make('skip_scripts') ->label('Run Egg Install Script?')->inline() + ->columnSpan([ + 'default' => 6, + 'sm' => 1, + 'md' => 1, + 'lg' => 2, + ]) ->options([ false => 'Yes', true => 'Skip', @@ -469,12 +453,9 @@ public function form(Form $form): Form Forms\Components\Textarea::make('startup') ->label('Startup Command') ->required() - ->columnSpan([ - 'default' => 2, - 'sm' => 4, - 'md' => 4, - 'lg' => 6, - ]) + ->hint('sdfa') + ->hintIcon('tabler-code') + ->columnSpan(6) ->rows(function ($state) { return str($state)->explode("\n")->reduce( fn (int $carry, $line) => $carry + floor(strlen($line) / 125), @@ -486,17 +467,12 @@ public function form(Form $form): Form ->hintAction(CopyAction::make()) ->label('Default Startup Command') ->disabled() - ->formatStateUsing(function ($state, Forms\Get $get, Forms\Set $set) { + ->formatStateUsing(function ($state, Get $get, Set $set) { $egg = Egg::query()->find($get('egg_id')); return $egg->startup; }) - ->columnSpan([ - 'default' => 2, - 'sm' => 4, - 'md' => 4, - 'lg' => 6, - ]), + ->columnSpan(6), Forms\Components\Repeater::make('server_variables') ->relationship('serverVariables') From c4864feaa5ff65515e705b65b07d94a812c2a8bf Mon Sep 17 00:00:00 2001 From: Charles Date: Tue, 30 Jul 2024 10:45:12 -0400 Subject: [PATCH 24/43] Whoops --- app/Filament/Resources/ServerResource/Pages/EditServer.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/Filament/Resources/ServerResource/Pages/EditServer.php b/app/Filament/Resources/ServerResource/Pages/EditServer.php index 48665601d0..c8726fde54 100644 --- a/app/Filament/Resources/ServerResource/Pages/EditServer.php +++ b/app/Filament/Resources/ServerResource/Pages/EditServer.php @@ -453,8 +453,6 @@ public function form(Form $form): Form Forms\Components\Textarea::make('startup') ->label('Startup Command') ->required() - ->hint('sdfa') - ->hintIcon('tabler-code') ->columnSpan(6) ->rows(function ($state) { return str($state)->explode("\n")->reduce( From d22f97568452bdbf2b6f99b3d17575599a642050 Mon Sep 17 00:00:00 2001 From: Charles Date: Tue, 30 Jul 2024 12:58:16 -0400 Subject: [PATCH 25/43] More Mobile UI Closes https://github.com/pelican-dev/panel/issues/512 --- .../ServerResource/Pages/CreateServer.php | 82 +++++++++---------- 1 file changed, 39 insertions(+), 43 deletions(-) diff --git a/app/Filament/Resources/ServerResource/Pages/CreateServer.php b/app/Filament/Resources/ServerResource/Pages/CreateServer.php index bfe5fcbdd1..62e62b192a 100644 --- a/app/Filament/Resources/ServerResource/Pages/CreateServer.php +++ b/app/Filament/Resources/ServerResource/Pages/CreateServer.php @@ -40,8 +40,8 @@ public function form(Form $form): Form ->icon('tabler-info-circle') ->completedIcon('tabler-check') ->columns([ - 'default' => 2, - 'sm' => 2, + 'default' => 1, + 'sm' => 1, 'md' => 4, 'lg' => 6, ]) @@ -61,7 +61,7 @@ public function form(Form $form): Form })) ->columnSpan([ 'default' => 2, - 'sm' => 4, + 'sm' => 3, 'md' => 2, 'lg' => 3, ]) @@ -75,8 +75,8 @@ public function form(Form $form): Form ->label('Owner') ->columnSpan([ 'default' => 2, - 'sm' => 4, - 'md' => 2, + 'sm' => 3, + 'md' => 3, 'lg' => 3, ]) ->relationship('user', 'username') @@ -125,10 +125,10 @@ public function form(Form $form): Form ->prefixIcon('tabler-server-2') ->default(fn () => ($this->node = Node::query()->latest()->first())?->id) ->columnSpan([ - 'default' => 1, - 'sm' => 2, - 'md' => 2, - 'lg' => 2, + 'default' => 2, + 'sm' => 3, + 'md' => 6, + 'lg' => 6, ]) ->live() ->relationship('node', 'name') @@ -146,10 +146,10 @@ public function form(Form $form): Form ->prefixIcon('tabler-network') ->label('Primary Allocation') ->columnSpan([ - 'default' => 1, - 'sm' => 2, - 'md' => 1, - 'lg' => 2, + 'default' => 2, + 'sm' => 3, + 'md' => 2, + 'lg' => 3, ]) ->disabled(fn (Forms\Get $get) => $get('node_id') === null) ->searchable(['ip', 'port', 'ip_alias']) @@ -268,10 +268,10 @@ public function form(Form $form): Form Forms\Components\Repeater::make('allocation_additional') ->label('Additional Allocations') ->columnSpan([ - 'default' => 1, - 'sm' => 2, - 'md' => 1, - 'lg' => 2, + 'default' => 2, + 'sm' => 3, + 'md' => 3, + 'lg' => 3, ]) ->addActionLabel('Add Allocation') ->disabled(fn (Forms\Get $get) => $get('allocation_id') === null) @@ -303,12 +303,13 @@ public function form(Form $form): Form ), ), - Forms\Components\TextInput::make('description') + Forms\Components\TextArea::make('description') ->placeholder('Description') + ->rows(3) ->columnSpan([ - 'default' => 1, - 'sm' => 2, - 'md' => 2, + 'default' => 2, + 'sm' => 6, + 'md' => 6, 'lg' => 6, ]) ->label('Notes'), @@ -491,12 +492,7 @@ public function form(Form $form): Form ->completedIcon('tabler-check') ->schema([ Forms\Components\Fieldset::make('Resource Limits') - ->columnSpan([ - 'default' => 2, - 'sm' => 4, - 'md' => 4, - 'lg' => 6, - ]) + ->columnSpan(6) ->columns([ 'default' => 1, 'sm' => 2, @@ -676,12 +672,7 @@ public function form(Form $form): Form Forms\Components\Fieldset::make('Feature Limits') ->inlineLabel() - ->columnSpan([ - 'default' => 2, - 'sm' => 4, - 'md' => 4, - 'lg' => 6, - ]) + ->columnSpan(6) ->columns([ 'default' => 1, 'sm' => 2, @@ -712,18 +703,13 @@ public function form(Form $form): Form ->default(0), ]), Forms\Components\Fieldset::make('Docker Settings') - ->columnSpan([ - 'default' => 2, - 'sm' => 4, - 'md' => 4, - 'lg' => 6, - ]) ->columns([ 'default' => 1, 'sm' => 2, 'md' => 3, - 'lg' => 3, + 'lg' => 4, ]) + ->columnSpan(6) ->schema([ Forms\Components\Select::make('select_image') ->label('Image Name') @@ -742,7 +728,12 @@ public function form(Form $form): Form return array_flip($images) + ['ghcr.io/custom-image' => 'Custom Image']; }) ->selectablePlaceholder(false) - ->columnSpan(1), + ->columnSpan([ + 'default' => 1, + 'sm' => 2, + 'md' => 3, + 'lg' => 2, + ]), Forms\Components\TextInput::make('image') ->label('Image') @@ -758,13 +749,18 @@ public function form(Form $form): Form } }) ->placeholder('Enter a custom Image') - ->columnSpan(2), + ->columnSpan([ + 'default' => 1, + 'sm' => 2, + 'md' => 3, + 'lg' => 2, + ]), Forms\Components\KeyValue::make('docker_labels') ->label('Container Labels') ->keyLabel('Title') ->valueLabel('Description') - ->columnSpan(3), + ->columnSpanFull(), Forms\Components\CheckboxList::make('mounts') ->live() From 525a106e817cf557384038174b341f5f537b327d Mon Sep 17 00:00:00 2001 From: Charles Date: Tue, 30 Jul 2024 14:12:29 -0400 Subject: [PATCH 26/43] Change TextArea -> Textarea... Makes no sense as we have TextInput, TagsInput and KeyValue... But TextArea is an issue... --- app/Filament/Resources/ServerResource/Pages/CreateServer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Filament/Resources/ServerResource/Pages/CreateServer.php b/app/Filament/Resources/ServerResource/Pages/CreateServer.php index 62e62b192a..343c8fa0c0 100644 --- a/app/Filament/Resources/ServerResource/Pages/CreateServer.php +++ b/app/Filament/Resources/ServerResource/Pages/CreateServer.php @@ -303,7 +303,7 @@ public function form(Form $form): Form ), ), - Forms\Components\TextArea::make('description') + Forms\Components\Textarea::make('description') ->placeholder('Description') ->rows(3) ->columnSpan([ From 18cf6e933849b9e6bf6579ed1c6ec475e86594d0 Mon Sep 17 00:00:00 2001 From: MartinOscar <40749467+RMartinOscar@users.noreply.github.com> Date: Wed, 31 Jul 2024 21:10:58 +0200 Subject: [PATCH 27/43] Update SetupTOTPDialog.tsx (#518) --- resources/scripts/components/dashboard/forms/SetupTOTPDialog.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/scripts/components/dashboard/forms/SetupTOTPDialog.tsx b/resources/scripts/components/dashboard/forms/SetupTOTPDialog.tsx index f68127bd2d..ec8326aa80 100644 --- a/resources/scripts/components/dashboard/forms/SetupTOTPDialog.tsx +++ b/resources/scripts/components/dashboard/forms/SetupTOTPDialog.tsx @@ -4,7 +4,6 @@ import getTwoFactorTokenData, { TwoFactorTokenData } from '@/api/account/getTwoF import { useFlashKey } from '@/plugins/useFlash'; import tw from 'twin.macro'; import { useTranslation } from 'react-i18next'; -import i18n from '@/i18n'; import QRCode from 'qrcode.react'; import { Button } from '@/components/elements/button/index'; import Spinner from '@/components/elements/Spinner'; From 496eaaaf832a78e05166b1b421accadb63af3400 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Sat, 3 Aug 2024 21:13:17 +0200 Subject: [PATCH 28/43] Web Installer (#504) * simplify setup command * add installer page * add route for installer * adjust gitignore * set colors globally * add "unsaved data changes" alert * add helper method to check if panel is installed * make nicer * redis username isn't required * bring back db settings command * store current date in "installed" file * only redirect if install was successfull * remove fpm requirement * change "installed" marker to env variable * improve requirements step * add commands to change cache, queue or session drivers respectively * removed `grouped` for better mobile view --- .env.example | 1 + .../Environment/AppSettingsCommand.php | 135 ++-------------- .../Environment/CacheSettingsCommand.php | 68 +++++++++ .../Environment/QueueSettingsCommand.php | 66 ++++++++ .../Environment/SessionSettingsCommand.php | 69 +++++++++ .../Pages/Installer/PanelInstaller.php | 144 ++++++++++++++++++ .../Pages/Installer/Steps/AdminUserStep.php | 31 ++++ .../Pages/Installer/Steps/DatabaseStep.php | 95 ++++++++++++ .../Pages/Installer/Steps/EnvironmentStep.php | 94 ++++++++++++ .../Pages/Installer/Steps/RedisStep.php | 42 +++++ .../Installer/Steps/RequirementsStep.php | 87 +++++++++++ app/Providers/AppServiceProvider.php | 11 ++ app/Providers/Filament/AdminPanelProvider.php | 10 -- .../Commands/RequestRedisSettingsTrait.php | 37 +++++ app/helpers.php | 8 + .../views/filament/pages/installer.blade.php | 7 + routes/base.php | 4 + 17 files changed, 778 insertions(+), 131 deletions(-) create mode 100644 app/Console/Commands/Environment/CacheSettingsCommand.php create mode 100644 app/Console/Commands/Environment/QueueSettingsCommand.php create mode 100644 app/Console/Commands/Environment/SessionSettingsCommand.php create mode 100644 app/Filament/Pages/Installer/PanelInstaller.php create mode 100644 app/Filament/Pages/Installer/Steps/AdminUserStep.php create mode 100644 app/Filament/Pages/Installer/Steps/DatabaseStep.php create mode 100644 app/Filament/Pages/Installer/Steps/EnvironmentStep.php create mode 100644 app/Filament/Pages/Installer/Steps/RedisStep.php create mode 100644 app/Filament/Pages/Installer/Steps/RequirementsStep.php create mode 100644 app/Traits/Commands/RequestRedisSettingsTrait.php create mode 100644 resources/views/filament/pages/installer.blade.php diff --git a/.env.example b/.env.example index 6a128b13df..84ff1d4324 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,7 @@ APP_KEY= APP_TIMEZONE=UTC APP_URL=http://panel.test APP_LOCALE=en +APP_INSTALLED=false LOG_CHANNEL=daily LOG_STACK=single diff --git a/app/Console/Commands/Environment/AppSettingsCommand.php b/app/Console/Commands/Environment/AppSettingsCommand.php index a480e9b6f5..b3ee96e081 100644 --- a/app/Console/Commands/Environment/AppSettingsCommand.php +++ b/app/Console/Commands/Environment/AppSettingsCommand.php @@ -3,7 +3,6 @@ namespace App\Console\Commands\Environment; use Illuminate\Console\Command; -use Illuminate\Contracts\Console\Kernel; use App\Traits\Commands\EnvironmentWriterTrait; use Illuminate\Support\Facades\Artisan; @@ -11,147 +10,41 @@ class AppSettingsCommand extends Command { use EnvironmentWriterTrait; - public const CACHE_DRIVERS = [ - 'file' => 'Filesystem (recommended)', - 'redis' => 'Redis', - ]; - - public const SESSION_DRIVERS = [ - 'file' => 'Filesystem (recommended)', - 'redis' => 'Redis', - 'database' => 'Database', - 'cookie' => 'Cookie', - ]; - - public const QUEUE_DRIVERS = [ - 'database' => 'Database (recommended)', - 'redis' => 'Redis', - 'sync' => 'Synchronous', - ]; - protected $description = 'Configure basic environment settings for the Panel.'; protected $signature = 'p:environment:setup - {--url= : The URL that this Panel is running on.} - {--cache= : The cache driver backend to use.} - {--session= : The session driver backend to use.} - {--queue= : The queue driver backend to use.} - {--redis-host= : Redis host to use for connections.} - {--redis-pass= : Password used to connect to redis.} - {--redis-port= : Port to connect to redis over.}'; + {--url= : The URL that this Panel is running on.}'; protected array $variables = []; - /** - * AppSettingsCommand constructor. - */ - public function __construct(private Kernel $console) - { - parent::__construct(); - } - - /** - * Handle command execution. - * - * @throws \App\Exceptions\PanelException - */ - public function handle(): int + public function handle(): void { - $this->variables['APP_TIMEZONE'] = 'UTC'; - - $this->output->comment(__('commands.appsettings.comment.url')); - $this->variables['APP_URL'] = $this->option('url') ?? $this->ask( - 'Application URL', - config('app.url', 'https://example.com') - ); - - $selected = config('cache.default', 'file'); - $this->variables['CACHE_STORE'] = $this->option('cache') ?? $this->choice( - 'Cache Driver', - self::CACHE_DRIVERS, - array_key_exists($selected, self::CACHE_DRIVERS) ? $selected : null - ); - - $selected = config('session.driver', 'file'); - $this->variables['SESSION_DRIVER'] = $this->option('session') ?? $this->choice( - 'Session Driver', - self::SESSION_DRIVERS, - array_key_exists($selected, self::SESSION_DRIVERS) ? $selected : null - ); - - $selected = config('queue.default', 'database'); - $this->variables['QUEUE_CONNECTION'] = $this->option('queue') ?? $this->choice( - 'Queue Driver', - self::QUEUE_DRIVERS, - array_key_exists($selected, self::QUEUE_DRIVERS) ? $selected : null - ); - - // Make sure session cookies are set as "secure" when using HTTPS - if (str_starts_with($this->variables['APP_URL'], 'https://')) { - $this->variables['SESSION_SECURE_COOKIE'] = 'true'; - } - - $redisUsed = count(collect($this->variables)->filter(function ($item) { - return $item === 'redis'; - })) !== 0; - - if ($redisUsed) { - $this->requestRedisSettings(); - } - $path = base_path('.env'); if (!file_exists($path)) { + $this->comment('Copying example .env file'); copy($path . '.example', $path); } - $this->writeToEnvironment($this->variables); - if (!config('app.key')) { + $this->comment('Generating app key'); Artisan::call('key:generate'); } - if ($this->variables['QUEUE_CONNECTION'] !== 'sync') { - $this->call('p:environment:queue-service', [ - '--use-redis' => $redisUsed, - ]); - } - - $this->info($this->console->output()); - - return 0; - } + $this->variables['APP_TIMEZONE'] = 'UTC'; - /** - * Request redis connection details and verify them. - */ - private function requestRedisSettings(): void - { - $this->output->note(__('commands.appsettings.redis.note')); - $this->variables['REDIS_HOST'] = $this->option('redis-host') ?? $this->ask( - 'Redis Host', - config('database.redis.default.host') + $this->variables['APP_URL'] = $this->option('url') ?? $this->ask( + 'Application URL', + config('app.url', 'https://example.com') ); - $askForRedisPassword = true; - if (!empty(config('database.redis.default.password'))) { - $this->variables['REDIS_PASSWORD'] = config('database.redis.default.password'); - $askForRedisPassword = $this->confirm('It seems a password is already defined for Redis, would you like to change it?'); - } - - if ($askForRedisPassword) { - $this->output->comment(__('commands.appsettings.redis.comment')); - $this->variables['REDIS_PASSWORD'] = $this->option('redis-pass') ?? $this->output->askHidden( - 'Redis Password' - ); + // Make sure session cookies are set as "secure" when using HTTPS + if (str_starts_with($this->variables['APP_URL'], 'https://')) { + $this->variables['SESSION_SECURE_COOKIE'] = 'true'; } - if (empty($this->variables['REDIS_PASSWORD'])) { - $this->variables['REDIS_PASSWORD'] = 'null'; - } + $this->comment('Writing variables to .env file'); + $this->writeToEnvironment($this->variables); - $this->variables['REDIS_PORT'] = $this->option('redis-port') ?? $this->ask( - 'Redis Port', - config('database.redis.default.port') - ); + $this->info("Setup complete. Vist {$this->variables['APP_URL']}/installer to complete the installation"); } } diff --git a/app/Console/Commands/Environment/CacheSettingsCommand.php b/app/Console/Commands/Environment/CacheSettingsCommand.php new file mode 100644 index 0000000000..4870e1bc97 --- /dev/null +++ b/app/Console/Commands/Environment/CacheSettingsCommand.php @@ -0,0 +1,68 @@ + 'Filesystem (default)', + 'database' => 'Database', + 'redis' => 'Redis', + ]; + + protected $description = 'Configure cache settings for the Panel.'; + + protected $signature = 'p:environment:cache + {--driver= : The cache driver backend to use.} + {--redis-host= : Redis host to use for connections.} + {--redis-pass= : Password used to connect to redis.} + {--redis-port= : Port to connect to redis over.}'; + + protected array $variables = []; + + /** + * CacheSettingsCommand constructor. + */ + public function __construct(private Kernel $console) + { + parent::__construct(); + } + + /** + * Handle command execution. + */ + public function handle(): int + { + $selected = config('cache.default', 'file'); + $this->variables['CACHE_STORE'] = $this->option('driver') ?? $this->choice( + 'Cache Driver', + self::CACHE_DRIVERS, + array_key_exists($selected, self::CACHE_DRIVERS) ? $selected : null + ); + + if ($this->variables['CACHE_STORE'] === 'redis') { + $this->requestRedisSettings(); + + if (config('queue.default') !== 'sync') { + $this->call('p:environment:queue-service', [ + '--use-redis' => true, + '--overwrite' => true, + ]); + } + } + + $this->writeToEnvironment($this->variables); + + $this->info($this->console->output()); + + return 0; + } +} diff --git a/app/Console/Commands/Environment/QueueSettingsCommand.php b/app/Console/Commands/Environment/QueueSettingsCommand.php new file mode 100644 index 0000000000..3d48a31239 --- /dev/null +++ b/app/Console/Commands/Environment/QueueSettingsCommand.php @@ -0,0 +1,66 @@ + 'Database (default)', + 'redis' => 'Redis', + 'sync' => 'Synchronous', + ]; + + protected $description = 'Configure queue settings for the Panel.'; + + protected $signature = 'p:environment:queue + {--driver= : The queue driver backend to use.} + {--redis-host= : Redis host to use for connections.} + {--redis-pass= : Password used to connect to redis.} + {--redis-port= : Port to connect to redis over.}'; + + protected array $variables = []; + + /** + * QueueSettingsCommand constructor. + */ + public function __construct(private Kernel $console) + { + parent::__construct(); + } + + /** + * Handle command execution. + */ + public function handle(): int + { + $selected = config('queue.default', 'database'); + $this->variables['QUEUE_CONNECTION'] = $this->option('driver') ?? $this->choice( + 'Queue Driver', + self::QUEUE_DRIVERS, + array_key_exists($selected, self::QUEUE_DRIVERS) ? $selected : null + ); + + if ($this->variables['QUEUE_CONNECTION'] === 'redis') { + $this->requestRedisSettings(); + + $this->call('p:environment:queue-service', [ + '--use-redis' => true, + '--overwrite' => true, + ]); + } + + $this->writeToEnvironment($this->variables); + + $this->info($this->console->output()); + + return 0; + } +} diff --git a/app/Console/Commands/Environment/SessionSettingsCommand.php b/app/Console/Commands/Environment/SessionSettingsCommand.php new file mode 100644 index 0000000000..9a4081c51a --- /dev/null +++ b/app/Console/Commands/Environment/SessionSettingsCommand.php @@ -0,0 +1,69 @@ + 'Filesystem (default)', + 'redis' => 'Redis', + 'database' => 'Database', + 'cookie' => 'Cookie', + ]; + + protected $description = 'Configure session settings for the Panel.'; + + protected $signature = 'p:environment:session + {--driver= : The session driver backend to use.} + {--redis-host= : Redis host to use for connections.} + {--redis-pass= : Password used to connect to redis.} + {--redis-port= : Port to connect to redis over.}'; + + protected array $variables = []; + + /** + * SessionSettingsCommand constructor. + */ + public function __construct(private Kernel $console) + { + parent::__construct(); + } + + /** + * Handle command execution. + */ + public function handle(): int + { + $selected = config('session.driver', 'file'); + $this->variables['SESSION_DRIVER'] = $this->option('driver') ?? $this->choice( + 'Session Driver', + self::SESSION_DRIVERS, + array_key_exists($selected, self::SESSION_DRIVERS) ? $selected : null + ); + + if ($this->variables['SESSION_DRIVER'] === 'redis') { + $this->requestRedisSettings(); + + if (config('queue.default') !== 'sync') { + $this->call('p:environment:queue-service', [ + '--use-redis' => true, + '--overwrite' => true, + ]); + } + } + + $this->writeToEnvironment($this->variables); + + $this->info($this->console->output()); + + return 0; + } +} diff --git a/app/Filament/Pages/Installer/PanelInstaller.php b/app/Filament/Pages/Installer/PanelInstaller.php new file mode 100644 index 0000000000..222793a646 --- /dev/null +++ b/app/Filament/Pages/Installer/PanelInstaller.php @@ -0,0 +1,144 @@ +form->fill(); + } + + public function dehydrate(): void + { + Artisan::call('config:clear'); + Artisan::call('cache:clear'); + } + + protected function getFormSchema(): array + { + return [ + Wizard::make([ + RequirementsStep::make(), + EnvironmentStep::make(), + DatabaseStep::make(), + RedisStep::make() + ->hidden(fn (Get $get) => $get('env.SESSION_DRIVER') != 'redis' && $get('env.QUEUE_CONNECTION') != 'redis' && $get('env.CACHE_STORE') != 'redis'), + AdminUserStep::make(), + ]) + ->persistStepInQueryString() + ->submitAction(new HtmlString(Blade::render(<<<'BLADE' + + Finish + + BLADE))), + ]; + } + + protected function getFormStatePath(): ?string + { + return 'data'; + } + + protected function hasUnsavedDataChangesAlert(): bool + { + return true; + } + + public function submit() + { + try { + $inputs = $this->form->getState(); + + // Write variables to .env file + $variables = array_get($inputs, 'env'); + $this->writeToEnvironment($variables); + + $redisUsed = count(collect($variables)->filter(function ($item) { + return $item === 'redis'; + })) !== 0; + + // Create queue worker service (if needed) + if ($variables['QUEUE_CONNECTION'] !== 'sync') { + Artisan::call('p:environment:queue-service', [ + '--use-redis' => $redisUsed, + '--overwrite' => true, + ]); + } + + // Run migrations + Artisan::call('migrate', [ + '--force' => true, + '--seed' => true, + ]); + + // Create first admin user + $userData = array_get($inputs, 'user'); + $userData['root_admin'] = true; + app(UserCreationService::class)->handle($userData); + + // Install setup complete + $this->writeToEnvironment(['APP_INSTALLED' => true]); + + $this->rememberData(); + + Notification::make() + ->title('Successfully Installed') + ->success() + ->send(); + + redirect()->intended(Filament::getUrl()); + } catch (Exception $exception) { + Notification::make() + ->title('Installation Failed') + ->body($exception->getMessage()) + ->danger() + ->send(); + } + } +} diff --git a/app/Filament/Pages/Installer/Steps/AdminUserStep.php b/app/Filament/Pages/Installer/Steps/AdminUserStep.php new file mode 100644 index 0000000000..68ebb6510e --- /dev/null +++ b/app/Filament/Pages/Installer/Steps/AdminUserStep.php @@ -0,0 +1,31 @@ +label('Admin User') + ->schema([ + TextInput::make('user.email') + ->label('Admin E-Mail') + ->required() + ->email() + ->default('admin@example.com'), + TextInput::make('user.username') + ->label('Admin Username') + ->required() + ->default('admin'), + TextInput::make('user.password') + ->label('Admin Password') + ->required() + ->password() + ->revealable(), + ]); + } +} diff --git a/app/Filament/Pages/Installer/Steps/DatabaseStep.php b/app/Filament/Pages/Installer/Steps/DatabaseStep.php new file mode 100644 index 0000000000..94a7952e2f --- /dev/null +++ b/app/Filament/Pages/Installer/Steps/DatabaseStep.php @@ -0,0 +1,95 @@ +label('Database') + ->columns() + ->schema([ + TextInput::make('env.DB_DATABASE') + ->label(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite' ? 'Database Path' : 'Database Name') + ->columnSpanFull() + ->hintIcon('tabler-question-mark') + ->hintIconTooltip(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite' ? 'The path of your .sqlite file relative to the database folder.' : 'The name of the panel database.') + ->required() + ->default(fn (Get $get) => env('DB_DATABASE', $get('env.DB_CONNECTION') === 'sqlite' ? 'database.sqlite' : 'panel')), + TextInput::make('env.DB_HOST') + ->label('Database Host') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('The host of your database. Make sure it is reachable.') + ->required() + ->default(env('DB_HOST', '127.0.0.1')) + ->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'), + TextInput::make('env.DB_PORT') + ->label('Database Port') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('The port of your database.') + ->required() + ->numeric() + ->minValue(1) + ->maxValue(65535) + ->default(env('DB_PORT', 3306)) + ->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'), + TextInput::make('env.DB_USERNAME') + ->label('Database Username') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('The name of your database user.') + ->required() + ->default(env('DB_USERNAME', 'pelican')) + ->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'), + TextInput::make('env.DB_PASSWORD') + ->label('Database Password') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('The password of your database user. Can be empty.') + ->password() + ->revealable() + ->default(env('DB_PASSWORD')) + ->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'), + ]) + ->afterValidation(function (Get $get) { + $driver = $get('env.DB_CONNECTION'); + if ($driver !== 'sqlite') { + /** @var DatabaseManager $database */ + $database = app(DatabaseManager::class); + + try { + config()->set('database.connections._panel_install_test', [ + 'driver' => $driver, + 'host' => $get('env.DB_HOST'), + 'port' => $get('env.DB_PORT'), + 'database' => $get('env.DB_DATABASE'), + 'username' => $get('env.DB_USERNAME'), + 'password' => $get('env.DB_PASSWORD'), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'strict' => true, + ]); + + $database->connection('_panel_install_test')->getPdo(); + } catch (PDOException $exception) { + Notification::make() + ->title('Database connection failed') + ->body($exception->getMessage()) + ->danger() + ->send(); + + $database->disconnect('_panel_install_test'); + + throw new Halt('Database connection failed'); + } + } + }); + } +} diff --git a/app/Filament/Pages/Installer/Steps/EnvironmentStep.php b/app/Filament/Pages/Installer/Steps/EnvironmentStep.php new file mode 100644 index 0000000000..d9cc5eafaa --- /dev/null +++ b/app/Filament/Pages/Installer/Steps/EnvironmentStep.php @@ -0,0 +1,94 @@ + 'Filesystem', + 'redis' => 'Redis', + ]; + + public const SESSION_DRIVERS = [ + 'file' => 'Filesystem', + 'redis' => 'Redis', + 'database' => 'Database', + 'cookie' => 'Cookie', + ]; + + public const QUEUE_DRIVERS = [ + 'database' => 'Database', + 'redis' => 'Redis', + 'sync' => 'Synchronous', + ]; + + public const DATABASE_DRIVERS = [ + 'sqlite' => 'SQLite', + 'mariadb' => 'MariaDB', + 'mysql' => 'MySQL', + ]; + + public static function make(): Step + { + return Step::make('environment') + ->label('Environment') + ->columns() + ->schema([ + TextInput::make('env.APP_NAME') + ->label('App Name') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('This will be the Name of your Panel.') + ->required() + ->default(config('app.name')), + TextInput::make('env.APP_URL') + ->label('App URL') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('This will be the URL you access your Panel from.') + ->required() + ->default(config('app.url')) + ->live() + ->afterStateUpdated(fn ($state, Set $set) => $set('env.SESSION_SECURE_COOKIE', str_starts_with($state, 'https://'))), + Toggle::make('env.SESSION_SECURE_COOKIE') + ->hidden() + ->default(env('SESSION_SECURE_COOKIE')), + ToggleButtons::make('env.CACHE_STORE') + ->label('Cache Driver') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('The driver used for caching. We recommend "Filesystem".') + ->required() + ->inline() + ->options(self::CACHE_DRIVERS) + ->default(config('cache.default', 'file')), + ToggleButtons::make('env.SESSION_DRIVER') + ->label('Session Driver') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('The driver used for storing sessions. We recommend "Filesystem" or "Database".') + ->required() + ->inline() + ->options(self::SESSION_DRIVERS) + ->default(config('session.driver', 'file')), + ToggleButtons::make('env.QUEUE_CONNECTION') + ->label('Queue Driver') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('The driver used for handling queues. We recommend "Database".') + ->required() + ->inline() + ->options(self::QUEUE_DRIVERS) + ->default(config('queue.default', 'database')), + ToggleButtons::make('env.DB_CONNECTION') + ->label('Database Driver') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('The driver used for the panel database. We recommend "SQLite".') + ->required() + ->inline() + ->options(self::DATABASE_DRIVERS) + ->default(config('database.default', 'sqlite')), + ]); + } +} diff --git a/app/Filament/Pages/Installer/Steps/RedisStep.php b/app/Filament/Pages/Installer/Steps/RedisStep.php new file mode 100644 index 0000000000..04a4b1b72f --- /dev/null +++ b/app/Filament/Pages/Installer/Steps/RedisStep.php @@ -0,0 +1,42 @@ +label('Redis') + ->columns() + ->schema([ + TextInput::make('env.REDIS_HOST') + ->label('Redis Host') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('The host of your redis server. Make sure it is reachable.') + ->required() + ->default(config('database.redis.default.host')), + TextInput::make('env.REDIS_PORT') + ->label('Redis Port') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('The port of your redis server.') + ->required() + ->default(config('database.redis.default.port')), + TextInput::make('env.REDIS_USERNAME') + ->label('Redis Username') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('The name of your redis user. Can be empty') + ->default(config('database.redis.default.username')), + TextInput::make('env.REDIS_PASSWORD') + ->label('Redis Password') + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('The password for your redis user. Can be empty.') + ->password() + ->revealable() + ->default(config('database.redis.default.password')), + ]); + } +} diff --git a/app/Filament/Pages/Installer/Steps/RequirementsStep.php b/app/Filament/Pages/Installer/Steps/RequirementsStep.php new file mode 100644 index 0000000000..483e4ca1bc --- /dev/null +++ b/app/Filament/Pages/Installer/Steps/RequirementsStep.php @@ -0,0 +1,87 @@ += 0; + + $fields = [ + Section::make('PHP Version') + ->description('8.2 or newer') + ->icon($correctPhpVersion ? 'tabler-check' : 'tabler-x') + ->iconColor($correctPhpVersion ? 'success' : 'danger') + ->schema([ + Placeholder::make('') + ->content('Your PHP Version ' . ($correctPhpVersion ? 'is' : 'needs to be') .' 8.2 or newer.'), + ]), + ]; + + $phpExtensions = [ + 'BCMath' => extension_loaded('bcmath'), + 'cURL' => extension_loaded('curl'), + 'GD' => extension_loaded('gd'), + 'intl' => extension_loaded('intl'), + 'mbstring' => extension_loaded('mbstring'), + 'MySQL' => extension_loaded('pdo_mysql'), + 'SQLite3' => extension_loaded('pdo_sqlite'), + 'XML' => extension_loaded('xml'), + 'Zip' => extension_loaded('zip'), + ]; + $allExtensionsInstalled = !in_array(false, $phpExtensions); + + $fields[] = Section::make('PHP Extensions') + ->description(implode(', ', array_keys($phpExtensions))) + ->icon($allExtensionsInstalled ? 'tabler-check' : 'tabler-x') + ->iconColor($allExtensionsInstalled ? 'success' : 'danger') + ->schema([ + Placeholder::make('') + ->content('All needed PHP Extensions are installed.') + ->visible($allExtensionsInstalled), + Placeholder::make('') + ->content('The following PHP Extensions are missing: ' . implode(', ', array_keys($phpExtensions, false))) + ->visible(!$allExtensionsInstalled), + ]); + + $folderPermissions = [ + 'Storage' => substr(sprintf('%o', fileperms(base_path('storage/'))), -4) >= 755, + 'Cache' => substr(sprintf('%o', fileperms(base_path('bootstrap/cache/'))), -4) >= 755, + ]; + $correctFolderPermissions = !in_array(false, $folderPermissions); + + $fields[] = Section::make('Folder Permissions') + ->description(implode(', ', array_keys($folderPermissions))) + ->icon($correctFolderPermissions ? 'tabler-check' : 'tabler-x') + ->iconColor($correctFolderPermissions ? 'success' : 'danger') + ->schema([ + Placeholder::make('') + ->content('All Folders have the correct permissions.') + ->visible($correctFolderPermissions), + Placeholder::make('') + ->content('The following Folders have wrong permissions: ' . implode(', ', array_keys($folderPermissions, false))) + ->visible(!$correctFolderPermissions), + ]); + + return Step::make('requirements') + ->label('Server Requirements') + ->schema($fields) + ->afterValidation(function () use ($correctPhpVersion, $allExtensionsInstalled, $correctFolderPermissions) { + if (!$correctPhpVersion || !$allExtensionsInstalled || !$correctFolderPermissions) { + Notification::make() + ->title('Some requirements are missing!') + ->danger() + ->send(); + + throw new Halt(); + } + }); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 384e4501d1..19a43bef47 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -10,6 +10,8 @@ use Dedoc\Scramble\Scramble; use Dedoc\Scramble\Support\Generator\OpenApi; use Dedoc\Scramble\Support\Generator\SecurityScheme; +use Filament\Support\Colors\Color; +use Filament\Support\Facades\FilamentColor; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Pagination\Paginator; use Illuminate\Support\Facades\Broadcast; @@ -80,6 +82,15 @@ public function boot(): void Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) { $event->extendSocialite('discord', \SocialiteProviders\Discord\Provider::class); }); + + FilamentColor::register([ + 'danger' => Color::Red, + 'gray' => Color::Zinc, + 'info' => Color::Sky, + 'primary' => Color::Blue, + 'success' => Color::Green, + 'warning' => Color::Amber, + ]); } /** diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 923a3a77d3..8c8f6355e2 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -9,7 +9,6 @@ use Filament\Http\Middleware\DispatchServingFilamentEvent; use Filament\Panel; use Filament\PanelProvider; -use Filament\Support\Colors\Color; use Filament\Support\Facades\FilamentAsset; use Filament\Widgets; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; @@ -44,15 +43,6 @@ public function panel(Panel $panel): Panel ->brandLogo(config('app.logo')) ->brandLogoHeight('2rem') ->profile(EditProfile::class, false) - ->colors([ - 'danger' => Color::Red, - 'gray' => Color::Zinc, - 'info' => Color::Sky, - 'primary' => Color::Blue, - 'success' => Color::Green, - 'warning' => Color::Amber, - 'blurple' => Color::hex('#5865F2'), - ]) ->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources') ->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages') ->discoverClusters(in: app_path('Filament/Clusters'), for: 'App\\Filament\\Clusters') diff --git a/app/Traits/Commands/RequestRedisSettingsTrait.php b/app/Traits/Commands/RequestRedisSettingsTrait.php new file mode 100644 index 0000000000..527915da6d --- /dev/null +++ b/app/Traits/Commands/RequestRedisSettingsTrait.php @@ -0,0 +1,37 @@ +output->note(__('commands.appsettings.redis.note')); + $this->variables['REDIS_HOST'] = $this->option('redis-host') ?? $this->ask( + 'Redis Host', + config('database.redis.default.host') + ); + + $askForRedisPassword = true; + if (!empty(config('database.redis.default.password'))) { + $this->variables['REDIS_PASSWORD'] = config('database.redis.default.password'); + $askForRedisPassword = $this->confirm('It seems a password is already defined for Redis, would you like to change it?'); + } + + if ($askForRedisPassword) { + $this->output->comment(__('commands.appsettings.redis.comment')); + $this->variables['REDIS_PASSWORD'] = $this->option('redis-pass') ?? $this->output->askHidden( + 'Redis Password' + ); + } + + if (empty($this->variables['REDIS_PASSWORD'])) { + $this->variables['REDIS_PASSWORD'] = 'null'; + } + + $this->variables['REDIS_PORT'] = $this->option('redis-port') ?? $this->ask( + 'Redis Port', + config('database.redis.default.port') + ); + } +} diff --git a/app/helpers.php b/app/helpers.php index c2aa5cd749..4f3e87021f 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -40,3 +40,11 @@ function object_get_strict(object $object, ?string $key, mixed $default = null): return $object; } } + +if (!function_exists('is_installed')) { + function is_installed(): bool + { + // This defaults to true so existing panels count as "installed" + return env('APP_INSTALLED', true); + } +} diff --git a/resources/views/filament/pages/installer.blade.php b/resources/views/filament/pages/installer.blade.php new file mode 100644 index 0000000000..1977991c2c --- /dev/null +++ b/resources/views/filament/pages/installer.blade.php @@ -0,0 +1,7 @@ + + + {{ $this->form }} + + + + \ No newline at end of file diff --git a/routes/base.php b/routes/base.php index 6fbc41ae81..69e1c2f9a7 100644 --- a/routes/base.php +++ b/routes/base.php @@ -1,5 +1,6 @@ withoutMiddleware(['auth', RequireTwoFactorAuthentication::class]) ->where('namespace', '.*'); +Route::get('installer', PanelInstaller::class)->name('installer') + ->withoutMiddleware(['auth', RequireTwoFactorAuthentication::class]); + Route::get('/{react}', [Base\IndexController::class, 'index']) ->where('react', '^(?!(\/)?(api|auth|admin|daemon|legacy)).+'); From 953ee940aa5955dfdad452dbdc46f48ca5e5bb55 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Sun, 4 Aug 2024 18:53:54 +0200 Subject: [PATCH 29/43] Installer followup (#519) * remove queue worker service creation from installer * auto check redis --- .../Environment/QueueWorkerServiceCommand.php | 4 ++-- app/Filament/Pages/Installer/PanelInstaller.php | 12 ------------ 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/app/Console/Commands/Environment/QueueWorkerServiceCommand.php b/app/Console/Commands/Environment/QueueWorkerServiceCommand.php index 607f2ae9bc..dbf1aa7354 100644 --- a/app/Console/Commands/Environment/QueueWorkerServiceCommand.php +++ b/app/Console/Commands/Environment/QueueWorkerServiceCommand.php @@ -14,7 +14,6 @@ class QueueWorkerServiceCommand extends Command {--service-name= : Name of the queue worker service.} {--user= : The user that PHP runs under.} {--group= : The group that PHP runs under.} - {--use-redis : Whether redis is used.} {--overwrite : Force overwrite if the service file already exists.}'; public function handle(): void @@ -32,7 +31,8 @@ public function handle(): void $user = $this->option('user') ?? $this->ask('Webserver User', 'www-data'); $group = $this->option('group') ?? $this->ask('Webserver Group', 'www-data'); - $afterRedis = $this->option('use-redis') ? ' + $redisUsed = config('queue.default') === 'redis' || config('session.driver') === 'redis' || config('cache.default') === 'redis'; + $afterRedis = $redisUsed ? ' After=redis-server.service' : ''; $basePath = base_path(); diff --git a/app/Filament/Pages/Installer/PanelInstaller.php b/app/Filament/Pages/Installer/PanelInstaller.php index 222793a646..00b182af08 100644 --- a/app/Filament/Pages/Installer/PanelInstaller.php +++ b/app/Filament/Pages/Installer/PanelInstaller.php @@ -99,18 +99,6 @@ public function submit() $variables = array_get($inputs, 'env'); $this->writeToEnvironment($variables); - $redisUsed = count(collect($variables)->filter(function ($item) { - return $item === 'redis'; - })) !== 0; - - // Create queue worker service (if needed) - if ($variables['QUEUE_CONNECTION'] !== 'sync') { - Artisan::call('p:environment:queue-service', [ - '--use-redis' => $redisUsed, - '--overwrite' => true, - ]); - } - // Run migrations Artisan::call('migrate', [ '--force' => true, From e6e3ce0f9d1665b9c6ee188f21d1f6310b341023 Mon Sep 17 00:00:00 2001 From: notCharles Date: Sun, 4 Aug 2024 14:59:49 -0400 Subject: [PATCH 30/43] Add Assign All, Layout Fixes. --- .../UserResource/Pages/ListUsers.php | 97 ++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/app/Filament/App/Resources/UserResource/Pages/ListUsers.php b/app/Filament/App/Resources/UserResource/Pages/ListUsers.php index b9a61187dc..d0935e0666 100644 --- a/app/Filament/App/Resources/UserResource/Pages/ListUsers.php +++ b/app/Filament/App/Resources/UserResource/Pages/ListUsers.php @@ -6,11 +6,15 @@ use App\Services\Subusers\SubuserCreationService; use Filament\Actions; use Filament\Facades\Filament; +use Filament\Forms\Components\Actions as assignAll; +use Filament\Forms\Components\Actions\Action; use Filament\Forms\Components\CheckboxList; use Filament\Forms\Components\Grid; use Filament\Forms\Components\Section; use Filament\Forms\Components\Tabs; use Filament\Forms\Components\TextInput; +use Filament\Forms\Get; +use Filament\Forms\Set; use Filament\Notifications\Notification; use Filament\Resources\Pages\ListRecords; @@ -27,12 +31,102 @@ protected function getHeaderActions(): array ->form([ Grid::make() ->columnSpanFull() + ->columns([ + 'default' => 1, + 'sm' => 1, + 'md' => 5, + 'lg' => 6, + ]) ->schema([ TextInput::make('email') ->email() - ->columnSpanFull() + ->inlineLabel() + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 4, + 'lg' => 5, + ]) ->required() ->unique(), + assignAll::make([ + Action::make('assignAll') + ->label('Assign All') + ->action(function (Set $set, Get $get) { + $permissions = [ + 'control' => [ + 'console', + 'start', + 'stop', + 'restart', + 'kill', + ], + 'user' => [ + 'read', + 'create', + 'update', + 'delete', + ], + 'file' => [ + 'read', + 'read-content', + 'create', + 'update', + 'delete', + 'archive', + 'sftp', + ], + 'backup' => [ + 'read', + 'create', + 'delete', + 'download', + 'restore', + ], + 'allocation' => [ + 'read', + 'create', + 'update', + 'delete', + ], + 'startup' => [ + 'read', + 'update', + 'docker-image', + ], + 'database' => [ + 'read', + 'create', + 'update', + 'delete', + 'view_password', + ], + 'schedule' => [ + 'read', + 'create', + 'update', + 'delete', + ], + 'settings' => [ + 'rename', + 'reinstall', + 'activity', + ], + ]; + + foreach ($permissions as $key => $value) { + $currentValues = $get($key) ?? []; + $allValues = array_unique(array_merge($currentValues, $value)); + $set($key, $allValues); + } + }), + ]) + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 1, + ]), Tabs::make() ->columnSpanFull() ->schema([ @@ -265,6 +359,7 @@ protected function getHeaderActions(): array ]), ]) ->modalHeading('Invite User') + ->modalSubmitActionLabel('Invite') ->action(function (array $data) { $email = $data['email']; $permissions = collect($data)->forget('email')->map(fn ($permissions, $key) => collect($permissions)->map(fn ($permission) => "$key.$permission"))->flatten()->all(); From 1636164c74adf93fe42d679c86f77bb7e1a0c6bc Mon Sep 17 00:00:00 2001 From: notCharles Date: Sun, 4 Aug 2024 16:18:41 -0400 Subject: [PATCH 31/43] conflict --- app/Services/Helpers/SoftwareVersionService.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Services/Helpers/SoftwareVersionService.php b/app/Services/Helpers/SoftwareVersionService.php index 7199d5679c..7d50203bf1 100644 --- a/app/Services/Helpers/SoftwareVersionService.php +++ b/app/Services/Helpers/SoftwareVersionService.php @@ -2,7 +2,7 @@ namespace App\Services\Helpers; -use Exception; +use GuzzleHttp\Client; use Carbon\CarbonImmutable; use GuzzleHttp\Exception\GuzzleException; use Illuminate\Support\Arr; @@ -19,7 +19,7 @@ class SoftwareVersionService */ public function __construct( protected CacheRepository $cache, - protected Factory $factory, + protected Client $client ) { self::$result = $this->cacheVersionData(); } From 6b857d70ccefa495218101f7b2c2262b5d72db18 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 5 Aug 2024 13:41:24 +0200 Subject: [PATCH 32/43] update schedule pages --- .../App/Resources/ScheduleResource.php | 108 +++++++++--------- .../ScheduleResource/Pages/CreateSchedule.php | 87 ++++---------- .../ScheduleResource/Pages/ListSchedules.php | 39 +++++++ .../RelationManagers/TasksRelationManager.php | 84 ++++++++++++++ 4 files changed, 200 insertions(+), 118 deletions(-) create mode 100644 app/Filament/App/Resources/ScheduleResource/RelationManagers/TasksRelationManager.php diff --git a/app/Filament/App/Resources/ScheduleResource.php b/app/Filament/App/Resources/ScheduleResource.php index b888b74a28..aa9c8c832e 100644 --- a/app/Filament/App/Resources/ScheduleResource.php +++ b/app/Filament/App/Resources/ScheduleResource.php @@ -3,10 +3,12 @@ namespace App\Filament\App\Resources; use App\Filament\App\Resources\ScheduleResource\Pages; +use App\Filament\App\Resources\ScheduleResource\RelationManagers\TasksRelationManager; use App\Models\Schedule; +use Filament\Forms\Components\TextInput; +use Filament\Forms\Components\Toggle; +use Filament\Forms\Form; use Filament\Resources\Resource; -use Filament\Tables; -use Filament\Tables\Table; class ScheduleResource extends Resource { @@ -14,65 +16,63 @@ class ScheduleResource extends Resource protected static ?string $navigationIcon = 'tabler-clock'; - public static function table(Table $table): Table + public static function form(Form $form): Form { - return $table - ->columns([ - Tables\Columns\TextColumn::make('server.name') - ->numeric() - ->sortable(), - Tables\Columns\TextColumn::make('name') - ->searchable(), - Tables\Columns\TextColumn::make('cron_day_of_week') - ->searchable(), - Tables\Columns\TextColumn::make('cron_day_of_month') - ->searchable(), - Tables\Columns\TextColumn::make('cron_hour') - ->searchable(), - Tables\Columns\TextColumn::make('cron_minute') - ->searchable(), - Tables\Columns\IconColumn::make('is_active') - ->boolean(), - Tables\Columns\IconColumn::make('is_processing') - ->boolean(), - Tables\Columns\TextColumn::make('last_run_at') - ->dateTime() - ->sortable(), - Tables\Columns\TextColumn::make('next_run_at') - ->dateTime() - ->sortable(), - Tables\Columns\TextColumn::make('created_at') - ->dateTime() - ->sortable() - ->toggleable(isToggledHiddenByDefault: true), - Tables\Columns\TextColumn::make('updated_at') - ->dateTime() - ->sortable() - ->toggleable(isToggledHiddenByDefault: true), - Tables\Columns\TextColumn::make('cron_month') - ->searchable(), - Tables\Columns\TextColumn::make('only_when_online') - ->numeric() - ->sortable(), - ]) - ->filters([ - // - ]) - ->actions([ - Tables\Actions\ViewAction::make(), - Tables\Actions\EditAction::make(), - ]) - ->bulkActions([ - Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), - ]), + return $form + ->columns(10) + ->schema([ + TextInput::make('name') + ->columnSpan(10) + ->label('Schedule Name') + ->placeholder('A human readable identifier for this schedule.') + ->autocomplete(false) + ->required(), + Toggle::make('only_when_online') + ->label('Only when Server is Online?') + ->hintIconTooltip('Only execute this schedule when the server is in a running state.') + ->hintIcon('tabler-question-mark') + ->columnSpan(5) + ->required() + ->default(1), + Toggle::make('is_active') + ->label('Enable Schedule?') + ->hintIconTooltip('This schedule will be executed automatically if enabled.') + ->hintIcon('tabler-question-mark') + ->columnSpan(5) + ->required() + ->default(1), + TextInput::make('cron_minute') + ->columnSpan(2) + ->label('Minute') + ->default('*/5') + ->required(), + TextInput::make('cron_hour') + ->columnSpan(2) + ->label('Hour') + ->default('*') + ->required(), + TextInput::make('cron_day_of_month') + ->columnSpan(2) + ->label('Day of Month') + ->default('*') + ->required(), + TextInput::make('cron_month') + ->columnSpan(2) + ->label('Month') + ->default('*') + ->required(), + TextInput::make('cron_day_of_week') + ->columnSpan(2) + ->label('Day of Week') + ->default('*') + ->required(), ]); } public static function getRelations(): array { return [ - // + TasksRelationManager::class, ]; } diff --git a/app/Filament/App/Resources/ScheduleResource/Pages/CreateSchedule.php b/app/Filament/App/Resources/ScheduleResource/Pages/CreateSchedule.php index d886f6db6e..a6fb998549 100644 --- a/app/Filament/App/Resources/ScheduleResource/Pages/CreateSchedule.php +++ b/app/Filament/App/Resources/ScheduleResource/Pages/CreateSchedule.php @@ -2,12 +2,12 @@ namespace App\Filament\App\Resources\ScheduleResource\Pages; +use App\Exceptions\DisplayException; use App\Filament\App\Resources\ScheduleResource; +use App\Helpers\Utilities; +use Carbon\Carbon; +use Exception; use Filament\Facades\Filament; -use Filament\Forms\Components\Section; -use Filament\Forms\Components\TextInput; -use Filament\Forms\Components\Toggle; -use Filament\Forms\Form; use Filament\Resources\Pages\CreateRecord; class CreateSchedule extends CreateRecord @@ -15,73 +15,32 @@ class CreateSchedule extends CreateRecord protected static string $resource = ScheduleResource::class; protected static bool $canCreateAnother = false; - public function form(Form $form): Form - { - return $form - ->columns(10) - ->schema([ - TextInput::make('name') - ->columnSpan(10) - ->label('Schedule Name') - ->placeholder('A human readable identifier for this schedule.') - ->autocomplete(false) - ->required(), - TextInput::make('cron_minute') - ->columnSpan(2) - ->label('Minute') - ->default('*/5') - ->required(), - TextInput::make('cron_hour') - ->columnSpan(2) - ->label('Hour') - ->default('*') - ->required(), - TextInput::make('cron_day_of_month') - ->columnSpan(2) - ->label('Day of Month') - ->default('*') - ->required(), - TextInput::make('cron_month') - ->columnSpan(2) - ->label('Month') - ->default('*') - ->required(), - TextInput::make('cron_day_of_week') - ->columnSpan(2) - ->label('Day of Week') - ->default('*') - ->required(), - Toggle::make('only_when_online') - ->label('Only when Server is Online?') - ->hintIconTooltip('Only execute this schedule when the server is in a running state.') - ->hintIcon('tabler-question-mark') - ->columnSpan(5) - ->required() - ->default(1), - Toggle::make('is_active') - ->label('Enable Schedule?') - ->hintIconTooltip('This schedule will be executed automatically if enabled.') - ->hintIcon('tabler-question-mark') - ->columnSpan(5) - ->required() - ->default(1), - Section::make() - ->columnSpanFull() - ->collapsible()->collapsed() - ->icon('tabler-question-mark') - ->heading('Cron Expression Help') - ->schema([ - // TODO - ]), - ]); - } protected function mutateFormDataBeforeSave(array $data): array { if (!isset($data['server_id'])) { $data['server_id'] = Filament::getTenant()->id; } + if (!isset($data['next_run_at'])) { + $data['next_run_at'] = $this->getNextRunAt($data['cron_minute'], $data['cron_hour'], $data['cron_day_of_month'], $data['cron_month'], $data['cron_day_of_week']); + } + return $data; } + + protected function getNextRunAt(string $minute, string $hour, string $dayOfMonth, string $month, string $dayOfWeek): Carbon + { + try { + return Utilities::getScheduleNextRunDate( + $minute, + $hour, + $dayOfMonth, + $month, + $dayOfWeek + ); + } catch (Exception) { + throw new DisplayException('The cron data provided does not evaluate to a valid expression.'); + } + } } diff --git a/app/Filament/App/Resources/ScheduleResource/Pages/ListSchedules.php b/app/Filament/App/Resources/ScheduleResource/Pages/ListSchedules.php index 2dc2cc3a20..7c73554007 100644 --- a/app/Filament/App/Resources/ScheduleResource/Pages/ListSchedules.php +++ b/app/Filament/App/Resources/ScheduleResource/Pages/ListSchedules.php @@ -3,13 +3,52 @@ namespace App\Filament\App\Resources\ScheduleResource\Pages; use App\Filament\App\Resources\ScheduleResource; +use App\Models\Schedule; use Filament\Actions; use Filament\Resources\Pages\ListRecords; +use Filament\Tables\Actions\BulkActionGroup; +use Filament\Tables\Actions\DeleteBulkAction; +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(), + TextColumn::make('last_run_at') + ->dateTime() + ->sortable(), + TextColumn::make('next_run_at') + ->dateTime() + ->sortable(), + ]) + ->actions([ + ViewAction::make(), + EditAction::make(), + ]) + ->bulkActions([ + BulkActionGroup::make([ + DeleteBulkAction::make(), + ]), + ]); + } + protected function getHeaderActions(): array { return [ diff --git a/app/Filament/App/Resources/ScheduleResource/RelationManagers/TasksRelationManager.php b/app/Filament/App/Resources/ScheduleResource/RelationManagers/TasksRelationManager.php new file mode 100644 index 0000000000..7fdaf6fd7b --- /dev/null +++ b/app/Filament/App/Resources/ScheduleResource/RelationManagers/TasksRelationManager.php @@ -0,0 +1,84 @@ +getOwnerRecord(); + + return $table + ->reorderable('sequence_id', true) + ->columns([ + TextColumn::make('action'), + TextColumn::make('time_offset') + ->hidden(fn () => config('queue.default') === 'sync') + ->suffix(' Seconds'), + IconColumn::make('continue_on_failure') + ->boolean(), + ]) + ->headerActions([ + CreateAction::make() + ->label('Create Task') + ->createAnother(false) + ->form([ + TextInput::make('sequence_id') + ->hidden() + ->default(fn () => ($schedule->tasks()->orderByDesc('sequence_id')->first()->sequence_id ?? 0) + 1), + Select::make('action') + ->required() + ->live() + ->options([ + Task::ACTION_POWER => 'Send power action', + Task::ACTION_COMMAND => 'Send command', + Task::ACTION_BACKUP => 'Create backup', + Task::ACTION_DELETE_FILES => 'Delete files', + ]), + Textarea::make('payload') + ->hidden(fn (Get $get) => $get('action') === Task::ACTION_POWER) + ->label(fn (Get $get) => match ($get('action')) { + Task::ACTION_POWER => 'Power action', + Task::ACTION_COMMAND => 'Command', + Task::ACTION_BACKUP => 'Files to ignore', + Task::ACTION_DELETE_FILES => 'Files to delete', + default => 'Payload' + }), + Select::make('payload') + ->visible(fn (Get $get) => $get('action') === Task::ACTION_POWER) + ->label('Power Action') + ->required() + ->options([ + 'start' => 'Start', + 'restart' => 'Restart', + 'stop' => 'Stop', + 'Kill' => 'Kill', + ]), + TextInput::make('time_offset') + ->hidden(fn (Get $get) => config('queue.default') === 'sync' || $get('sequence_id') === 1) + ->default(0) + ->numeric() + ->minValue(0) + ->maxValue(900) + ->suffix('Seconds'), + Toggle::make('continue_on_failure'), + ]), + ]); + } +} From d7dd067b776da52b4d31b65aaf82bf84a4c5c63f Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 5 Aug 2024 14:10:22 +0200 Subject: [PATCH 33/43] fix phpstan --- .../Resources/DatabaseResource/Pages/ListDatabases.php | 4 +++- .../ScheduleResource/Pages/CreateSchedule.php | 6 +++++- .../App/Resources/UserResource/Pages/ListUsers.php | 10 +++++++++- .../Resources/UserResource/Pages/EditProfile.php | 2 +- .../Api/Remote/Servers/ServerContainersController.php | 2 +- app/Models/User.php | 4 ++-- 6 files changed, 21 insertions(+), 7 deletions(-) diff --git a/app/Filament/App/Resources/DatabaseResource/Pages/ListDatabases.php b/app/Filament/App/Resources/DatabaseResource/Pages/ListDatabases.php index 88f88c0e57..22eca7c86c 100644 --- a/app/Filament/App/Resources/DatabaseResource/Pages/ListDatabases.php +++ b/app/Filament/App/Resources/DatabaseResource/Pages/ListDatabases.php @@ -5,6 +5,7 @@ use App\Filament\App\Resources\DatabaseResource; use App\Models\Database; use App\Models\DatabaseHost; +use App\Models\Server; use App\Services\Databases\DatabaseManagementService; use App\Services\Databases\DatabasePasswordService; use Filament\Actions\CreateAction; @@ -42,7 +43,7 @@ public function form(Form $form): Form ->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'), + ->formatStateUsing(fn (Database $database) => $database->max_connections === 0 ? $database->max_connections : 'Unlimited'), TextInput::make('JDBC') ->label('JDBC Connection String') ->suffixAction(CopyAction::make()) @@ -70,6 +71,7 @@ public function table(Table $table): Table protected function getHeaderActions(): array { + /** @var Server $server */ $server = Filament::getTenant(); return [ diff --git a/app/Filament/App/Resources/ScheduleResource/Pages/CreateSchedule.php b/app/Filament/App/Resources/ScheduleResource/Pages/CreateSchedule.php index a6fb998549..cac1f599ad 100644 --- a/app/Filament/App/Resources/ScheduleResource/Pages/CreateSchedule.php +++ b/app/Filament/App/Resources/ScheduleResource/Pages/CreateSchedule.php @@ -5,6 +5,7 @@ use App\Exceptions\DisplayException; use App\Filament\App\Resources\ScheduleResource; use App\Helpers\Utilities; +use App\Models\Server; use Carbon\Carbon; use Exception; use Filament\Facades\Filament; @@ -19,7 +20,10 @@ class CreateSchedule extends CreateRecord protected function mutateFormDataBeforeSave(array $data): array { if (!isset($data['server_id'])) { - $data['server_id'] = Filament::getTenant()->id; + /** @var Server $server */ + $server = Filament::getTenant(); + + $data['server_id'] = $server->id; } if (!isset($data['next_run_at'])) { diff --git a/app/Filament/App/Resources/UserResource/Pages/ListUsers.php b/app/Filament/App/Resources/UserResource/Pages/ListUsers.php index d0935e0666..91761dd3d5 100644 --- a/app/Filament/App/Resources/UserResource/Pages/ListUsers.php +++ b/app/Filament/App/Resources/UserResource/Pages/ListUsers.php @@ -3,6 +3,7 @@ namespace App\Filament\App\Resources\UserResource\Pages; use App\Filament\App\Resources\UserResource; +use App\Models\Server; use App\Services\Subusers\SubuserCreationService; use Filament\Actions; use Filament\Facades\Filament; @@ -363,9 +364,16 @@ protected function getHeaderActions(): array ->action(function (array $data) { $email = $data['email']; $permissions = collect($data)->forget('email')->map(fn ($permissions, $key) => collect($permissions)->map(fn ($permission) => "$key.$permission"))->flatten()->all(); + + /** @var Server $server */ $server = Filament::getTenant(); + resolve(SubuserCreationService::class)->handle($server, $email, $permissions); // "It's Fine" ~ Lance - Notification::make()->title('User Invited!')->success()->send(); + + Notification::make() + ->title('User Invited!') + ->success() + ->send(); return redirect()->route('filament.app.resources.users.index', ['tenant' => $server]); }), diff --git a/app/Filament/Resources/UserResource/Pages/EditProfile.php b/app/Filament/Resources/UserResource/Pages/EditProfile.php index d8eb7274f0..1fe1336151 100644 --- a/app/Filament/Resources/UserResource/Pages/EditProfile.php +++ b/app/Filament/Resources/UserResource/Pages/EditProfile.php @@ -293,7 +293,7 @@ protected function handleRecordUpdate($record, $data): \Illuminate\Database\Eloq $service = resolve(ToggleTwoFactorService::class); $tokens = $service->handle($record, $token, true); - cache()->set("users.$record->id.2fa.tokens", implode("\n", $tokens), now()->addSeconds(15)); + cache(["users.$record->id.2fa.tokens" => implode("\n", $tokens)], now()->addSeconds(15)); $this->redirectRoute('filament.admin.auth.profile', ['tab' => '-2fa-tab']); } diff --git a/app/Http/Controllers/Api/Remote/Servers/ServerContainersController.php b/app/Http/Controllers/Api/Remote/Servers/ServerContainersController.php index cd59c3d2cc..5f106b426b 100644 --- a/app/Http/Controllers/Api/Remote/Servers/ServerContainersController.php +++ b/app/Http/Controllers/Api/Remote/Servers/ServerContainersController.php @@ -16,7 +16,7 @@ public function status(Server $server, Request $request): JsonResponse { $status = fluent($request->json()->all())->get('data.new_state'); - cache()->set("servers.$server->uuid.container.status", $status, now()->addHour()); + cache(["servers.$server->uuid.container.status" => $status], now()->addHour()); return new JsonResponse([]); } diff --git a/app/Models/User.php b/app/Models/User.php index 49b9ded19d..12ac1d6169 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -361,7 +361,7 @@ public function isLastRootAdmin(): bool public function canAccessPanel(Panel $panel): bool { - return $this->root_admin; + return $this->root_admin; // TODO } public function getFilamentName(): string @@ -388,6 +388,6 @@ public function canAccessTenant(\Illuminate\Database\Eloquent\Model $tenant): bo { return true; - return $this->servers()->whereKey($tenant)->exists(); + //return $this->servers()->whereKey($tenant)->exists(); // TODO } } From 572991a8c295e2dba97aa94421cf1b99f5f4de8d Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 5 Aug 2024 14:15:59 +0200 Subject: [PATCH 34/43] update pint.json --- pint.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pint.json b/pint.json index 4d7ccf1135..845f4e54bd 100644 --- a/pint.json +++ b/pint.json @@ -7,6 +7,7 @@ "nullable_type_declaration_for_default_null_value": false, "ordered_imports": false, "phpdoc_align": false, - "phpdoc_separation": false + "phpdoc_separation": false, + "single_line_empty_body": false } -} +} \ No newline at end of file From 1a94c2a8fe72f4435badf89e60be4d55760a43e9 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 5 Aug 2024 14:16:12 +0200 Subject: [PATCH 35/43] add cron presets to schedule --- .../App/Resources/ScheduleResource.php | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/app/Filament/App/Resources/ScheduleResource.php b/app/Filament/App/Resources/ScheduleResource.php index aa9c8c832e..82289cd466 100644 --- a/app/Filament/App/Resources/ScheduleResource.php +++ b/app/Filament/App/Resources/ScheduleResource.php @@ -5,9 +5,14 @@ use App\Filament\App\Resources\ScheduleResource\Pages; use App\Filament\App\Resources\ScheduleResource\RelationManagers\TasksRelationManager; use App\Models\Schedule; +use Filament\Forms\Components\Actions; +use Filament\Forms\Components\Actions\Action; +use Filament\Forms\Components\Section; +use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; use Filament\Forms\Form; +use Filament\Forms\Set; use Filament\Resources\Resource; class ScheduleResource extends Resource @@ -66,6 +71,142 @@ public static function form(Form $form): Form ->label('Day of Week') ->default('*') ->required(), + Section::make('Presets') + ->schema([ + Actions::make([ + Action::make('hourly') + ->disabled(fn (string $operation) => $operation === 'view') + ->action(function (Set $set) { + $set('cron_minute', '0'); + $set('cron_hour', '*'); + $set('cron_day_of_month', '*'); + $set('cron_month', '*'); + $set('cron_day_of_week', '*'); + }), + Action::make('daily') + ->disabled(fn (string $operation) => $operation === 'view') + ->action(function (Set $set) { + $set('cron_minute', '0'); + $set('cron_hour', '0'); + $set('cron_day_of_month', '*'); + $set('cron_month', '*'); + $set('cron_day_of_week', '*'); + }), + Action::make('weekly') + ->disabled(fn (string $operation) => $operation === 'view') + ->action(function (Set $set) { + $set('cron_minute', '0'); + $set('cron_hour', '0'); + $set('cron_day_of_month', '*'); + $set('cron_month', '*'); + $set('cron_day_of_week', '0'); + }), + Action::make('monthly') + ->disabled(fn (string $operation) => $operation === 'view') + ->action(function (Set $set) { + $set('cron_minute', '0'); + $set('cron_hour', '0'); + $set('cron_day_of_month', '1'); + $set('cron_month', '*'); + $set('cron_day_of_week', '0'); + }), + Action::make('every_x_minutes') + ->disabled(fn (string $operation) => $operation === 'view') + ->form([ + TextInput::make('x') + ->label('') + ->numeric() + ->minValue(1) + ->maxValue(60) + ->prefix('Every') + ->suffix('Minutes'), + ]) + ->action(function (Set $set, $data) { + $set('cron_minute', '*/' . $data['x']); + $set('cron_hour', '*'); + $set('cron_day_of_month', '*'); + $set('cron_month', '*'); + $set('cron_day_of_week', '*'); + }), + Action::make('every_x_hours') + ->disabled(fn (string $operation) => $operation === 'view') + ->form([ + TextInput::make('x') + ->label('') + ->numeric() + ->minValue(1) + ->maxValue(24) + ->prefix('Every') + ->suffix('Hours'), + ]) + ->action(function (Set $set, $data) { + $set('cron_minute', '0'); + $set('cron_hour', '*/' . $data['x']); + $set('cron_day_of_month', '*'); + $set('cron_month', '*'); + $set('cron_day_of_week', '*'); + }), + Action::make('every_x_days') + ->disabled(fn (string $operation) => $operation === 'view') + ->form([ + TextInput::make('x') + ->label('') + ->numeric() + ->minValue(1) + ->maxValue(24) + ->prefix('Every') + ->suffix('Days'), + ]) + ->action(function (Set $set, $data) { + $set('cron_minute', '0'); + $set('cron_hour', '0'); + $set('cron_day_of_month', '*/' . $data['x']); + $set('cron_month', '*'); + $set('cron_day_of_week', '*'); + }), + Action::make('every_x_months') + ->disabled(fn (string $operation) => $operation === 'view') + ->form([ + TextInput::make('x') + ->label('') + ->numeric() + ->minValue(1) + ->maxValue(24) + ->prefix('Every') + ->suffix('Months'), + ]) + ->action(function (Set $set, $data) { + $set('cron_minute', '0'); + $set('cron_hour', '0'); + $set('cron_day_of_month', '0'); + $set('cron_month', '*/' . $data['x']); + $set('cron_day_of_week', '*'); + }), + Action::make('every_x_day_of_week') + ->disabled(fn (string $operation) => $operation === 'view') + ->form([ + Select::make('x') + ->label('') + ->prefix('Every') + ->options([ + '0' => 'Sunday', + '1' => 'Monday', + '2' => 'Tuesday', + '3' => 'Wednesday', + '4' => 'Thursday', + '5' => 'Friday', + '6' => 'Saturday', + ]), + ]) + ->action(function (Set $set, $data) { + $set('cron_minute', '0'); + $set('cron_hour', '0'); + $set('cron_day_of_month', '*'); + $set('cron_month', '*'); + $set('cron_day_of_week', $data['x']); + }), + ]), + ]), ]); } From 8c94c378edf112dd848cd9d35f53567f5c206ec1 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 5 Aug 2024 14:25:14 +0200 Subject: [PATCH 36/43] fix tests --- app/Transformers/Api/Client/ServerTransformer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Transformers/Api/Client/ServerTransformer.php b/app/Transformers/Api/Client/ServerTransformer.php index 517f62e15c..07cd64dbd2 100644 --- a/app/Transformers/Api/Client/ServerTransformer.php +++ b/app/Transformers/Api/Client/ServerTransformer.php @@ -16,7 +16,7 @@ class ServerTransformer extends BaseClientTransformer { - protected array $defaultIncludes = ['variables']; + protected array $defaultIncludes = ['allocations', 'variables']; protected array $availableIncludes = ['egg', 'subusers']; From 2d57a2e42e400767fe13c32f2f3738dc757a93d4 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 5 Aug 2024 14:41:16 +0200 Subject: [PATCH 37/43] fix task creation --- .../RelationManagers/TasksRelationManager.php | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/app/Filament/App/Resources/ScheduleResource/RelationManagers/TasksRelationManager.php b/app/Filament/App/Resources/ScheduleResource/RelationManagers/TasksRelationManager.php index 7fdaf6fd7b..67215374c7 100644 --- a/app/Filament/App/Resources/ScheduleResource/RelationManagers/TasksRelationManager.php +++ b/app/Filament/App/Resources/ScheduleResource/RelationManagers/TasksRelationManager.php @@ -2,6 +2,7 @@ namespace App\Filament\App\Resources\ScheduleResource\RelationManagers; +use App\Facades\Activity; use App\Models\Schedule; use App\Models\Task; use Filament\Forms\Components\Select; @@ -27,7 +28,14 @@ public function table(Table $table): Table return $table ->reorderable('sequence_id', true) ->columns([ - TextColumn::make('action'), + TextColumn::make('action') + ->state(fn (Task $task) => match ($task->action) { + Task::ACTION_POWER => 'Send power action', + Task::ACTION_COMMAND => 'Send command', + Task::ACTION_BACKUP => 'Create backup', + Task::ACTION_DELETE_FILES => 'Delete files', + default => $task->action + }), TextColumn::make('time_offset') ->hidden(fn () => config('queue.default') === 'sync') ->suffix(' Seconds'), @@ -39,9 +47,6 @@ public function table(Table $table): Table ->label('Create Task') ->createAnother(false) ->form([ - TextInput::make('sequence_id') - ->hidden() - ->default(fn () => ($schedule->tasks()->orderByDesc('sequence_id')->first()->sequence_id ?? 0) + 1), Select::make('action') ->required() ->live() @@ -78,7 +83,24 @@ public function table(Table $table): Table ->maxValue(900) ->suffix('Seconds'), Toggle::make('continue_on_failure'), - ]), + ]) + ->action(function ($data) use ($schedule) { + $sequenceId = ($schedule->tasks()->orderByDesc('sequence_id')->first()->sequence_id ?? 0) + 1; + + $task = Task::query()->create([ + 'schedule_id' => $schedule->id, + 'sequence_id' => $sequenceId, + 'action' => $data['action'], + 'payload' => $data['payload'] ?? '', + 'time_offset' => $data['time_offset'] ?? 0, + 'continue_on_failure' => (bool) $data['continue_on_failure'], + ]); + + Activity::event('server:task.create') + ->subject($schedule, $task) + ->property(['name' => $schedule->name, 'action' => $task->action, 'payload' => $task->payload]) + ->log(); + }), ]); } } From b23c9cf4b4abf263656f1cb64c61bd4227d68153 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 5 Aug 2024 14:55:14 +0200 Subject: [PATCH 38/43] schedules: disable task creation if limit is reached & disable backup action if backup limit is 0 --- .../RelationManagers/TasksRelationManager.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/Filament/App/Resources/ScheduleResource/RelationManagers/TasksRelationManager.php b/app/Filament/App/Resources/ScheduleResource/RelationManagers/TasksRelationManager.php index 67215374c7..0671d2a686 100644 --- a/app/Filament/App/Resources/ScheduleResource/RelationManagers/TasksRelationManager.php +++ b/app/Filament/App/Resources/ScheduleResource/RelationManagers/TasksRelationManager.php @@ -44,12 +44,14 @@ public function table(Table $table): Table ]) ->headerActions([ CreateAction::make() - ->label('Create Task') ->createAnother(false) + ->label(fn () => $schedule->tasks()->count() >= config('panel.client_features.schedules.per_schedule_task_limit', 10) ? 'Task Limit Reached' : 'Create Task') + ->disabled(fn () => $schedule->tasks()->count() >= config('panel.client_features.schedules.per_schedule_task_limit', 10)) ->form([ Select::make('action') ->required() ->live() + ->disableOptionWhen(fn (string $value): bool => $value === Task::ACTION_BACKUP && $schedule->server->backup_limit === 0) ->options([ Task::ACTION_POWER => 'Send power action', Task::ACTION_COMMAND => 'Send command', From d80094a3e050bd004b3c902bef09ec48914759b3 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Mon, 5 Aug 2024 16:27:58 +0200 Subject: [PATCH 39/43] update activity pages --- .../App/Resources/ActivityResource.php | 38 +------------------ .../ActivityResource/Pages/ListActivities.php | 20 ++++++++++ 2 files changed, 21 insertions(+), 37 deletions(-) diff --git a/app/Filament/App/Resources/ActivityResource.php b/app/Filament/App/Resources/ActivityResource.php index 8b7661ba88..0ac0d8b59f 100644 --- a/app/Filament/App/Resources/ActivityResource.php +++ b/app/Filament/App/Resources/ActivityResource.php @@ -4,10 +4,7 @@ use App\Filament\App\Resources\ActivityResource\Pages; use App\Models\ActivityLog; -use Filament\Forms\Form; use Filament\Resources\Resource; -use Filament\Tables; -use Filament\Tables\Table; class ActivityResource extends Resource { @@ -17,40 +14,7 @@ class ActivityResource extends Resource protected static ?string $tenantOwnershipRelationshipName = 'actor'; - public static function form(Form $form): Form - { - return $form - ->schema([ - // - ]); - } - - public static function table(Table $table): Table - { - return $table - ->columns([ - // - ]) - ->filters([ - // - ]) - ->actions([ - Tables\Actions\ViewAction::make(), - Tables\Actions\EditAction::make(), - ]) - ->bulkActions([ - Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), - ]), - ]); - } - - public static function getRelations(): array - { - return [ - // - ]; - } + protected static ?string $tenantRelationshipName = 'activity'; public static function getPages(): array { diff --git a/app/Filament/App/Resources/ActivityResource/Pages/ListActivities.php b/app/Filament/App/Resources/ActivityResource/Pages/ListActivities.php index 7c9ab806b8..8f6b5c8dfb 100644 --- a/app/Filament/App/Resources/ActivityResource/Pages/ListActivities.php +++ b/app/Filament/App/Resources/ActivityResource/Pages/ListActivities.php @@ -3,9 +3,29 @@ namespace App\Filament\App\Resources\ActivityResource\Pages; use App\Filament\App\Resources\ActivityResource; +use App\Models\ActivityLog; +use App\Models\User; use Filament\Resources\Pages\ListRecords; +use Filament\Tables\Columns\TextColumn; +use Filament\Tables\Table; class ListActivities extends ListRecords { protected static string $resource = ActivityResource::class; + + public function table(Table $table): Table + { + return $table + ->columns([ + TextColumn::make('event') + ->formatStateUsing(fn ($state, ActivityLog $activityLog) => __('activity.'.str($activityLog->event)->replace(':', '.'), $activityLog->properties?->toArray() ?? [])) + ->description(fn ($state) => $state), + TextColumn::make('user') + ->state(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User ? $activityLog->actor->username : 'System') + ->url(fn (ActivityLog $activityLog): string => $activityLog->actor instanceof User ? route('filament.admin.resources.users.edit', ['record' => $activityLog->actor]) : ''), + TextColumn::make('timestamp') + ->sortable() + ->formatStateUsing(fn ($state) => $state->diffForHumans()), + ]); + } } From 92a6c2c7e993350acce17fb73c416a8961df79f5 Mon Sep 17 00:00:00 2001 From: notCharles Date: Mon, 5 Aug 2024 20:11:51 -0400 Subject: [PATCH 40/43] update resources --- .../Resources/BackupResource/Pages/ListBackups.php | 12 +++++++----- .../App/Resources/UserResource/Pages/ListUsers.php | 4 +++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/Filament/App/Resources/BackupResource/Pages/ListBackups.php b/app/Filament/App/Resources/BackupResource/Pages/ListBackups.php index bdbc6fc100..84a119057c 100644 --- a/app/Filament/App/Resources/BackupResource/Pages/ListBackups.php +++ b/app/Filament/App/Resources/BackupResource/Pages/ListBackups.php @@ -8,6 +8,7 @@ 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; @@ -52,7 +53,7 @@ public function form(Form $form): Form public function table(Table $table): Table { - /** @var \App\Models\Server $server */ + /** @var Server $server */ $server = Filament::getTenant(); return $table @@ -98,7 +99,7 @@ public function table(Table $table): Table ]) ->action(function (Backup $backup, $data, DaemonBackupRepository $daemonRepository, DownloadLinkService $downloadLinkService) { - /** @var \App\Models\Server $server */ + /** @var Server $server */ $server = Filament::getTenant(); if (!is_null($server->status)) { @@ -160,18 +161,19 @@ public function table(Table $table): Table protected function getHeaderActions(): array { - /** @var \App\Models\Server $server */ + /** @var Server $server */ $server = Filament::getTenant(); return [ Actions\CreateAction::make() + ->hidden(!auth()->user()->can(Permission::ACTION_BACKUP_CREATE, Filament::getTenant())) ->label(fn () => $server->backups()->count() >= $server->backup_limit ? 'Backup Limit Reached' : 'Create Backup') ->disabled(fn () => $server->backups()->count() >= $server->backup_limit) ->color(fn () => $server->backups()->count() >= $server->backup_limit ? 'danger' : 'primary') ->createAnother(false) ->action(function (InitiateBackupService $initiateBackupService, $data) { - /** @var \App\Models\Server $server */ + /** @var Server $server */ $server = Filament::getTenant(); $action = $initiateBackupService @@ -197,7 +199,7 @@ protected function getHeaderActions(): array ]; } - public function convertToReadableSize($size) + public function convertToReadableSize($size) //Replace with panel prefix config { $base = log($size) / log(1024); $suffix = ['', 'KB', 'MB', 'GB', 'TB']; diff --git a/app/Filament/App/Resources/UserResource/Pages/ListUsers.php b/app/Filament/App/Resources/UserResource/Pages/ListUsers.php index 91761dd3d5..97e89cf821 100644 --- a/app/Filament/App/Resources/UserResource/Pages/ListUsers.php +++ b/app/Filament/App/Resources/UserResource/Pages/ListUsers.php @@ -3,6 +3,7 @@ namespace App\Filament\App\Resources\UserResource\Pages; use App\Filament\App\Resources\UserResource; +use App\Models\Permission; use App\Models\Server; use App\Services\Subusers\SubuserCreationService; use Filament\Actions; @@ -29,6 +30,7 @@ protected function getHeaderActions(): array Actions\CreateAction::make('invite') ->label('Invite User') ->createAnother(false) + ->hidden(!auth()->user()->can(Permission::ACTION_USER_CREATE, Filament::getTenant())) ->form([ Grid::make() ->columnSpanFull() @@ -368,7 +370,7 @@ protected function getHeaderActions(): array /** @var Server $server */ $server = Filament::getTenant(); - resolve(SubuserCreationService::class)->handle($server, $email, $permissions); // "It's Fine" ~ Lance + resolve(SubuserCreationService::class)->handle($server, $email, $permissions); Notification::make() ->title('User Invited!') From f7a2c7cecd063f0cf45001aebd6f1c06e85eb587 Mon Sep 17 00:00:00 2001 From: notCharles Date: Mon, 5 Aug 2024 20:14:36 -0400 Subject: [PATCH 41/43] Update Edit User TODO: actually save permissions when they're changed. TODO: Figure out why Control does not update it's state... but the rest do... --- app/Filament/App/Resources/UserResource.php | 347 +++++++++++++++++++- 1 file changed, 338 insertions(+), 9 deletions(-) diff --git a/app/Filament/App/Resources/UserResource.php b/app/Filament/App/Resources/UserResource.php index 5b75d9686b..f12d3be623 100644 --- a/app/Filament/App/Resources/UserResource.php +++ b/app/Filament/App/Resources/UserResource.php @@ -3,7 +3,19 @@ namespace App\Filament\App\Resources; use App\Filament\App\Resources\UserResource\Pages; +use App\Models\Permission; +use App\Models\Server; +use App\Models\Subuser; use App\Models\User; +use Filament\Facades\Filament; +use Filament\Forms\Components\Actions as assignAll; +use Filament\Forms\Components\Actions\Action; +use Filament\Forms\Components\CheckboxList; +use Filament\Forms\Components\Grid; +use Filament\Forms\Components\Section; +use Filament\Forms\Components\Tabs; +use Filament\Forms\Components\TextInput; +use Filament\Forms\Set; use Filament\Tables\Actions\DeleteAction; use Filament\Resources\Resource; use Filament\Tables\Actions\EditAction; @@ -35,22 +47,339 @@ public static function table(Table $table): Table TextColumn::make('email') ->searchable(), TextColumn::make('permissions') - ->placeholder('Show # of permissions'), + ->state(function (User $user) { + /** @var Server $server */ + $server = Filament::getTenant(); + $permissions = Subuser::query()->where('user_id', $user->id)->where('server_id', $server->id)->first()->permissions; + + return count($permissions); + }), ]) ->actions([ DeleteAction::make() ->label('Remove User'), EditAction::make() - ->label('TODO: Edit User'), - ]); - } + ->label('Edit User') + ->hidden(!auth()->user()->can(Permission::ACTION_USER_UPDATE, Filament::getTenant())) + ->form([ + Grid::make() + ->columnSpanFull() + ->columns([ + 'default' => 1, + 'sm' => 1, + 'md' => 5, + 'lg' => 6, + ]) + ->schema([ + TextInput::make('email') + ->inlineLabel() + ->disabled() + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 4, + 'lg' => 5, + ]), + assignAll::make([ + Action::make('assignAll') + ->label('Assign All') + ->action(function (Set $set) { + $permissions = [ + 'control' => [ + 'console', + 'start', + 'stop', + 'restart', + 'kill', + ], + 'user' => [ + 'read', + 'create', + 'update', + 'delete', + ], + 'file' => [ + 'read', + 'read-content', + 'create', + 'update', + 'delete', + 'archive', + 'sftp', + ], + 'backup' => [ + 'read', + 'create', + 'delete', + 'download', + 'restore', + ], + 'allocation' => [ + 'read', + 'create', + 'update', + 'delete', + ], + 'startup' => [ + 'read', + 'update', + 'docker-image', + ], + 'database' => [ + 'read', + 'create', + 'update', + 'delete', + 'view_password', + ], + 'schedule' => [ + 'read', + 'create', + 'update', + 'delete', + ], + 'settings' => [ + 'rename', + 'reinstall', + 'activity', + ], + ]; - public static function getRelations(): array - { - return [ - // - ]; + foreach ($permissions as $key => $value) { + $allValues = array_unique($value); + $set($key, $allValues); + } + }), + ]) + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 1, + ]), + Tabs::make() + ->columnSpanFull() + ->schema([ + Tabs\Tab::make('Console') + ->schema([ + Section::make() + ->description(trans('server/users.permissions.control_desc')) + ->icon('tabler-terminal-2') + ->schema([ + CheckboxList::make('control') + ->bulkToggleable() + ->label('') + ->options([ + 'console' => 'Console', + 'start' => 'Start', + 'stop' => 'Stop', + 'restart' => 'Restart', + 'kill' => 'Kill', + ]) + ->descriptions([ + 'console' => trans('server/users.permissions.control_console'), + 'start' => trans('server/users.permissions.control_start'), + 'stop' => trans('server/users.permissions.control_stop'), + 'restart' => trans('server/users.permissions.control_restart'), + 'kill' => trans('server/users.permissions.control_kill'), + ]), + ]), + ]), + Tabs\Tab::make('User') + ->schema([ + Section::make() + ->description(trans('server/users.permissions.user_desc')) + ->icon('tabler-users') + ->schema([ + CheckboxList::make('user') + ->bulkToggleable() + ->label('') + ->options([ + 'read' => 'Read', + 'create' => 'Create', + 'update' => 'Update', + 'delete' => 'Delete', + ]) + ->descriptions([ + 'create' => trans('server/users.permissions.user_create'), + 'read' => trans('server/users.permissions.user_read'), + 'update' => trans('server/users.permissions.user_update'), + 'delete' => trans('server/users.permissions.user_delete'), + ]), + ]), + ]), + Tabs\Tab::make('File') + ->schema([ + Section::make() + ->description(trans('server/users.permissions.file_desc')) + ->icon('tabler-folders') + ->schema([ + CheckboxList::make('file') + ->bulkToggleable() + ->label('') + ->options([ + 'read' => 'Read', + 'read-content' => 'Read Content', + 'create' => 'Create', + 'update' => 'Update', + 'delete' => 'Delete', + 'archive' => 'Archive', + 'sftp' => 'SFTP', + ]) + ->descriptions([ + 'create' => trans('server/users.permissions.file_create'), + 'read' => trans('server/users.permissions.file_read'), + 'read-content' => trans('server/users.permissions.file_read_content'), + 'update' => trans('server/users.permissions.file_update'), + 'delete' => trans('server/users.permissions.file_delete'), + 'archive' => trans('server/users.permissions.file_archive'), + 'sftp' => trans('server/users.permissions.file_sftp'), + ]), + ]), + ]), + Tabs\Tab::make('Backup') + ->schema([ + Section::make() + ->description(trans('server/users.permissions.backup_desc')) + ->icon('tabler-download') + ->schema([ + CheckboxList::make('backup') + ->bulkToggleable() + ->label('') + ->options([ + 'read' => 'Read', + 'create' => 'Create', + 'delete' => 'Delete', + 'download' => 'Download', + 'restore' => 'Restore', + ]) + ->descriptions([ + 'create' => trans('server/users.permissions.backup_create'), + 'read' => trans('server/users.permissions.backup_read'), + 'delete' => trans('server/users.permissions.backup_delete'), + 'download' => trans('server/users.permissions.backup_download'), + 'restore' => trans('server/users.permissions.backup_restore'), + ]), + ]), + ]), + Tabs\Tab::make('Allocation') + ->schema([ + Section::make() + ->description(trans('server/users.permissions.allocation_desc')) + ->icon('tabler-network') + ->schema([ + CheckboxList::make('allocation') + ->bulkToggleable() + ->label('') + ->options([ + 'read' => 'Read', + 'create' => 'Create', + 'update' => 'Update', + 'delete' => 'Delete', + ]) + ->descriptions([ + 'read' => trans('server/users.permissions.allocation_read'), + 'create' => trans('server/users.permissions.allocation_create'), + 'update' => trans('server/users.permissions.allocation_update'), + 'delete' => trans('server/users.permissions.allocation_delete'), + ]), + ]), + ]), + Tabs\Tab::make('Startup') + ->schema([ + Section::make() + ->description(trans('server/users.permissions.startup_desc')) + ->icon('tabler-question-mark') + ->schema([ + CheckboxList::make('startup') + ->bulkToggleable() + ->label('') + ->options([ + 'read' => 'Read', + 'update' => 'Update', + 'docker-image' => 'Docker Image', + ]) + ->descriptions([ + 'read' => trans('server/users.permissions.startup_read'), + 'update' => trans('server/users.permissions.startup_update'), + 'docker-image' => trans('server/users.permissions.startup_docker_image'), + ]), + ]), + ]), + Tabs\Tab::make('Database') + ->schema([ + Section::make() + ->description(trans('server/users.permissions.database_desc')) + ->icon('tabler-database') + ->schema([ + CheckboxList::make('database') + ->bulkToggleable() + ->label('') + ->options([ + 'read' => 'Read', + 'create' => 'Create', + 'update' => 'Update', + 'delete' => 'Delete', + 'view_password' => 'View Password', + ]) + ->descriptions([ + 'read' => trans('server/users.permissions.database_read'), + 'create' => trans('server/users.permissions.database_create'), + 'update' => trans('server/users.permissions.database_update'), + 'delete' => trans('server/users.permissions.database_delete'), + 'view_password' => trans('server/users.permissions.database_view_password'), + ]), + ]), + ]), + Tabs\Tab::make('Schedule') + ->schema([ + Section::make() + ->description(trans('server/users.permissions.schedule_desc')) + ->icon('tabler-clock') + ->schema([ + CheckboxList::make('schedule') + ->bulkToggleable() + ->label('') + ->options([ + 'read' => 'Read', + 'create' => 'Create', + 'update' => 'Update', + 'delete' => 'Delete', + ]) + ->descriptions([ + 'read' => trans('server/users.permissions.schedule_read'), + 'create' => trans('server/users.permissions.schedule_create'), + 'update' => trans('server/users.permissions.schedule_update'), + 'delete' => trans('server/users.permissions.schedule_delete'), + ]), + ]), + ]), + Tabs\Tab::make('Settings') + ->schema([ + Section::make() + ->description(trans('server/users.permissions.settings_desc')) + ->icon('tabler-settings') + ->schema([ + CheckboxList::make('settings') + ->bulkToggleable() + ->label('') + ->options([ + 'rename' => 'Rename', + 'reinstall' => 'Reinstall', + 'activity' => 'Activity', + ]) + ->descriptions([ + 'rename' => trans('server/users.permissions.setting_rename'), + 'reinstall' => trans('server/users.permissions.setting_reinstall'), + 'activity' => trans('server/users.permissions.setting_activity'), + ]), + ]), + ]), + ]), + ]), + ]), + ]); } public static function getPages(): array From c67a2e9c89b506a2c59a14b599e331c156296b4d Mon Sep 17 00:00:00 2001 From: notCharles Date: Mon, 5 Aug 2024 21:46:54 -0400 Subject: [PATCH 42/43] .... Sure it works. TODO: Update permissions when you save editing a sub user. --- app/Filament/App/Resources/UserResource.php | 25 ++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/app/Filament/App/Resources/UserResource.php b/app/Filament/App/Resources/UserResource.php index f12d3be623..ac76773ef5 100644 --- a/app/Filament/App/Resources/UserResource.php +++ b/app/Filament/App/Resources/UserResource.php @@ -58,10 +58,12 @@ public static function table(Table $table): Table ]) ->actions([ DeleteAction::make() - ->label('Remove User'), + ->label('Remove User') + ->requiresConfirmation(), EditAction::make() ->label('Edit User') ->hidden(!auth()->user()->can(Permission::ACTION_USER_UPDATE, Filament::getTenant())) + ->modalHeading(fn (User $user) => 'Editing ' . $user->email) ->form([ Grid::make() ->columnSpanFull() @@ -168,6 +170,27 @@ public static function table(Table $table): Table ->icon('tabler-terminal-2') ->schema([ CheckboxList::make('control') + ->formatStateUsing(function (User $user, Set $set) { + $server = Filament::getTenant(); + $permissionsArray = Subuser::query() + ->where('user_id', $user->id) + ->where('server_id', $server->id) + ->first() + ->permissions; + + $transformedPermissions = []; + + foreach ($permissionsArray as $permission) { + [$group, $action] = explode('.', $permission, 2); + $transformedPermissions[$group][] = $action; + } + + foreach ($transformedPermissions as $key => $value) { + $set($key, $value); + } + + return $transformedPermissions['control']; + }) ->bulkToggleable() ->label('') ->options([ From 7e6fc676aee6b2551ed4df85a893b9c826eb8ae7 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 6 Aug 2024 11:34:48 +0200 Subject: [PATCH 43/43] user: update canAccessPanel & canAccessTenant --- app/Models/User.php | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/app/Models/User.php b/app/Models/User.php index 12ac1d6169..57507d8800 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -316,6 +316,11 @@ public function subusers(): HasMany return $this->hasMany(Subuser::class); } + public function subServers(): BelongsToMany + { + return $this->belongsToMany(Server::class, 'subusers'); + } + protected function checkPermission(Server $server, string $permission = ''): bool { if ($this->root_admin || $server->owner_id === $this->id) { @@ -359,11 +364,6 @@ public function isLastRootAdmin(): bool return once(fn () => $rootAdmins->count() === 1 && $rootAdmins->first()->is($this)); } - public function canAccessPanel(Panel $panel): bool - { - return $this->root_admin; // TODO - } - public function getFilamentName(): string { return $this->name_first ?: $this->username; @@ -374,20 +374,27 @@ public function getFilamentAvatarUrl(): ?string return 'https://gravatar.com/avatar/' . md5(strtolower($this->email)); } - public function getTenants(Panel $panel): array|Collection + public function canAccessPanel(Panel $panel): bool { - return $this->servers; + if ($panel->getId() === 'admin') { + return $this->root_admin; + } + + return true; } - public function subServers(): BelongsToMany + public function getTenants(Panel $panel): array|Collection { - return $this->belongsToMany(Server::class, 'subusers'); + return $this->accessibleServers()->get(); } public function canAccessTenant(\Illuminate\Database\Eloquent\Model $tenant): bool { - return true; + if ($tenant instanceof Server) { + /** @var Server $tenant */ + return $this->checkPermission($tenant); + } - //return $this->servers()->whereKey($tenant)->exists(); // TODO + return false; } }