diff --git a/.env.example b/.env.example index ae71e78e6..84ff1d432 100644 --- a/.env.example +++ b/.env.example @@ -4,7 +4,7 @@ APP_KEY= APP_TIMEZONE=UTC APP_URL=http://panel.test APP_LOCALE=en -APP_ENVIRONMENT_ONLY=true +APP_INSTALLED=false LOG_CHANNEL=daily LOG_STACK=single @@ -27,11 +27,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/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index fd1b1e6f7..c9cb05955 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -33,7 +33,6 @@ body: attributes: label: Panel Version description: Version number of your Panel (latest is not a version) - placeholder: 1.4.0 validations: required: true @@ -42,7 +41,6 @@ body: attributes: label: Wings Version description: Version number of your Wings (latest is not a version) - placeholder: 1.4.2 validations: required: true @@ -68,7 +66,7 @@ body: Run the following command to collect logs on your system. Wings: `sudo wings diagnostics` - Panel: `tail -n 150 /var/www/pelican/storage/logs/laravel-$(date +%F).log | nc pelipaste.com 99` + Panel: `tail -n 150 /var/www/pelican/storage/logs/laravel-$(date +%F).log | curl -X POST -F 'c=@-' paste.pelistuff.com` placeholder: "https://pelipaste.com/a1h6z" render: bash validations: diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 43a644a99..5e2ee8847 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 @@ -41,6 +40,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 @@ -104,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 @@ -113,6 +113,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 @@ -166,13 +168,14 @@ jobs: APP_KEY: ThisIsARandomStringForTests12345 APP_TIMEZONE: UTC APP_URL: http://localhost/ - APP_ENVIRONMENT_ONLY: "true" CACHE_DRIVER: array MAIL_MAILER: array SESSION_DRIVER: array 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/Console/Commands/Environment/AppSettingsCommand.php b/app/Console/Commands/Environment/AppSettingsCommand.php index 4f2e93119..e74278ad3 100644 --- a/app/Console/Commands/Environment/AppSettingsCommand.php +++ b/app/Console/Commands/Environment/AppSettingsCommand.php @@ -2,163 +2,49 @@ namespace App\Console\Commands\Environment; +use App\Traits\EnvironmentWriterTrait; use Illuminate\Console\Command; -use Illuminate\Contracts\Console\Kernel; -use App\Traits\Commands\EnvironmentWriterTrait; use Illuminate\Support\Facades\Artisan; 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.} - {--settings-ui= : Enable or disable the settings UI.}'; + {--url= : The URL that this Panel is running on.}'; protected array $variables = []; - /** - * AppSettingsCommand constructor. - */ - public function __construct(private Kernel $console) + public function handle(): void { - parent::__construct(); - } + $path = base_path('.env'); + if (!file_exists($path)) { + $this->comment('Copying example .env file'); + copy($path . '.example', $path); + } + + if (!config('app.key')) { + $this->comment('Generating app key'); + Artisan::call('key:generate'); + } - /** - * Handle command execution. - * - * @throws \App\Exceptions\PanelException - */ - public function handle(): int - { $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 - ); - - 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'; } - $redisUsed = count(collect($this->variables)->filter(function ($item) { - return $item === 'redis'; - })) !== 0; - - if ($redisUsed) { - $this->requestRedisSettings(); - } - - $path = base_path('.env'); - if (!file_exists($path)) { - copy($path . '.example', $path); - } - + $this->comment('Writing variables to .env file'); $this->writeToEnvironment($this->variables); - if (!config('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; - } - - /** - * 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') - ); - - $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') - ); + $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 000000000..7837bea1d --- /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-user= : User used to connect to redis.} + {--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', [ + '--overwrite' => true, + ]); + } + } + + $this->writeToEnvironment($this->variables); + + $this->info($this->console->output()); + + return 0; + } +} diff --git a/app/Console/Commands/Environment/DatabaseSettingsCommand.php b/app/Console/Commands/Environment/DatabaseSettingsCommand.php index f05f9bbc4..f1218e40c 100644 --- a/app/Console/Commands/Environment/DatabaseSettingsCommand.php +++ b/app/Console/Commands/Environment/DatabaseSettingsCommand.php @@ -2,10 +2,10 @@ namespace App\Console\Commands\Environment; +use App\Traits\EnvironmentWriterTrait; use Illuminate\Console\Command; use Illuminate\Contracts\Console\Kernel; use Illuminate\Database\DatabaseManager; -use App\Traits\Commands\EnvironmentWriterTrait; class DatabaseSettingsCommand extends Command { diff --git a/app/Console/Commands/Environment/EmailSettingsCommand.php b/app/Console/Commands/Environment/EmailSettingsCommand.php index b67a5ed77..87c8186c1 100644 --- a/app/Console/Commands/Environment/EmailSettingsCommand.php +++ b/app/Console/Commands/Environment/EmailSettingsCommand.php @@ -2,8 +2,8 @@ namespace App\Console\Commands\Environment; +use App\Traits\EnvironmentWriterTrait; use Illuminate\Console\Command; -use App\Traits\Commands\EnvironmentWriterTrait; class EmailSettingsCommand extends Command { @@ -61,6 +61,8 @@ class EmailSettingsCommand extends Command $this->writeToEnvironment($this->variables); + $this->call('queue:restart'); + $this->line('Updating stored environment configuration file.'); $this->line(''); } diff --git a/app/Console/Commands/Environment/QueueSettingsCommand.php b/app/Console/Commands/Environment/QueueSettingsCommand.php new file mode 100644 index 000000000..f8b667402 --- /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-user= : User used to connect to redis.} + {--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', [ + '--overwrite' => true, + ]); + } + + $this->writeToEnvironment($this->variables); + + $this->info($this->console->output()); + + return 0; + } +} diff --git a/app/Console/Commands/Environment/QueueWorkerServiceCommand.php b/app/Console/Commands/Environment/QueueWorkerServiceCommand.php index 607f2ae9b..dbf1aa735 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 @@ class QueueWorkerServiceCommand extends Command $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/Console/Commands/Environment/SessionSettingsCommand.php b/app/Console/Commands/Environment/SessionSettingsCommand.php new file mode 100644 index 000000000..be99686f4 --- /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-user= : User used to connect to redis.} + {--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', [ + '--overwrite' => true, + ]); + } + } + + $this->writeToEnvironment($this->variables); + + $this->info($this->console->output()); + + return 0; + } +} diff --git a/app/Console/Commands/Maintenance/PruneImagesCommand.php b/app/Console/Commands/Maintenance/PruneImagesCommand.php new file mode 100644 index 000000000..23f613fa3 --- /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/Commands/Overrides/SeedCommand.php b/app/Console/Commands/Overrides/SeedCommand.php index 23c476860..c4ca828b2 100644 --- a/app/Console/Commands/Overrides/SeedCommand.php +++ b/app/Console/Commands/Overrides/SeedCommand.php @@ -2,7 +2,7 @@ namespace App\Console\Commands\Overrides; -use App\Console\RequiresDatabaseMigrations; +use App\Traits\Commands\RequiresDatabaseMigrations; use Illuminate\Database\Console\Seeds\SeedCommand as BaseSeedCommand; class SeedCommand extends BaseSeedCommand diff --git a/app/Console/Commands/Overrides/UpCommand.php b/app/Console/Commands/Overrides/UpCommand.php index 3b71ea854..e17808e24 100644 --- a/app/Console/Commands/Overrides/UpCommand.php +++ b/app/Console/Commands/Overrides/UpCommand.php @@ -2,7 +2,7 @@ namespace App\Console\Commands\Overrides; -use App\Console\RequiresDatabaseMigrations; +use App\Traits\Commands\RequiresDatabaseMigrations; use Illuminate\Foundation\Console\UpCommand as BaseUpCommand; class UpCommand extends BaseUpCommand diff --git a/app/Console/Commands/User/DeleteUserCommand.php b/app/Console/Commands/User/DeleteUserCommand.php index a6810feee..8c85510ed 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 @@ class DeleteUserCommand extends Command 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 @@ class DeleteUserCommand extends Command } 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')); } diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 1dd998646..4a9bbee17 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; @@ -9,6 +10,7 @@ use Illuminate\Foundation\Console\Kernel as ConsoleKernel; 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 { @@ -30,7 +32,11 @@ class Kernel extends ConsoleKernel // 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(); if (config('backups.prune_age')) { // Every 30 minutes, run the backup pruning command so that any abandoned backups can be deleted. diff --git a/app/Filament/Clusters/Settings.php b/app/Filament/Clusters/Settings.php deleted file mode 100644 index 0ac8254c8..000000000 --- a/app/Filament/Clusters/Settings.php +++ /dev/null @@ -1,10 +0,0 @@ -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); + + // Clear config cache + Artisan::call('config:clear'); + + // Run migrations + Artisan::call('migrate', [ + '--force' => true, + '--seed' => true, + '--database' => $variables['DB_CONNECTION'], + ]); + + if (!$this->hasCompletedMigrations()) { + throw new Exception('Migrations didn\'t run successfully. Double check your database configuration.'); + } + + // 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) { + report($exception); + + Notification::make() + ->title('Installation Failed') + ->body($exception->getMessage()) + ->danger() + ->persistent() + ->send(); + } + } +} diff --git a/app/Filament/Pages/Installer/Steps/AdminUserStep.php b/app/Filament/Pages/Installer/Steps/AdminUserStep.php new file mode 100644 index 000000000..68ebb6510 --- /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 000000000..db9b36cea --- /dev/null +++ b/app/Filament/Pages/Installer/Steps/DatabaseStep.php @@ -0,0 +1,92 @@ +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') { + 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, + ]); + + DB::connection('_panel_install_test')->getPdo(); + } catch (PDOException $exception) { + Notification::make() + ->title('Database connection failed') + ->body($exception->getMessage()) + ->danger() + ->send(); + + DB::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 000000000..d9cc5eafa --- /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 000000000..2ab176f40 --- /dev/null +++ b/app/Filament/Pages/Installer/Steps/RedisStep.php @@ -0,0 +1,67 @@ +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')), + ]) + ->afterValidation(function (Get $get) { + try { + config()->set('database.redis._panel_install_test', [ + 'host' => $get('env.REDIS_HOST'), + 'username' => $get('env.REDIS_USERNAME'), + 'password' => $get('env.REDIS_PASSWORD'), + 'port' => $get('env.REDIS_PORT'), + ]); + + Redis::connection('_panel_install_test')->command('ping'); + } catch (Exception $exception) { + Notification::make() + ->title('Redis connection failed') + ->body($exception->getMessage()) + ->danger() + ->send(); + + throw new Halt('Redis connection failed'); + } + }); + } +} diff --git a/app/Filament/Pages/Installer/Steps/RequirementsStep.php b/app/Filament/Pages/Installer/Steps/RequirementsStep.php new file mode 100644 index 000000000..483e4ca1b --- /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/Filament/Pages/Settings.php b/app/Filament/Pages/Settings.php new file mode 100644 index 000000000..08f95c84a --- /dev/null +++ b/app/Filament/Pages/Settings.php @@ -0,0 +1,572 @@ +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() + ->alphaNum() + ->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('APP_DEBUG', config('app.debug'))), + ToggleButtons::make('FILAMENT_TOP_NAVIGATION') + ->label('Navigation') + ->inline() + ->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') + ->inline() + ->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') + ->inline() + ->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() + ->inline() + ->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('Host') + ->required() + ->default(env('MAIL_HOST', config('mail.mailers.smtp.host'))), + TextInput::make('MAIL_PORT') + ->label('Port') + ->required() + ->numeric() + ->minValue(1) + ->maxValue(65535) + ->default(env('MAIL_PORT', config('mail.mailers.smtp.port'))), + TextInput::make('MAIL_USERNAME') + ->label('Username') + ->default(env('MAIL_USERNAME', config('mail.mailers.smtp.username'))), + TextInput::make('MAIL_PASSWORD') + ->label('Password') + ->password() + ->revealable() + ->default(env('MAIL_PASSWORD')), + ToggleButtons::make('MAIL_ENCRYPTION') + ->label('Encryption') + ->inline() + ->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('Domain') + ->required() + ->default(env('MAILGUN_DOMAIN', config('services.mailgun.domain'))), + TextInput::make('MAILGUN_SECRET') + ->label('Secret') + ->required() + ->default(env('MAILGUN_SECRET', config('services.mailgun.secret'))), + TextInput::make('MAILGUN_ENDPOINT') + ->label('Endpoint') + ->required() + ->default(env('MAILGUN_ENDPOINT', config('services.mailgun.endpoint'))), + ]), + ]; + } + + private function backupSettings(): array + { + return [ + ToggleButtons::make('APP_BACKUP_DRIVER') + ->label('Backup Driver') + ->columnSpanFull() + ->inline() + ->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(); + + // 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'); + 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/Filament/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php b/app/Filament/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php index c31b16c53..45f194e89 100644 --- a/app/Filament/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php +++ b/app/Filament/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php @@ -65,8 +65,7 @@ class EditDatabaseHost extends EditRecord ->helperText('The password for the database user.') ->password() ->revealable() - ->maxLength(255) - ->required(), + ->maxLength(255), Select::make('node_id') ->searchable() ->preload() diff --git a/app/Filament/Resources/EggResource/Pages/CreateEgg.php b/app/Filament/Resources/EggResource/Pages/CreateEgg.php index 5dd15695c..5a1a4e0dc 100644 --- a/app/Filament/Resources/EggResource/Pages/CreateEgg.php +++ b/app/Filament/Resources/EggResource/Pages/CreateEgg.php @@ -74,9 +74,10 @@ class CreateEgg extends CreateRecord ->helperText('') ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]), TextInput::make('update_url') - ->disabled() - ->helperText('Not implemented.') - ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]), + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('URLs must point directly to the raw .json file.') + ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]) + ->url(), KeyValue::make('docker_images') ->live() ->columnSpanFull() @@ -133,7 +134,7 @@ class CreateEgg extends CreateRecord ->mutateRelationshipDataBeforeCreateUsing(function (array $data): array { $data['default_value'] ??= ''; $data['description'] ??= ''; - $data['rules'] ??= ''; + $data['rules'] ??= []; $data['user_viewable'] ??= ''; $data['user_editable'] ??= ''; @@ -142,7 +143,7 @@ class CreateEgg extends CreateRecord ->mutateRelationshipDataBeforeSaveUsing(function (array $data): array { $data['default_value'] ??= ''; $data['description'] ??= ''; - $data['rules'] ??= ''; + $data['rules'] ??= []; $data['user_viewable'] ??= ''; $data['user_editable'] ??= ''; @@ -172,7 +173,30 @@ class CreateEgg extends CreateRecord Checkbox::make('user_viewable')->label('Viewable'), Checkbox::make('user_editable')->label('Editable'), ]), - Textarea::make('rules')->columnSpanFull(), + TagsInput::make('rules') + ->columnSpanFull() + ->placeholder('Add Rule') + ->reorderable() + ->suggestions([ + 'required', + 'nullable', + 'string', + 'integer', + 'numeric', + 'boolean', + 'alpha', + 'alpha_dash', + 'alpha_num', + 'url', + 'email', + 'regex:', + 'min:', + 'max:', + 'between:', + 'between:1024,65535', + 'in:', + 'in:true,false', + ]), ]), ]), Tab::make('Install Script') @@ -186,7 +210,7 @@ class CreateEgg extends CreateRecord TextInput::make('script_container') ->required() ->maxLength(255) - ->default('alpine:3.4'), + ->default('ghcr.io/pelican-eggs/installers:debian'), Select::make('script_entry') ->selectablePlaceholder(false) diff --git a/app/Filament/Resources/EggResource/Pages/EditEgg.php b/app/Filament/Resources/EggResource/Pages/EditEgg.php index f4c906c3b..a1b0e61f8 100644 --- a/app/Filament/Resources/EggResource/Pages/EditEgg.php +++ b/app/Filament/Resources/EggResource/Pages/EditEgg.php @@ -91,8 +91,10 @@ class EditEgg extends EditRecord ->helperText('') ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]), TextInput::make('update_url') - ->disabled() - ->helperText('Not implemented.') + ->label('Update URL') + ->url() + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('URLs must point directly to the raw .json file.') ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]), KeyValue::make('docker_images') ->live() @@ -142,7 +144,7 @@ class EditEgg extends EditRecord ->mutateRelationshipDataBeforeCreateUsing(function (array $data): array { $data['default_value'] ??= ''; $data['description'] ??= ''; - $data['rules'] ??= ''; + $data['rules'] ??= []; $data['user_viewable'] ??= ''; $data['user_editable'] ??= ''; @@ -151,7 +153,7 @@ class EditEgg extends EditRecord ->mutateRelationshipDataBeforeSaveUsing(function (array $data): array { $data['default_value'] ??= ''; $data['description'] ??= ''; - $data['rules'] ??= ''; + $data['rules'] ??= []; $data['user_viewable'] ??= ''; $data['user_editable'] ??= ''; @@ -181,7 +183,30 @@ class EditEgg extends EditRecord Checkbox::make('user_viewable')->label('Viewable'), Checkbox::make('user_editable')->label('Editable'), ]), - TextInput::make('rules')->columnSpanFull(), + TagsInput::make('rules') + ->columnSpanFull() + ->placeholder('Add Rule') + ->reorderable() + ->suggestions([ + 'required', + 'nullable', + 'string', + 'integer', + 'numeric', + 'boolean', + 'alpha', + 'alpha_dash', + 'alpha_num', + 'url', + 'email', + 'regex:', + 'min:', + 'max:', + 'between:', + 'between:1024,65535', + 'in:', + 'in:true,false', + ]), ]), ]), Tab::make('Install Script') @@ -249,6 +274,7 @@ class EditEgg extends EditRecord ->schema([ TextInput::make('url') ->label('URL') + ->default(fn (Egg $egg): ?string => $egg->update_url) ->hint('Link to the egg file (eg. minecraft.json)') ->url(), ]), @@ -267,16 +293,14 @@ class EditEgg extends EditRecord Notification::make() ->title('Import Failed') ->body($exception->getMessage()) - ->danger() + ->danger() // Will Robinson ->send(); report($exception); return; } - } - - if (!empty($data['url'])) { + } elseif (!empty($data['url'])) { try { $eggImportService->fromUrl($data['url'], $egg); } catch (Exception $exception) { diff --git a/app/Filament/Resources/NodeResource/Pages/EditNode.php b/app/Filament/Resources/NodeResource/Pages/EditNode.php index 26e61771a..ac06837d8 100644 --- a/app/Filament/Resources/NodeResource/Pages/EditNode.php +++ b/app/Filament/Resources/NodeResource/Pages/EditNode.php @@ -3,13 +3,12 @@ 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\Models\Objects\Endpoint; 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; @@ -18,6 +17,7 @@ use Filament\Forms\Components\TagsInput; 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; @@ -42,6 +42,32 @@ class EditNode extends EditRecord ->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'] ?? 'Unknown'), + Placeholder::make('') + ->label('CPU Threads') + ->content(fn (Node $node) => $node->systemInformation()['cpu_count'] ?? 0), + Placeholder::make('') + ->label('Architecture') + ->content(fn (Node $node) => $node->systemInformation()['architecture'] ?? 'Unknown'), + Placeholder::make('') + ->label('Kernel') + ->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), + // TODO: Make purdy View::make('filament.components.node-storage-chart')->columnSpan(3), + ]), Tab::make('Basic Settings') ->icon('tabler-server') ->schema([ @@ -438,16 +464,17 @@ class EditNode extends EditRecord ]; } - protected function getFooterWidgets(): array - { - return [ - NodeStorageChart::class, - NodeMemoryChart::class, - ]; - } - protected function afterSave(): void { $this->fillForm(); } + + protected function getColumnSpan() + { + 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 000000000..45d18fc90 --- /dev/null +++ b/app/Filament/Resources/NodeResource/Widgets/NodeCpuChart.php @@ -0,0 +1,81 @@ +record; + $threads = $node->systemInformation()['cpu_count'] ?? 0; + + $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'] ?? 0; + + $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 8ed87046a..3d8762447 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 bcfbfcf4f..b841d84ef 100644 --- a/app/Filament/Resources/NodeResource/Widgets/NodeStorageChart.php +++ b/app/Filament/Resources/NodeResource/Widgets/NodeStorageChart.php @@ -9,8 +9,8 @@ use Illuminate\Database\Eloquent\Model; 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 @@ class NodeStorageChart extends ChartWidget return [ 'datasets' => [ [ - 'label' => 'Data Cool', 'data' => [$used, $unused], 'backgroundColor' => [ 'rgb(255, 99, 132)', @@ -55,7 +54,6 @@ class NodeStorageChart extends ChartWidget 'rgb(255, 205, 86)', ], ], - // 'backgroundColor' => [], ], 'labels' => ['Used', 'Unused'], ]; diff --git a/app/Filament/Resources/ServerResource/Pages/CreateServer.php b/app/Filament/Resources/ServerResource/Pages/CreateServer.php index 2893a9cae..5f664959e 100644 --- a/app/Filament/Resources/ServerResource/Pages/CreateServer.php +++ b/app/Filament/Resources/ServerResource/Pages/CreateServer.php @@ -41,8 +41,8 @@ class CreateServer extends CreateRecord ->icon('tabler-info-circle') ->completedIcon('tabler-check') ->columns([ - 'default' => 2, - 'sm' => 2, + 'default' => 1, + 'sm' => 1, 'md' => 4, 'lg' => 6, ]) @@ -62,7 +62,7 @@ class CreateServer extends CreateRecord })) ->columnSpan([ 'default' => 2, - 'sm' => 4, + 'sm' => 3, 'md' => 2, 'lg' => 3, ]) @@ -76,12 +76,12 @@ class CreateServer extends CreateRecord ->label('Owner') ->columnSpan([ 'default' => 2, - 'sm' => 4, - 'md' => 2, + 'sm' => 3, + 'md' => 3, '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') @@ -126,10 +126,10 @@ class CreateServer extends CreateRecord ->prefixIcon('tabler-server-2') ->default(fn () => ($this->node = Node::query()->latest()->first())?->id) ->columnSpan([ - 'default' => 1, - 'sm' => 2, - 'md' => 2, - 'lg' => 3, + 'default' => 2, + 'sm' => 3, + 'md' => 6, + 'lg' => 6, ]) ->live() ->relationship('node', 'name') @@ -140,13 +140,14 @@ class CreateServer extends CreateRecord }) ->required(), - Forms\Components\TextInput::make('description') - ->placeholder('Notes') + Forms\Components\Textarea::make('description') + ->placeholder('Description') + ->rows(3) ->columnSpan([ - 'default' => 1, - 'sm' => 2, - 'md' => 2, - 'lg' => 3, + 'default' => 2, + 'sm' => 6, + 'md' => 6, + 'lg' => 6, ]) ->label('Notes'), ]), @@ -157,9 +158,9 @@ class CreateServer extends CreateRecord ->completedIcon('tabler-check') ->columns([ 'default' => 1, - 'sm' => 2, - 'md' => 2, - 'lg' => 4, + 'sm' => 4, + 'md' => 4, + 'lg' => 6, ]) ->schema([ @@ -170,7 +171,7 @@ class CreateServer extends CreateRecord 'default' => 2, 'sm' => 2, 'md' => 2, - 'lg' => 3, + 'lg' => 4, ]) ->relationship('egg', 'name') ->searchable() @@ -214,6 +215,30 @@ class CreateServer extends CreateRecord ->inline() ->required(), + Forms\Components\ToggleButtons::make('start_on_completion') + ->label('Start Server After Install?') + ->default(true) + ->required() + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 1, + ]) + ->options([ + true => 'Yes', + false => 'No', + ]) + ->colors([ + true => 'primary', + false => 'danger', + ]) + ->icons([ + true => 'tabler-code', + false => 'tabler-code-off', + ]) + ->inline(), + Forms\Components\Textarea::make('startup') ->hidden(fn () => !$this->egg) ->hintIcon('tabler-code') @@ -233,12 +258,16 @@ class CreateServer extends CreateRecord fn (int $carry, $line) => $carry + floor(strlen($line) / 125), 0 ); - }), + }) + ->columnSpan([ + 'default' => 1, + 'sm' => 4, + 'md' => 4, + 'lg' => 6, + ]), Forms\Components\Hidden::make('environment')->default([]), - Forms\Components\Hidden::make('start_on_completion')->default(true), - Forms\Components\Section::make('Variables') ->icon('tabler-eggs') ->iconColor('primary') @@ -269,8 +298,7 @@ class CreateServer extends CreateRecord $text = Forms\Components\TextInput::make('variable_value') ->hidden($this->shouldHideComponent(...)) - ->maxLength(255) - ->required(fn (Forms\Get $get) => in_array('required', explode('|', $get('rules')))) + ->required(fn (Forms\Get $get) => in_array('required', $get('rules'))) ->rules( fn (Forms\Get $get): Closure => function (string $attribute, $value, Closure $fail) use ($get) { $validator = Validator::make(['validatorkey' => $value], [ @@ -297,7 +325,7 @@ class CreateServer extends CreateRecord ->live(onBlur: true) ->hintIcon('tabler-code') ->label(fn (Forms\Get $get) => $get('name')) - ->hintIconTooltip(fn (Forms\Get $get) => $get('rules')) + ->hintIconTooltip(fn (Forms\Get $get) => implode('|', $get('rules'))) ->prefix(fn (Forms\Get $get) => '{{' . $get('env_variable') . '}}') ->helperText(fn (Forms\Get $get) => empty($get('description')) ? '—' : $get('description')) ->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state) { @@ -382,12 +410,7 @@ class CreateServer extends CreateRecord ->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, @@ -567,12 +590,7 @@ class CreateServer extends CreateRecord Forms\Components\Fieldset::make('Feature Limits') ->inlineLabel() - ->columnSpan([ - 'default' => 2, - 'sm' => 4, - 'md' => 4, - 'lg' => 6, - ]) + ->columnSpan(6) ->columns([ 'default' => 1, 'sm' => 2, @@ -603,18 +621,13 @@ class CreateServer extends CreateRecord ->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') @@ -633,7 +646,12 @@ class CreateServer extends CreateRecord 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') @@ -649,13 +667,18 @@ class CreateServer extends CreateRecord } }) ->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() @@ -710,7 +733,7 @@ class CreateServer extends CreateRecord private function shouldHideComponent(Forms\Get $get, Forms\Components\Component $component): bool { - $containsRuleIn = str($get('rules'))->explode('|')->reduce( + $containsRuleIn = collect($get('rules'))->reduce( fn ($result, $value) => $result === true && !str($value)->startsWith('in:'), true ); @@ -727,7 +750,7 @@ class CreateServer extends CreateRecord private function getSelectOptionsFromRules(Forms\Get $get): array { - $inRule = str($get('rules'))->explode('|')->reduce( + $inRule = collect($get('rules'))->reduce( fn ($result, $value) => str($value)->startsWith('in:') ? $value : $result, '' ); diff --git a/app/Filament/Resources/ServerResource/Pages/EditServer.php b/app/Filament/Resources/ServerResource/Pages/EditServer.php index d8cdea3df..a20287383 100644 --- a/app/Filament/Resources/ServerResource/Pages/EditServer.php +++ b/app/Filament/Resources/ServerResource/Pages/EditServer.php @@ -12,6 +12,8 @@ use App\Services\Databases\DatabaseManagementService; 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; @@ -46,22 +48,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') @@ -133,7 +129,8 @@ class EditServer extends EditRecord 'md' => 2, 'lg' => 3, ]) - ->readOnly(), + ->readOnly() + ->dehydrated(false), Forms\Components\TextInput::make('uuid_short') ->label('Short UUID') ->hintAction(CopyAction::make()) @@ -143,7 +140,8 @@ class EditServer extends EditRecord 'md' => 2, 'lg' => 3, ]) - ->readOnly(), + ->readOnly() + ->dehydrated(false), Forms\Components\TextInput::make('external_id') ->label('External ID') ->columnSpan([ @@ -169,12 +167,6 @@ class EditServer extends EditRecord ->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, @@ -350,12 +342,6 @@ class EditServer extends EditRecord Forms\Components\Fieldset::make('Feature Limits') ->inlineLabel() - ->columnSpan([ - 'default' => 2, - 'sm' => 4, - 'md' => 4, - 'lg' => 6, - ]) ->columns([ 'default' => 1, 'sm' => 2, @@ -380,12 +366,6 @@ class EditServer extends EditRecord ->numeric(), ]), Forms\Components\Fieldset::make('Docker Settings') - ->columnSpan([ - 'default' => 2, - 'sm' => 4, - 'md' => 4, - 'lg' => 6, - ]) ->columns([ 'default' => 1, 'sm' => 2, @@ -448,10 +428,10 @@ class EditServer extends EditRecord ->disabledOn('edit') ->prefixIcon('tabler-egg') ->columnSpan([ - 'default' => 1, + 'default' => 6, 'sm' => 3, 'md' => 3, - 'lg' => 5, + 'lg' => 4, ]) ->relationship('egg', 'name') ->searchable() @@ -460,6 +440,12 @@ class EditServer extends EditRecord 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', @@ -537,29 +523,40 @@ class EditServer extends EditRecord Forms\Components\Textarea::make('startup') ->label('Startup Command') ->required() - ->hintAction(Forms\Components\Actions\Action::make('startup-restore') - ->label('Restore Default') - ->icon('tabler-restore') - ->action(fn (Forms\Get $get, Forms\Set $set) => $set('startup', Egg::find($get('egg_id'))?->startup ?? '') - ) - ) - ->columnSpan([ - 'default' => 2, - 'sm' => 4, - 'md' => 4, - 'lg' => 6, - ]) - ->rows(fn ($state) => str($state)->explode("\n")->reduce( - fn (int $carry, $line) => $carry + floor(strlen($line) / 125), 0 - )), + ->columnSpan(6) + ->rows(function ($state) { + return str($state)->explode("\n")->reduce( + fn (int $carry, $line) => $carry + floor(strlen($line) / 125), + 0 + ); + }), - Forms\Components\Hidden::make('environment')->default([]), + Forms\Components\Textarea::make('defaultStartup') + ->hintAction(CopyAction::make()) + ->label('Default Startup Command') + ->disabled() + ->formatStateUsing(function ($state, Get $get, Set $set) { + $egg = Egg::query()->find($get('egg_id')); + + return $egg->startup; + }) + ->columnSpan(6), Forms\Components\Repeater::make('server_variables') ->relationship('serverVariables', function (Builder $query) { - $query->whereHas('variable', function (Builder $query) { - $query->whereNot('rules', 'like', '%port%'); - }); + /** @var Server $server */ + $server = $this->getRecord(); + + foreach ($server->variables as $variable) { + ServerVariable::query()->firstOrCreate([ + 'server_id' => $server->id, + 'variable_id' => $variable->id, + ], [ + 'variable_value' => $variable->server_value ?? '', + ]); + } + + return $query; }) ->grid() ->mutateRelationshipDataBeforeSaveUsing(function (array &$data): array { @@ -576,7 +573,7 @@ class EditServer extends EditRecord $text = Forms\Components\TextInput::make('variable_value') ->hidden($this->shouldHideComponent(...)) - ->required(fn (ServerVariable $serverVariable) => in_array('required', explode('|', $serverVariable->variable->rules))) + ->required(fn (ServerVariable $serverVariable) => $serverVariable->variable->getRequiredAttribute()) ->rules([ fn (ServerVariable $serverVariable): Closure => function (string $attribute, $value, Closure $fail) use ($serverVariable) { $validator = Validator::make(['validatorkey' => $value], [ @@ -603,7 +600,7 @@ class EditServer extends EditRecord ->live(onBlur: true) ->hintIcon('tabler-code') ->label(fn (ServerVariable $serverVariable) => $serverVariable->variable->name) - ->hintIconTooltip(fn (ServerVariable $serverVariable) => $serverVariable->variable->rules) + ->hintIconTooltip(fn (ServerVariable $serverVariable) => implode('|', $serverVariable->variable->rules)) ->prefix(fn (ServerVariable $serverVariable) => '{{' . $serverVariable->variable->env_variable . '}}') ->helperText(fn (ServerVariable $serverVariable) => empty($serverVariable->variable?->description) ? '—' : $serverVariable->variable->description); } @@ -810,8 +807,7 @@ class EditServer extends EditRecord Actions\DeleteAction::make('Delete') ->successRedirectUrl(route('filament.admin.resources.servers.index')) ->color('danger') - ->disabled(fn (Server $server) => $server->databases()->count() > 0) - ->label(fn (Server $server) => $server->databases()->count() > 0 ? 'Server has a Database' : 'Delete') + ->label('Delete') ->after(fn (Server $server) => resolve(ServerDeletionService::class)->handle($server)) ->requiresConfirmation(), Actions\Action::make('console') @@ -842,20 +838,18 @@ class EditServer extends EditRecord { $rules = str($serverVariable->variable->rules)->explode('|'); - $containsRuleIn = $rules->reduce( - fn ($result, $value) => $result === true && !str($value)->startsWith('in:'), true - ); + $containsRuleIn = array_first($serverVariable->variable->rules, fn ($value) => str($value)->startsWith('in:'), false); if ($rules->contains('port')) { return true; } if ($component instanceof Forms\Components\Select) { - return $containsRuleIn; + return !$containsRuleIn; } if ($component instanceof Forms\Components\TextInput) { - return !$containsRuleIn; + return $containsRuleIn; } throw new \Exception('Component type not supported: ' . $component::class); @@ -863,10 +857,8 @@ class EditServer extends EditRecord private function getSelectOptionsFromRules(ServerVariable $serverVariable): array { - $rules = str($serverVariable->variable->rules)->explode('|'); - $inRule = $rules->reduce( - fn ($result, $value) => str($value)->startsWith('in:') ? $value : $result, '' - ); + + $inRule = array_first($serverVariable->variable->rules, fn ($value) => str($value)->startsWith('in:')); return str($inRule) ->after('in:') diff --git a/app/Filament/Resources/UserResource/Pages/EditProfile.php b/app/Filament/Resources/UserResource/Pages/EditProfile.php index 95c97a736..668bd77f6 100644 --- a/app/Filament/Resources/UserResource/Pages/EditProfile.php +++ b/app/Filament/Resources/UserResource/Pages/EditProfile.php @@ -53,6 +53,7 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile ->label(trans('strings.username')) ->disabled() ->readOnly() + ->dehydrated(false) ->maxLength(255) ->unique(ignoreRecord: true) ->autofocus(), @@ -119,6 +120,7 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile ->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:'), @@ -215,7 +217,7 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile Action::make('Create') ->disabled(fn (Get $get) => $get('description') === null) ->successRedirectUrl(route('filament.admin.auth.profile', ['tab' => '-api-keys-tab'])) - ->action(function (Get $get, Action $action, $user) { + ->action(function (Get $get, Action $action, User $user) { $token = $user->createToken( $get('description'), $get('allowed_ips'), diff --git a/app/Http/Controllers/Admin/Settings/AdvancedController.php b/app/Http/Controllers/Admin/Settings/AdvancedController.php deleted file mode 100644 index def9124a8..000000000 --- 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 47c567458..000000000 --- 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 33aa5c31f..000000000 --- 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/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php b/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php index c7166e9df..913204b83 100644 --- a/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php +++ b/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php @@ -20,6 +20,7 @@ class StoreNodeRequest extends ApplicationApiRequest return collect($rules ?? Node::getRules())->only([ 'public', 'name', + 'description', 'fqdn', 'scheme', 'behind_proxy', diff --git a/app/Http/Requests/Api/Client/Servers/Schedules/StoreTaskRequest.php b/app/Http/Requests/Api/Client/Servers/Schedules/StoreTaskRequest.php index a510c1b5f..190d3e54f 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 @@ class StoreTaskRequest extends ViewScheduleRequest 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/NodeStatistics.php b/app/Jobs/NodeStatistics.php new file mode 100644 index 000000000..19fae9b9d --- /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/app/Jobs/Schedule/RunTaskJob.php b/app/Jobs/Schedule/RunTaskJob.php index 0d2b255a6..6a660e5c0 100644 --- a/app/Jobs/Schedule/RunTaskJob.php +++ b/app/Jobs/Schedule/RunTaskJob.php @@ -12,6 +12,7 @@ use Illuminate\Foundation\Bus\DispatchesJobs; 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 @@ class RunTaskJob extends Job implements ShouldQueue */ 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 @@ class RunTaskJob extends Job implements ShouldQueue 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/Listeners/Auth/AuthenticationListener.php b/app/Listeners/Auth/AuthenticationListener.php index 01a8e35c0..b06428bd2 100644 --- a/app/Listeners/Auth/AuthenticationListener.php +++ b/app/Listeners/Auth/AuthenticationListener.php @@ -5,7 +5,6 @@ namespace App\Listeners\Auth; use App\Facades\Activity; use Illuminate\Auth\Events\Failed; use App\Events\Auth\DirectLogin; -use Illuminate\Events\Dispatcher; class AuthenticationListener { @@ -28,10 +27,4 @@ class AuthenticationListener $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/Models/ActivityLog.php b/app/Models/ActivityLog.php index 06fd2d108..bb6149c14 100644 --- a/app/Models/ActivityLog.php +++ b/app/Models/ActivityLog.php @@ -140,6 +140,10 @@ class ActivityLog extends Model { parent::boot(); + static::creating(function (self $model) { + $model->timestamp = Carbon::now(); + }); + static::created(function (self $model) { Event::dispatch(new ActivityLogged($model)); }); diff --git a/app/Models/Egg.php b/app/Models/Egg.php index 858e7425d..721324c87 100644 --- a/app/Models/Egg.php +++ b/app/Models/Egg.php @@ -17,7 +17,7 @@ use Illuminate\Support\Str; * @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 @@ -95,6 +95,7 @@ class Egg extends Model 'config_stop', 'config_from', 'startup', + 'update_url', 'script_is_privileged', 'script_install', 'script_entry', diff --git a/app/Models/EggVariable.php b/app/Models/EggVariable.php index ba9f9a40a..0b9dd9e32 100644 --- a/app/Models/EggVariable.php +++ b/app/Models/EggVariable.php @@ -15,7 +15,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; * @property string $default_value * @property bool $user_viewable * @property bool $user_editable - * @property string $rules + * @property array $rules * @property \Carbon\CarbonImmutable $created_at * @property \Carbon\CarbonImmutable $updated_at * @property bool $required @@ -58,12 +58,14 @@ class EggVariable extends Model 'default_value' => 'string', 'user_viewable' => 'boolean', 'user_editable' => 'boolean', - 'rules' => 'string', + 'rules' => 'array', + 'rules.*' => 'string', ]; protected $attributes = [ 'user_editable' => 0, 'user_viewable' => 0, + 'rules' => '[]', ]; protected function casts(): array @@ -72,6 +74,7 @@ class EggVariable extends Model 'egg_id' => 'integer', 'user_viewable' => 'bool', 'user_editable' => 'bool', + 'rules' => 'array', 'created_at' => 'immutable_datetime', 'updated_at' => 'immutable_datetime', ]; @@ -79,7 +82,7 @@ class EggVariable extends Model public function getRequiredAttribute(): bool { - return in_array('required', explode('|', $this->rules)); + return in_array('required', $this->rules); } public function egg(): HasOne diff --git a/app/Models/Node.php b/app/Models/Node.php index 54014dfc8..377fc07ac 100644 --- a/app/Models/Node.php +++ b/app/Models/Node.php @@ -237,21 +237,21 @@ class Node extends Model */ public function isViable(int $memory, int $disk, int $cpu): bool { - if ($this->memory_overallocate >= 0) { + if ($this->memory > 0 && $this->memory_overallocate >= 0) { $memoryLimit = $this->memory * (1 + ($this->memory_overallocate / 100)); if ($this->servers_sum_memory + $memory > $memoryLimit) { return false; } } - if ($this->disk_overallocate >= 0) { + if ($this->disk > 0 && $this->disk_overallocate >= 0) { $diskLimit = $this->disk * (1 + ($this->disk_overallocate / 100)); if ($this->servers_sum_disk + $disk > $diskLimit) { return false; } } - if ($this->cpu_overallocate >= 0) { + if ($this->cpu > 0 && $this->cpu_overallocate >= 0) { $cpuLimit = $this->cpu * (1 + ($this->cpu_overallocate / 100)); if ($this->servers_sum_cpu + $cpu > $cpuLimit) { return false; diff --git a/app/Models/Setting.php b/app/Models/Setting.php index d25bd1b5d..9efad2b08 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/Models/Task.php b/app/Models/Task.php index 6545ab5f5..254d38ece 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/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index fe734f1a3..4b4e53f74 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -11,6 +11,8 @@ use App\Services\Helpers\SoftwareVersionService; 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; @@ -83,6 +85,15 @@ class AppServiceProvider extends ServiceProvider 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, + ]); } /** @@ -90,11 +101,6 @@ class AppServiceProvider extends ServiceProvider */ 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/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 88b8a3d01..e8b9a9a0d 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -10,7 +10,6 @@ use App\Observers\UserObserver; 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/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 60339c744..8c8f6355e 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -9,7 +9,6 @@ use Filament\Http\Middleware\DisableBladeIconComponents; 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; @@ -37,21 +36,13 @@ class AdminPanelProvider extends PanelProvider ->path('admin') ->topNavigation(config('panel.filament.top-navigation', true)) ->login() + ->breadcrumbs(false) ->homeUrl('/') ->favicon(config('app.favicon', '/pelican.ico')) ->brandName(config('app.name', 'Pelican')) ->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/Providers/SettingsServiceProvider.php b/app/Providers/SettingsServiceProvider.php deleted file mode 100644 index 5c7412455..000000000 --- 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/app/Services/Eggs/Sharing/EggImporterService.php b/app/Services/Eggs/Sharing/EggImporterService.php index 1e85923d7..dc54525a6 100644 --- a/app/Services/Eggs/Sharing/EggImporterService.php +++ b/app/Services/Eggs/Sharing/EggImporterService.php @@ -54,6 +54,8 @@ class EggImporterService // Update existing variables or create new ones. foreach ($parsed['variables'] ?? [] as $variable) { EggVariable::unguarded(function () use ($egg, $variable) { + $variable['rules'] = is_array($variable['rules']) ? $variable['rules'] : explode('|', $variable['rules']); + $egg->variables()->updateOrCreate([ 'env_variable' => $variable['env_variable'], ], Collection::make($variable)->except(['egg_id', 'env_variable'])->toArray()); diff --git a/app/Services/Eggs/Variables/VariableCreationService.php b/app/Services/Eggs/Variables/VariableCreationService.php index d2213f5be..c6c726986 100644 --- a/app/Services/Eggs/Variables/VariableCreationService.php +++ b/app/Services/Eggs/Variables/VariableCreationService.php @@ -40,7 +40,7 @@ class VariableCreationService throw new ReservedVariableNameException(sprintf('Cannot use the protected name %s for this environment variable.', array_get($data, 'env_variable'))); } - if (!empty($data['rules'] ?? '')) { + if (!empty($data['rules'] ?? [])) { $this->validateRules($data['rules']); } @@ -55,7 +55,7 @@ class VariableCreationService 'default_value' => $data['default_value'] ?? '', 'user_viewable' => in_array('user_viewable', $options), 'user_editable' => in_array('user_editable', $options), - 'rules' => $data['rules'] ?? '', + 'rules' => $data['rules'] ?? [], ]); return $eggVariable; diff --git a/app/Services/Eggs/Variables/VariableUpdateService.php b/app/Services/Eggs/Variables/VariableUpdateService.php index 515cbc271..e56ae8ea7 100644 --- a/app/Services/Eggs/Variables/VariableUpdateService.php +++ b/app/Services/Eggs/Variables/VariableUpdateService.php @@ -2,7 +2,6 @@ namespace App\Services\Eggs\Variables; -use Illuminate\Support\Str; use App\Models\EggVariable; use App\Exceptions\DisplayException; use App\Traits\Services\ValidatesValidationRules; @@ -54,12 +53,8 @@ class VariableUpdateService } } - if (!empty($data['rules'] ?? '')) { - $this->validateRules( - (is_string($data['rules']) && Str::contains($data['rules'], ';;')) - ? explode(';;', $data['rules']) - : $data['rules'] - ); + if (!empty($data['rules'] ?? [])) { + $this->validateRules($data['rules']); } $options = array_get($data, 'options') ?? []; @@ -71,7 +66,7 @@ class VariableUpdateService 'default_value' => $data['default_value'] ?? '', 'user_viewable' => in_array('user_viewable', $options), 'user_editable' => in_array('user_editable', $options), - 'rules' => $data['rules'] ?? '', + 'rules' => $data['rules'] ?? [], ]); } } diff --git a/app/Services/Files/DeleteFilesService.php b/app/Services/Files/DeleteFilesService.php new file mode 100644 index 000000000..2b1be5fda --- /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/app/Services/Helpers/SoftwareVersionService.php b/app/Services/Helpers/SoftwareVersionService.php index c66d85904..7d50203bf 100644 --- a/app/Services/Helpers/SoftwareVersionService.php +++ b/app/Services/Helpers/SoftwareVersionService.php @@ -4,7 +4,7 @@ namespace App\Services\Helpers; 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; @@ -89,18 +89,28 @@ class SoftwareVersionService $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'); } - } catch (ClientException $e) { + } catch (GuzzleException $e) { } $versionData['discord'] = 'https://pelican.dev/discord'; diff --git a/app/Services/Servers/ServerConfigurationStructureService.php b/app/Services/Servers/ServerConfigurationStructureService.php index a8e761847..b004263f6 100644 --- a/app/Services/Servers/ServerConfigurationStructureService.php +++ b/app/Services/Servers/ServerConfigurationStructureService.php @@ -51,12 +51,12 @@ class ServerConfigurationStructureService 'invocation' => $server->startup, 'skip_egg_scripts' => $server->skip_scripts, 'build' => [ - 'memory_limit' => config('panel.use_binary_prefix') ? $server->memory : $server->memory / 1.048576, - 'swap' => config('panel.use_binary_prefix') ? $server->swap : $server->swap / 1.048576, + 'memory_limit' => (int) round(config('panel.use_binary_prefix') ? $server->memory : $server->memory / 1.048576), + 'swap' => (int) round(config('panel.use_binary_prefix') ? $server->swap : $server->swap / 1.048576), 'io_weight' => $server->io, 'cpu_limit' => $server->cpu, 'threads' => $server->threads, - 'disk_space' => config('panel.use_binary_prefix') ? $server->disk : $server->disk / 1.048576, + 'disk_space' => (int) round(config('panel.use_binary_prefix') ? $server->disk : $server->disk / 1.048576), 'oom_killer' => $server->oom_killer, ], 'container' => [ diff --git a/app/Traits/CheckMigrationsTrait.php b/app/Traits/CheckMigrationsTrait.php new file mode 100644 index 000000000..ee2de8f3a --- /dev/null +++ b/app/Traits/CheckMigrationsTrait.php @@ -0,0 +1,29 @@ +make('migrator'); + + $files = $migrator->getMigrationFiles(database_path('migrations')); + + if (!$migrator->repositoryExists()) { + return false; + } + + if (array_diff(array_keys($files), $migrator->getRepository()->getRan())) { + return false; + } + + return true; + } +} diff --git a/app/Traits/Commands/RequestRedisSettingsTrait.php b/app/Traits/Commands/RequestRedisSettingsTrait.php new file mode 100644 index 000000000..07ef229c2 --- /dev/null +++ b/app/Traits/Commands/RequestRedisSettingsTrait.php @@ -0,0 +1,52 @@ +output->note(__('commands.appsettings.redis.note')); + $this->variables['REDIS_HOST'] = $this->option('redis-host') ?? $this->ask( + 'Redis Host', + config('database.redis.default.host') + ); + + $askForRedisUser = true; + $askForRedisPassword = true; + + if (!empty(config('database.redis.default.user'))) { + $this->variables['REDIS_USERNAME'] = config('database.redis.default.user'); + $askForRedisUser = $this->confirm(__('commands.appsettings.redis.confirm', ['field' => 'user'])); + } + if (!empty(config('database.redis.default.password'))) { + $this->variables['REDIS_PASSWORD'] = config('database.redis.default.password'); + $askForRedisPassword = $this->confirm(__('commands.appsettings.redis.confirm', ['field' => 'password'])); + } + + if ($askForRedisUser) { + $this->output->comment(__('commands.appsettings.redis.comment')); + $this->variables['REDIS_USERNAME'] = $this->option('redis-user') ?? $this->output->askHidden( + 'Redis User' + ); + } + 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_USERNAME'])) { + $this->variables['REDIS_USERNAME'] = 'null'; + } + 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/Console/RequiresDatabaseMigrations.php b/app/Traits/Commands/RequiresDatabaseMigrations.php similarity index 63% rename from app/Console/RequiresDatabaseMigrations.php rename to app/Traits/Commands/RequiresDatabaseMigrations.php index c6b71f1bd..67551a26a 100644 --- a/app/Console/RequiresDatabaseMigrations.php +++ b/app/Traits/Commands/RequiresDatabaseMigrations.php @@ -1,32 +1,16 @@ getLaravel()->make('migrator'); - - $files = $migrator->getMigrationFiles(database_path('migrations')); - - if (!$migrator->repositoryExists()) { - return false; - } - - if (array_diff(array_keys($files), $migrator->getRepository()->getRan())) { - return false; - } - - return true; - } + use CheckMigrationsTrait; /** * Throw a massive error into the console to hopefully catch the users attention and get diff --git a/app/Traits/Commands/EnvironmentWriterTrait.php b/app/Traits/EnvironmentWriterTrait.php similarity index 88% rename from app/Traits/Commands/EnvironmentWriterTrait.php rename to app/Traits/EnvironmentWriterTrait.php index eb40f8c8b..cd7fbdfe0 100644 --- a/app/Traits/Commands/EnvironmentWriterTrait.php +++ b/app/Traits/EnvironmentWriterTrait.php @@ -1,8 +1,8 @@ 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/app/Transformers/Api/Client/EggVariableTransformer.php b/app/Transformers/Api/Client/EggVariableTransformer.php index bdc2fad15..141f43b61 100644 --- a/app/Transformers/Api/Client/EggVariableTransformer.php +++ b/app/Transformers/Api/Client/EggVariableTransformer.php @@ -27,7 +27,7 @@ class EggVariableTransformer extends BaseClientTransformer 'default_value' => $variable->default_value, 'server_value' => $variable->server_value, 'is_editable' => $variable->user_editable, - 'rules' => $variable->rules, + 'rules' => implode('|', $variable->rules), ]; } } diff --git a/app/helpers.php b/app/helpers.php index c2aa5cd74..4f3e87021 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -40,3 +40,11 @@ if (!function_exists('object_get_strict')) { 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/bootstrap/providers.php b/bootstrap/providers.php index 8c37bb7b5..a20d8785c 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -8,6 +8,5 @@ return [ App\Providers\Filament\AdminPanelProvider::class, App\Providers\RouteServiceProvider::class, App\Providers\ViewComposerServiceProvider::class, - SocialiteProviders\Manager\ServiceProvider::class, ]; diff --git a/config/app.php b/config/app.php index e8d4deb30..1a89ba65f 100644 --- a/config/app.php +++ b/config/app.php @@ -5,6 +5,7 @@ use Illuminate\Support\Facades\Facade; 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 a8b73ad2e..e2d6eb416 100644 --- a/config/panel.php +++ b/config/panel.php @@ -1,18 +1,6 @@ (bool) env('APP_ENVIRONMENT_ONLY', false), - /* |-------------------------------------------------------------------------- | Authentication diff --git a/contributing.md b/contributing.md new file mode 100644 index 000000000..5c9ee66c8 --- /dev/null +++ b/contributing.md @@ -0,0 +1,54 @@ +# Contributing + +Welcome to the Pelican project! We are excited to have you contribute to our open-source project. This guide will help you get started with setting up your development environment, understanding our coding standards, and making your first or next contribution. + +## Getting started + +To start contributing to Pelican Panel, you need to have a basic understanding of the following: + +* [PHP](https://php.net) & [Laravel](https://laravel.com) +* [Livewire](https://laravel-livewire.com) & [Filament](https://filamentphp.com) +* [Git](https://git-scm.com) & [Github](https://github.com) + +## Dev Environment Setup + +1. Fork the Repository +2. Clone your Fork +3. Install Dependencies (PHP modules & composer, and run `composer install`) +4. Configure your Environment (via `php artisan p:environment:setup`) +5. Set up your Database (via `php artisan p:environment:database`) and run Migrations (via `php artisan migrate --seed --force`) +6. Create your first Admin User (via `php artisan p:user:make`) +7. Start your Webserver (e.g. Nginx or Apache) + +As IDE we recommend [Visual Studio](https://visualstudio.microsoft.com)/ [Visual Studio Code](https://code.visualstudio.com) (free) or [PhpStorm](https://www.jetbrains.com/phpstorm) (paid). + +To easily install PHP and the Webserver we recommend Laravel Herd. ([Windows](https://herd.laravel.com/windows) & [macOS](https://herd.laravel.com)) +The (paid) Pro version of Laravel Herd also offers easy MySQL and Redis hosting, but it is not needed. + +## Coding Standards + +We use PHPStan/ [Larastan](https://github.com/larastan/larastan) and PHP-CS-Fixer/ [Pint](https://laravel.com/docs/11.x/pint) to enforce certain code styles and standards. +You can run PHPStan via `\vendor\bin\phpstan analyse` and Pint via `\vendor\bin\pint`. + +## Making Contributions + +From your forked repository, make your own changes on your own branch. (do not make changes directly to `main`!) +When you are ready, you can submit a pull request to the Pelican repository. If you still work on your pull request or need help with something make sure to mark it as Draft. + +Also, please make sure that your pull requests are as targeted and simple as possible and don't do a hundred things at a time. If you want to add/ change/ fix 5 different things you should make 5 different pull requests. + +*Note: For now we only accept pull requests that handle existing issues!* + +## Code Review Process + +Your pull request will then be reviewed by the maintainers. +Once you have an approval from a maintainer, another will merge it once it’s confirmed. + +Depending on the pull request size this process can take multiple days. + +## Community and Support + +* Help: [Discord](https://discord.gg/pelican-panel) +* Bugs: [GitHub Issues](https://github.com/pelican-dev/panel/issues) +* Features: [GitHub Discussions](https://github.com/pelican-dev/panel/discussions) +* Security vulnerabilities: See our [security policy](./security.md). diff --git a/database/.gitignore b/database/.gitignore index 9b1dffd90..0c40e2f77 100644 --- a/database/.gitignore +++ b/database/.gitignore @@ -1 +1,2 @@ *.sqlite +*.sqlite.backup diff --git a/database/Seeders/eggs/minecraft/egg-bungeecord.json b/database/Seeders/eggs/minecraft/egg-bungeecord.json index 9a2f68233..66fcbc885 100644 --- a/database/Seeders/eggs/minecraft/egg-bungeecord.json +++ b/database/Seeders/eggs/minecraft/egg-bungeecord.json @@ -2,9 +2,9 @@ "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL", "meta": { "version": "PTDL_v2", - "update_url": null + "update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-bungeecord.json" }, - "exported_at": "2024-07-03T10:30:17+00:00", + "exported_at": "2024-07-25T12:03:12+00:00", "name": "Bungeecord", "author": "panel@example.com", "uuid": "9e6b409e-4028-4947-aea8-50a2c404c271", @@ -44,7 +44,11 @@ "default_value": "latest", "user_viewable": true, "user_editable": true, - "rules": "required|alpha_num|between:1,6", + "rules": [ + "required", + "alpha_num", + "between:1,6" + ], "sort": 1, "field_type": "text" }, @@ -55,7 +59,10 @@ "default_value": "bungeecord.jar", "user_viewable": true, "user_editable": true, - "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/", + "rules": [ + "required", + "regex:\/^([\\w\\d._-]+)(\\.jar)$\/" + ], "sort": 2, "field_type": "text" }, @@ -71,4 +78,4 @@ "field_type": "text" } ] -} \ No newline at end of file +} diff --git a/database/Seeders/eggs/minecraft/egg-forge-minecraft.json b/database/Seeders/eggs/minecraft/egg-forge-minecraft.json index dcae88222..cd7560a12 100644 --- a/database/Seeders/eggs/minecraft/egg-forge-minecraft.json +++ b/database/Seeders/eggs/minecraft/egg-forge-minecraft.json @@ -2,9 +2,9 @@ "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL", "meta": { "version": "PTDL_v2", - "update_url": null + "update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-forge-minecraft.json" }, - "exported_at": "2024-07-03T10:30:18+00:00", + "exported_at": "2024-07-25T12:03:19+00:00", "name": "Forge Minecraft", "author": "panel@example.com", "uuid": "ed072427-f209-4603-875c-f540c6dd5a65", @@ -44,7 +44,10 @@ "default_value": "server.jar", "user_viewable": true, "user_editable": true, - "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/", + "rules": [ + "required", + "regex:\/^([\\w\\d._-]+)(\\.jar)$\/" + ], "sort": 1, "field_type": "text" }, @@ -55,7 +58,11 @@ "default_value": "latest", "user_viewable": true, "user_editable": true, - "rules": "required|string|max:9", + "rules": [ + "required", + "string", + "max:9" + ], "sort": 2, "field_type": "text" }, @@ -66,7 +73,11 @@ "default_value": "recommended", "user_viewable": true, "user_editable": true, - "rules": "required|string|in:recommended,latest", + "rules": [ + "required", + "string", + "in:recommended,latest" + ], "sort": 3, "field_type": "text" }, @@ -77,7 +88,10 @@ "default_value": "", "user_viewable": true, "user_editable": true, - "rules": "nullable|regex:\/^[0-9\\.\\-]+$\/", + "rules": [ + "nullable", + "regex:\/^[0-9\\.\\-]+$\/" + ], "sort": 4, "field_type": "text" }, @@ -93,4 +107,4 @@ "field_type": "text" } ] -} \ No newline at end of file +} diff --git a/database/Seeders/eggs/minecraft/egg-paper.json b/database/Seeders/eggs/minecraft/egg-paper.json index 89db21f56..d8a73b9f4 100644 --- a/database/Seeders/eggs/minecraft/egg-paper.json +++ b/database/Seeders/eggs/minecraft/egg-paper.json @@ -2,9 +2,9 @@ "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL", "meta": { "version": "PTDL_v2", - "update_url": null + "update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-paper.json" }, - "exported_at": "2024-07-03T10:30:19+00:00", + "exported_at": "2024-07-25T12:03:43+00:00", "name": "Paper", "author": "parker@example.com", "uuid": "5da37ef6-58da-4169-90a6-e683e1721247", @@ -44,7 +44,11 @@ "default_value": "latest", "user_viewable": true, "user_editable": true, - "rules": "nullable|string|max:20", + "rules": [ + "nullable", + "string", + "max:20" + ], "sort": 1, "field_type": "text" }, @@ -55,7 +59,10 @@ "default_value": "server.jar", "user_viewable": true, "user_editable": true, - "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/", + "rules": [ + "required", + "regex:\/^([\\w\\d._-]+)(\\.jar)$\/" + ], "sort": 2, "field_type": "text" }, @@ -66,7 +73,10 @@ "default_value": "", "user_viewable": false, "user_editable": false, - "rules": "nullable|string", + "rules": [ + "nullable", + "string" + ], "sort": 3, "field_type": "text" }, @@ -77,20 +87,13 @@ "default_value": "latest", "user_viewable": true, "user_editable": true, - "rules": "required|string|max:20", + "rules": [ + "required", + "string", + "max:20" + ], "sort": 4, "field_type": "text" - }, - { - "name": "Server Port", - "description": "", - "env_variable": "SERVER_PORT", - "default_value": "25565", - "user_viewable": true, - "user_editable": false, - "rules": "required|port", - "sort": 5, - "field_type": "text" } ] } \ No newline at end of file diff --git a/database/Seeders/eggs/minecraft/egg-sponge-sponge-vanilla.json b/database/Seeders/eggs/minecraft/egg-sponge-sponge-vanilla.json index d910cfb33..1a72aefe7 100644 --- a/database/Seeders/eggs/minecraft/egg-sponge-sponge-vanilla.json +++ b/database/Seeders/eggs/minecraft/egg-sponge-sponge-vanilla.json @@ -2,9 +2,9 @@ "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL", "meta": { "version": "PTDL_v2", - "update_url": null + "update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-sponge--sponge-vanilla.json" }, - "exported_at": "2024-07-03T10:30:19+00:00", + "exported_at": "2024-07-25T12:03:55+00:00", "name": "Sponge (SpongeVanilla)", "author": "panel@example.com", "uuid": "f0d2f88f-1ff3-42a0-b03f-ac44c5571e6d", @@ -37,17 +37,6 @@ } }, "variables": [ - { - "name": "Server Port", - "description": "Main Port", - "env_variable": "SERVER_PORT", - "default_value": "null", - "user_viewable": true, - "user_editable": false, - "rules": "port", - "sort": 1, - "field_type": "text" - }, { "name": "Sponge Version", "description": "The version of SpongeVanilla to download and use.", @@ -55,8 +44,11 @@ "default_value": "1.12.2-7.3.0", "user_viewable": true, "user_editable": true, - "rules": "required|regex:\/^([a-zA-Z0-9.\\-_]+)$\/", - "sort": 2, + "rules": [ + "required", + "regex:\/^([a-zA-Z0-9.\\-_]+)$\/" + ], + "sort": 1, "field_type": "text" }, { @@ -66,19 +58,11 @@ "default_value": "server.jar", "user_viewable": true, "user_editable": true, - "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/", - "sort": 3, - "field_type": "text" - }, - { - "name": "Server Port", - "description": "", - "env_variable": "SERVER_PORT", - "default_value": "25565", - "user_viewable": true, - "user_editable": false, - "rules": "required|port", - "sort": 4, + "rules": [ + "required", + "regex:\/^([\\w\\d._-]+)(\\.jar)$\/" + ], + "sort": 2, "field_type": "text" } ] diff --git a/database/Seeders/eggs/minecraft/egg-vanilla-minecraft.json b/database/Seeders/eggs/minecraft/egg-vanilla-minecraft.json index 8698c3a55..71d71938e 100644 --- a/database/Seeders/eggs/minecraft/egg-vanilla-minecraft.json +++ b/database/Seeders/eggs/minecraft/egg-vanilla-minecraft.json @@ -2,9 +2,9 @@ "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL", "meta": { "version": "PTDL_v2", - "update_url": null + "update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-vanilla-minecraft.json" }, - "exported_at": "2024-07-03T10:30:20+00:00", + "exported_at": "2024-07-25T12:04:05+00:00", "name": "Vanilla Minecraft", "author": "panel@example.com", "uuid": "9ac39f3d-0c34-4d93-8174-c52ab9e6c57b", @@ -44,7 +44,10 @@ "default_value": "server.jar", "user_viewable": true, "user_editable": true, - "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/", + "rules": [ + "required", + "regex:\/^([\\w\\d._-]+)(\\.jar)$\/" + ], "sort": 1, "field_type": "text" }, @@ -55,20 +58,13 @@ "default_value": "latest", "user_viewable": true, "user_editable": true, - "rules": "required|string|between:3,15", + "rules": [ + "required", + "string", + "between:3,15" + ], "sort": 2, "field_type": "text" - }, - { - "name": "Server Port", - "description": "", - "env_variable": "SERVER_PORT", - "default_value": "25565", - "user_viewable": true, - "user_editable": false, - "rules": "required|port", - "sort": 3, - "field_type": "text" } ] } \ No newline at end of file diff --git a/database/Seeders/eggs/rust/egg-rust.json b/database/Seeders/eggs/rust/egg-rust.json index 30aa18534..cd0367c3f 100644 --- a/database/Seeders/eggs/rust/egg-rust.json +++ b/database/Seeders/eggs/rust/egg-rust.json @@ -2,9 +2,9 @@ "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL", "meta": { "version": "PTDL_v2", - "update_url": null + "update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/rust\/egg-rust.json" }, - "exported_at": "2024-07-03T10:30:26+00:00", + "exported_at": "2024-07-25T12:06:17+00:00", "name": "Rust", "author": "panel@example.com", "uuid": "bace2dfb-209c-452a-9459-7d6f340b07ae", @@ -38,7 +38,11 @@ "default_value": "A Rust Server", "user_viewable": true, "user_editable": true, - "rules": "required|string|max:60", + "rules": [ + "required", + "string", + "max:60" + ], "sort": 1, "field_type": "text" }, @@ -49,7 +53,10 @@ "default_value": "vanilla", "user_viewable": true, "user_editable": true, - "rules": "required|in:vanilla,oxide,carbon", + "rules": [ + "required", + "in:vanilla,oxide,carbon" + ], "sort": 2, "field_type": "text" }, @@ -60,7 +67,11 @@ "default_value": "Procedural Map", "user_viewable": true, "user_editable": true, - "rules": "required|string|max:20", + "rules": [ + "required", + "string", + "max:20" + ], "sort": 3, "field_type": "text" }, @@ -71,7 +82,10 @@ "default_value": "Powered by Panel", "user_viewable": true, "user_editable": true, - "rules": "required|string", + "rules": [ + "required", + "string" + ], "sort": 4, "field_type": "text" }, @@ -82,7 +96,10 @@ "default_value": "http:\/\/example.com", "user_viewable": true, "user_editable": true, - "rules": "nullable|url", + "rules": [ + "nullable", + "url" + ], "sort": 5, "field_type": "text" }, @@ -93,7 +110,10 @@ "default_value": "3000", "user_viewable": true, "user_editable": true, - "rules": "required|integer", + "rules": [ + "required", + "integer" + ], "sort": 6, "field_type": "text" }, @@ -104,7 +124,10 @@ "default_value": "", "user_viewable": true, "user_editable": true, - "rules": "nullable|string", + "rules": [ + "nullable", + "string" + ], "sort": 7, "field_type": "text" }, @@ -115,7 +138,10 @@ "default_value": "40", "user_viewable": true, "user_editable": true, - "rules": "required|integer", + "rules": [ + "required", + "integer" + ], "sort": 8, "field_type": "text" }, @@ -126,7 +152,10 @@ "default_value": "", "user_viewable": true, "user_editable": true, - "rules": "nullable|url", + "rules": [ + "nullable", + "url" + ], "sort": 9, "field_type": "text" }, @@ -137,7 +166,10 @@ "default_value": "27017", "user_viewable": true, "user_editable": false, - "rules": "required|port", + "rules": [ + "required", + "port" + ], "sort": 10, "field_type": "text" }, @@ -148,7 +180,10 @@ "default_value": "28016", "user_viewable": true, "user_editable": false, - "rules": "required|port", + "rules": [ + "required", + "port" + ], "sort": 11, "field_type": "text" }, @@ -159,7 +194,11 @@ "default_value": "", "user_viewable": true, "user_editable": true, - "rules": "required|regex:\/^[\\w.-]*$\/|max:64", + "rules": [ + "required", + "regex:\/^[\\w.-]*$\/", + "max:64" + ], "sort": 12, "field_type": "text" }, @@ -170,7 +209,10 @@ "default_value": "60", "user_viewable": true, "user_editable": true, - "rules": "required|integer", + "rules": [ + "required", + "integer" + ], "sort": 13, "field_type": "text" }, @@ -181,7 +223,10 @@ "default_value": "", "user_viewable": true, "user_editable": true, - "rules": "nullable|string", + "rules": [ + "nullable", + "string" + ], "sort": 14, "field_type": "text" }, @@ -192,7 +237,10 @@ "default_value": "28082", "user_viewable": true, "user_editable": false, - "rules": "required|port", + "rules": [ + "required", + "port" + ], "sort": 15, "field_type": "text" }, @@ -203,7 +251,10 @@ "default_value": "", "user_viewable": true, "user_editable": true, - "rules": "nullable|url", + "rules": [ + "nullable", + "url" + ], "sort": 16, "field_type": "text" }, @@ -214,7 +265,10 @@ "default_value": "", "user_viewable": true, "user_editable": true, - "rules": "nullable|url", + "rules": [ + "nullable", + "url" + ], "sort": 17, "field_type": "text" }, @@ -225,9 +279,13 @@ "default_value": "258550", "user_viewable": false, "user_editable": false, - "rules": "required|string|in:258550", + "rules": [ + "required", + "string", + "in:258550" + ], "sort": 18, "field_type": "text" } ] -} \ No newline at end of file +} diff --git a/database/Seeders/eggs/source-engine/egg-counter-strike-global-offensive.json b/database/Seeders/eggs/source-engine/egg-counter-strike-global-offensive.json index 887610caf..1c092bd21 100644 --- a/database/Seeders/eggs/source-engine/egg-counter-strike-global-offensive.json +++ b/database/Seeders/eggs/source-engine/egg-counter-strike-global-offensive.json @@ -2,9 +2,9 @@ "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL", "meta": { "version": "PTDL_v2", - "update_url": null + "update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/source-engine\/egg-counter--strike--global-offensive.json" }, - "exported_at": "2024-07-03T10:30:21+00:00", + "exported_at": "2024-07-25T12:04:25+00:00", "name": "Counter-Strike: Global Offensive", "author": "panel@example.com", "uuid": "437c367d-06be-498f-a604-fdad135504d7", @@ -39,7 +39,11 @@ "default_value": "de_dust2", "user_viewable": true, "user_editable": true, - "rules": "required|string|alpha_dash", + "rules": [ + "required", + "string", + "alpha_dash" + ], "sort": 1, "field_type": "text" }, @@ -50,7 +54,12 @@ "default_value": "", "user_viewable": true, "user_editable": true, - "rules": "required|string|alpha_num|size:32", + "rules": [ + "required", + "string", + "alpha_num", + "size:32" + ], "sort": 2, "field_type": "text" }, @@ -61,20 +70,13 @@ "default_value": "740", "user_viewable": false, "user_editable": false, - "rules": "required|string|max:20", + "rules": [ + "required", + "string", + "max:20" + ], "sort": 3, "field_type": "text" - }, - { - "name": "Server Port", - "description": "", - "env_variable": "SERVER_PORT", - "default_value": "27015", - "user_viewable": true, - "user_editable": false, - "rules": "required|port", - "sort": 4, - "field_type": "text" } ] } \ No newline at end of file diff --git a/database/Seeders/eggs/source-engine/egg-custom-source-engine-game.json b/database/Seeders/eggs/source-engine/egg-custom-source-engine-game.json index a104ff83d..1126e629f 100644 --- a/database/Seeders/eggs/source-engine/egg-custom-source-engine-game.json +++ b/database/Seeders/eggs/source-engine/egg-custom-source-engine-game.json @@ -2,9 +2,9 @@ "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL", "meta": { "version": "PTDL_v2", - "update_url": null + "update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/source-engine\/egg-custom-source-engine-game.json" }, - "exported_at": "2024-07-03T10:30:22+00:00", + "exported_at": "2024-07-25T12:03:27+00:00", "name": "Custom Source Engine Game", "author": "panel@example.com", "uuid": "2a42d0c2-c0ba-4067-9a0a-9b95d77a3490", @@ -38,7 +38,11 @@ "default_value": "", "user_viewable": true, "user_editable": false, - "rules": "required|numeric|digits_between:1,6", + "rules": [ + "required", + "numeric", + "digits_between:1,6" + ], "sort": 1, "field_type": "text" }, @@ -49,7 +53,11 @@ "default_value": "", "user_viewable": true, "user_editable": false, - "rules": "required|alpha_dash|between:1,100", + "rules": [ + "required", + "alpha_dash", + "between:1,100" + ], "sort": 2, "field_type": "text" }, @@ -60,7 +68,11 @@ "default_value": "", "user_viewable": true, "user_editable": true, - "rules": "required|string|alpha_dash", + "rules": [ + "required", + "string", + "alpha_dash" + ], "sort": 3, "field_type": "text" }, @@ -71,7 +83,10 @@ "default_value": "", "user_viewable": true, "user_editable": true, - "rules": "nullable|string", + "rules": [ + "nullable", + "string" + ], "sort": 4, "field_type": "text" }, @@ -82,7 +97,10 @@ "default_value": "", "user_viewable": true, "user_editable": true, - "rules": "nullable|string", + "rules": [ + "nullable", + "string" + ], "sort": 5, "field_type": "text" }, @@ -93,20 +111,12 @@ "default_value": "", "user_viewable": true, "user_editable": true, - "rules": "nullable|string", + "rules": [ + "nullable", + "string" + ], "sort": 6, "field_type": "text" - }, - { - "name": "Server Port", - "description": "", - "env_variable": "SERVER_PORT", - "default_value": "", - "user_viewable": true, - "user_editable": false, - "rules": "required|port", - "sort": 7, - "field_type": "text" } ] } \ No newline at end of file diff --git a/database/Seeders/eggs/source-engine/egg-garrys-mod.json b/database/Seeders/eggs/source-engine/egg-garrys-mod.json index 0b853ed2d..21a289cd5 100644 --- a/database/Seeders/eggs/source-engine/egg-garrys-mod.json +++ b/database/Seeders/eggs/source-engine/egg-garrys-mod.json @@ -2,9 +2,9 @@ "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL", "meta": { "version": "PTDL_v2", - "update_url": null + "update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/source-engine\/egg-garrys-mod.json" }, - "exported_at": "2024-07-03T10:30:22+00:00", + "exported_at": "2024-07-25T12:05:02+00:00", "name": "Garrys Mod", "author": "panel@example.com", "uuid": "60ef81d4-30a2-4d98-ab64-f59c69e2f915", @@ -39,7 +39,11 @@ "default_value": "gm_flatgrass", "user_viewable": true, "user_editable": true, - "rules": "required|string|alpha_dash", + "rules": [ + "required", + "string", + "alpha_dash" + ], "sort": 1, "field_type": "text" }, @@ -50,7 +54,12 @@ "default_value": "", "user_viewable": true, "user_editable": true, - "rules": "nullable|string|alpha_num|size:32", + "rules": [ + "nullable", + "string", + "alpha_num", + "size:32" + ], "sort": 2, "field_type": "text" }, @@ -61,7 +70,11 @@ "default_value": "4020", "user_viewable": false, "user_editable": false, - "rules": "required|string|max:20", + "rules": [ + "required", + "string", + "max:20" + ], "sort": 3, "field_type": "text" }, @@ -72,7 +85,10 @@ "default_value": "", "user_viewable": true, "user_editable": true, - "rules": "nullable|integer", + "rules": [ + "nullable", + "integer" + ], "sort": 4, "field_type": "text" }, @@ -83,7 +99,10 @@ "default_value": "sandbox", "user_viewable": true, "user_editable": true, - "rules": "required|string", + "rules": [ + "required", + "string" + ], "sort": 5, "field_type": "text" }, @@ -94,7 +113,11 @@ "default_value": "32", "user_viewable": true, "user_editable": true, - "rules": "required|integer|max:128", + "rules": [ + "required", + "integer", + "max:128" + ], "sort": 6, "field_type": "text" }, @@ -105,7 +128,11 @@ "default_value": "22", "user_viewable": true, "user_editable": true, - "rules": "required|integer|max:100", + "rules": [ + "required", + "integer", + "max:100" + ], "sort": 7, "field_type": "text" }, @@ -116,20 +143,12 @@ "default_value": "0", "user_viewable": true, "user_editable": true, - "rules": "required|boolean", + "rules": [ + "required", + "boolean" + ], "sort": 8, "field_type": "text" - }, - { - "name": "Server Port", - "description": "", - "env_variable": "SERVER_PORT", - "default_value": "27015", - "user_viewable": true, - "user_editable": false, - "rules": "required|port", - "sort": 9, - "field_type": "text" } ] } \ No newline at end of file diff --git a/database/Seeders/eggs/source-engine/egg-insurgency.json b/database/Seeders/eggs/source-engine/egg-insurgency.json index fbbabe260..1c7e1525f 100644 --- a/database/Seeders/eggs/source-engine/egg-insurgency.json +++ b/database/Seeders/eggs/source-engine/egg-insurgency.json @@ -2,9 +2,9 @@ "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL", "meta": { "version": "PTDL_v2", - "update_url": null + "update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/source-engine\/egg-insurgency.json" }, - "exported_at": "2024-07-03T10:30:23+00:00", + "exported_at": "2024-07-25T12:05:30+00:00", "name": "Insurgency", "author": "panel@example.com", "uuid": "a5702286-655b-4069-bf1e-925c7300b61a", @@ -38,7 +38,10 @@ "default_value": "237410", "user_viewable": true, "user_editable": false, - "rules": "required|regex:\/^(237410)$\/", + "rules": [ + "required", + "regex:\/^(237410)$\/" + ], "sort": 1, "field_type": "text" }, @@ -49,20 +52,12 @@ "default_value": "sinjar", "user_viewable": true, "user_editable": true, - "rules": "required|regex:\/^(\\w{1,20})$\/", + "rules": [ + "required", + "regex:\/^(\\w{1,20})$\/" + ], "sort": 2, "field_type": "text" - }, - { - "name": "Server Port", - "description": "", - "env_variable": "SERVER_PORT", - "default_value": "27015", - "user_viewable": true, - "user_editable": false, - "rules": "required|port", - "sort": 3, - "field_type": "text" } ] } \ No newline at end of file diff --git a/database/Seeders/eggs/source-engine/egg-team-fortress2.json b/database/Seeders/eggs/source-engine/egg-team-fortress2.json index 101912570..667d04555 100644 --- a/database/Seeders/eggs/source-engine/egg-team-fortress2.json +++ b/database/Seeders/eggs/source-engine/egg-team-fortress2.json @@ -2,9 +2,9 @@ "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL", "meta": { "version": "PTDL_v2", - "update_url": null + "update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/source-engine\/egg-team-fortress2.json" }, - "exported_at": "2024-07-03T10:30:24+00:00", + "exported_at": "2024-07-25T12:05:42+00:00", "name": "Team Fortress 2", "author": "panel@example.com", "uuid": "7f8eb681-b2c8-4bf8-b9f4-d79ff70b6e5d", @@ -39,7 +39,10 @@ "default_value": "232250", "user_viewable": true, "user_editable": false, - "rules": "required|in:232250", + "rules": [ + "required", + "in:232250" + ], "sort": 1, "field_type": "text" }, @@ -50,7 +53,10 @@ "default_value": "cp_dustbowl", "user_viewable": true, "user_editable": true, - "rules": "required|regex:\/^(\\w{1,20})$\/", + "rules": [ + "required", + "regex:\/^(\\w{1,20})$\/" + ], "sort": 2, "field_type": "text" }, @@ -61,20 +67,14 @@ "default_value": "", "user_viewable": true, "user_editable": true, - "rules": "required|string|alpha_num|size:32", + "rules": [ + "required", + "string", + "alpha_num", + "size:32" + ], "sort": 3, "field_type": "text" - }, - { - "name": "Server Port", - "description": "", - "env_variable": "SERVER_PORT", - "default_value": "27015", - "user_viewable": true, - "user_editable": false, - "rules": "required|port", - "sort": 4, - "field_type": "text" } ] } \ No newline at end of file diff --git a/database/Seeders/eggs/voice-servers/egg-mumble-server.json b/database/Seeders/eggs/voice-servers/egg-mumble-server.json index 8ab2ac06d..a4c02919f 100644 --- a/database/Seeders/eggs/voice-servers/egg-mumble-server.json +++ b/database/Seeders/eggs/voice-servers/egg-mumble-server.json @@ -2,9 +2,9 @@ "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL", "meta": { "version": "PTDL_v2", - "update_url": null + "update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/voice-servers\/egg-mumble-server.json" }, - "exported_at": "2024-07-03T10:30:24+00:00", + "exported_at": "2024-07-25T12:05:52+00:00", "name": "Mumble Server", "author": "panel@example.com", "uuid": "727ee758-7fb2-4979-972b-d3eba4e1e9f0", @@ -36,20 +36,13 @@ "default_value": "100", "user_viewable": true, "user_editable": false, - "rules": "required|numeric|digits_between:1,5", + "rules": [ + "required", + "numeric", + "digits_between:1,5" + ], "sort": 1, "field_type": "text" - }, - { - "name": "Server Port", - "description": "", - "env_variable": "SERVER_PORT", - "default_value": "64738", - "user_viewable": true, - "user_editable": false, - "rules": "required|port", - "sort": 2, - "field_type": "text" } ] } \ No newline at end of file diff --git a/database/Seeders/eggs/voice-servers/egg-teamspeak3-server.json b/database/Seeders/eggs/voice-servers/egg-teamspeak3-server.json index 3cddb0463..d470ca3f2 100644 --- a/database/Seeders/eggs/voice-servers/egg-teamspeak3-server.json +++ b/database/Seeders/eggs/voice-servers/egg-teamspeak3-server.json @@ -2,14 +2,14 @@ "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL", "meta": { "version": "PTDL_v2", - "update_url": null + "update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/voice-servers\/egg-teamspeak3-server.json" }, - "exported_at": "2024-07-03T10:30:25+00:00", + "exported_at": "2024-07-25T12:06:05+00:00", "name": "Teamspeak3 Server", "author": "panel@example.com", "uuid": "983b1fac-d322-4d5f-a636-436127326b37", "description": "VoIP software designed with security in mind, featuring crystal clear voice quality, endless customization options, and scalabilty up to thousands of simultaneous users.", - "features": null, + "features": [], "docker_images": { "Debian": "ghcr.io\/parkervcp\/yolks:debian" }, @@ -36,8 +36,12 @@ "default_value": "latest", "user_viewable": true, "user_editable": true, - "rules": "required|string|max:6", - "sort": null, + "rules": [ + "required", + "string", + "max:6" + ], + "sort": 1, "field_type": "text" }, { @@ -47,8 +51,11 @@ "default_value": "30033", "user_viewable": true, "user_editable": false, - "rules": "required|port", - "sort": null, + "rules": [ + "required", + "port" + ], + "sort": 2, "field_type": "text" }, { @@ -58,8 +65,12 @@ "default_value": "10011", "user_viewable": true, "user_editable": false, - "rules": "required|port", - "sort": null, + "rules": [ + "required", + "integer", + "port" + ], + "sort": 3, "field_type": "text" }, { @@ -69,8 +80,12 @@ "default_value": "raw,http,ssh", "user_viewable": true, "user_editable": true, - "rules": "required|string|max:12", - "sort": null, + "rules": [ + "required", + "string", + "max:12" + ], + "sort": 4, "field_type": "text" }, { @@ -81,7 +96,11 @@ "user_viewable": true, "user_editable": false, "rules": "required|port", - "sort": null, + "rules": [ + "required", + "port" + ], + "sort": 5, "field_type": "text" }, { @@ -91,9 +110,12 @@ "default_value": "10080", "user_viewable": true, "user_editable": false, - "rules": "required|port", - "sort": null, + "rules": [ + "required", + "port", + ], + "sort": 6, "field_type": "text" } ] -} \ No newline at end of file +} 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 000000000..892ca1826 --- /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(); + }); + } +}; 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 000000000..47c78a09a --- /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 + } +}; diff --git a/database/migrations/2024_07_25_072050_convert_rules_to_array.php b/database/migrations/2024_07_25_072050_convert_rules_to_array.php new file mode 100644 index 000000000..6646a6661 --- /dev/null +++ b/database/migrations/2024_07_25_072050_convert_rules_to_array.php @@ -0,0 +1,37 @@ +select(['id', 'rules'])->cursor()->each(function ($eggVariable) { + DB::table('egg_variables')->where('id', $eggVariable->id)->update(['rules' => explode('|', $eggVariable->rules)]); + }); + + Schema::table('egg_variables', function (Blueprint $table) { + $table->json('rules')->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('egg_variables', function (Blueprint $table) { + $table->text('rules')->change(); + }); + + DB::table('egg_variables')->select(['id', 'rules'])->cursor()->each(function ($eggVariable) { + DB::table('egg_variables')->where('id', $eggVariable->id)->update(['rules' => implode('|', json_decode($eggVariable->rules))]); + }); + } +}; diff --git a/database/migrations/2024_08_13_171337_fix_allocation_server_foreign_key.php b/database/migrations/2024_08_13_171337_fix_allocation_server_foreign_key.php new file mode 100644 index 000000000..9abb9317e --- /dev/null +++ b/database/migrations/2024_08_13_171337_fix_allocation_server_foreign_key.php @@ -0,0 +1,54 @@ +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 () { + 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 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")'); + }); + + DB::statement('PRAGMA foreign_keys = ON'); + DB::statement('PRAGMA legacy_alter_table = OFF'); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // Reverse not needed + } +}; diff --git a/database/migrations/2024_06_26_043350_modify_allocations.php b/database/migrations/2024_09_18_043350_modify_allocations.php similarity index 100% rename from database/migrations/2024_06_26_043350_modify_allocations.php rename to database/migrations/2024_09_18_043350_modify_allocations.php diff --git a/lang/en/commands.php b/lang/en/commands.php index a42ca228b..b6fa6c9ca 100644 --- a/lang/en/commands.php +++ b/lang/en/commands.php @@ -6,11 +6,11 @@ return [ '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.', - 'comment' => 'By default a Redis server instance has no password as it is running locally and inaccessible to the outside world. If this is the case, simply hit enter without entering a value.', + 'comment' => 'By default a Redis server instance has for username default and no password as it is running locally and inaccessible to the outside world. If this is the case, simply hit enter without entering a value.', + 'confirm' => 'It seems a :field is already defined for Redis, would you like to change it?', ], ], 'database_settings' => [ diff --git a/lang/en/exceptions.php b/lang/en/exceptions.php index 3c9adf4c9..9ec18f9b2 100644 --- a/lang/en/exceptions.php +++ b/lang/en/exceptions.php @@ -11,7 +11,7 @@ return [ '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.', diff --git a/package.json b/package.json index ebb3266be..713b5cbaf 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "react-router-dom": "^5.1.2", "react-transition-group": "^4.4.1", "reaptcha": "^1.7.2", + "rimraf": "^4", "sockette": "^2.0.6", "styled-components": "^5.2.1", "styled-components-breakpoint": "^3.0.0-preview.20", @@ -130,7 +131,7 @@ "yarn-deduplicate": "^1.1.1" }, "scripts": { - "clean": "cd public/assets && find . \\( -name \"*.js\" -o -name \"*.map\" \\) -type f -delete", + "clean": "cd public/assets && rimraf -g *.js *.map", "test": "jest", "lint": "eslint ./resources/scripts/**/*.{ts,tsx} --ext .ts,.tsx", "watch": "cross-env NODE_ENV=development ./node_modules/.bin/webpack --watch --progress", diff --git a/readme.md b/readme.md index 74b0c7998..210802d90 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* diff --git a/resources/scripts/components/dashboard/forms/SetupTOTPDialog.tsx b/resources/scripts/components/dashboard/forms/SetupTOTPDialog.tsx index b1635a32d..ec8326aa8 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'; @@ -127,7 +126,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); diff --git a/resources/scripts/components/dashboard/forms/UpdateEmailAddressForm.tsx b/resources/scripts/components/dashboard/forms/UpdateEmailAddressForm.tsx index 2258e67b9..7980f0d7a 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' })} />
diff --git a/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx b/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx index 36db1fff9..ee638e3f9 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 0af8fb441..4903df931 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' ? (
{
+ ) : ( +
+ + + + +
)}
diff --git a/resources/views/admin/settings/advanced.blade.php b/resources/views/admin/settings/advanced.blade.php deleted file mode 100644 index dc5543ca9..000000000 --- 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 19356e8b8..000000000 --- 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 0488d1c7a..000000000 --- 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/components/node-cpu-chart.blade.php b/resources/views/filament/components/node-cpu-chart.blade.php new file mode 100644 index 000000000..d2627c42d --- /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 000000000..cb934d007 --- /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 000000000..ea7b5358c --- /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()]) + diff --git a/resources/views/filament/pages/installer.blade.php b/resources/views/filament/pages/installer.blade.php new file mode 100644 index 000000000..1977991c2 --- /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/resources/views/filament/pages/settings.blade.php b/resources/views/filament/pages/settings.blade.php new file mode 100644 index 000000000..9f9b3f439 --- /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 d48fad64b..24b21db19 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 9f1ace7f3..000000000 --- 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 980c5ef60..000000000 --- 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 894f3db6d..ffac12d88 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -40,26 +40,6 @@ Route::prefix('databases')->group(function () { 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 diff --git a/routes/base.php b/routes/base.php index 6fbc41ae8..69e1c2f9a 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)).+'); diff --git a/tests/Integration/Api/Client/Server/Startup/UpdateStartupVariableTest.php b/tests/Integration/Api/Client/Server/Startup/UpdateStartupVariableTest.php index 02d972fe4..e9c8af97c 100644 --- a/tests/Integration/Api/Client/Server/Startup/UpdateStartupVariableTest.php +++ b/tests/Integration/Api/Client/Server/Startup/UpdateStartupVariableTest.php @@ -120,7 +120,7 @@ class UpdateStartupVariableTest extends ClientApiIntegrationTestCase [$user, $server] = $this->generateTestAccount(); $egg = $this->cloneEggAndVariables($server->egg); - $egg->variables()->first()->update(['rules' => 'nullable|string']); + $egg->variables()->first()->update(['rules' => ['nullable', 'string']]); $server->fill(['egg_id' => $egg->id])->save(); $server->refresh(); diff --git a/tests/Integration/Services/Servers/VariableValidatorServiceTest.php b/tests/Integration/Services/Servers/VariableValidatorServiceTest.php index 113e29762..6f49b97fb 100644 --- a/tests/Integration/Services/Servers/VariableValidatorServiceTest.php +++ b/tests/Integration/Services/Servers/VariableValidatorServiceTest.php @@ -120,7 +120,7 @@ class VariableValidatorServiceTest extends IntegrationTestCase $egg = $this->cloneEggAndVariables($this->egg); $egg->variables()->where('env_variable', '!=', 'BUNGEE_VERSION')->delete(); - $egg->variables()->update(['rules' => 'nullable|string']); + $egg->variables()->update(['rules' => ['nullable', 'string']]); $response = $this->getService()->handle($egg->id, []); $this->assertCount(1, $response); diff --git a/tests/Unit/Helpers/EnvironmentWriterTraitTest.php b/tests/Unit/Helpers/EnvironmentWriterTraitTest.php index d73681b39..a700c592f 100644 --- a/tests/Unit/Helpers/EnvironmentWriterTraitTest.php +++ b/tests/Unit/Helpers/EnvironmentWriterTraitTest.php @@ -3,7 +3,7 @@ namespace App\Tests\Unit\Helpers; use App\Tests\TestCase; -use App\Traits\Commands\EnvironmentWriterTrait; +use App\Traits\EnvironmentWriterTrait; class EnvironmentWriterTraitTest extends TestCase { diff --git a/yarn.lock b/yarn.lock index ff431b75e..fca7a7415 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2736,6 +2736,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^2.3.1, braces@^2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" @@ -4758,6 +4765,16 @@ glob@^7.0.3, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^9.2.0: + version "9.3.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-9.3.5.tgz#ca2ed8ca452781a3009685607fdf025a899dfe21" + integrity sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q== + dependencies: + fs.realpath "^1.0.0" + minimatch "^8.0.2" + minipass "^4.2.4" + path-scurry "^1.6.1" + global-modules@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" @@ -6164,6 +6181,11 @@ loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4 dependencies: js-tokens "^3.0.0 || ^4.0.0" +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -6376,6 +6398,13 @@ minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" +minimatch@^8.0.2: + version "8.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-8.0.4.tgz#847c1b25c014d4e9a7f68aaf63dedd668a626229" + integrity sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" @@ -6409,11 +6438,21 @@ minipass@^3.0.0, minipass@^3.1.1: dependencies: yallist "^4.0.0" +minipass@^4.2.4: + version "4.2.8" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.8.tgz#f0010f64393ecfc1d1ccb5f582bcaf45f48e1a3a" + integrity sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ== + minipass@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + minizlib@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" @@ -6923,6 +6962,14 @@ path-parse@^1.0.6, path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-scurry@^1.6.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" @@ -7991,6 +8038,13 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" +rimraf@^4: + version "4.4.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-4.4.1.tgz#bd33364f67021c5b79e93d7f4fa0568c7c21b755" + integrity sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og== + dependencies: + glob "^9.2.0" + ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c"