Compare commits

...

29 Commits

Author SHA1 Message Date
Charles
c1fa74ebfd WIP 2026-01-14 13:21:01 -05:00
Boy132
0e810f3110 Throw yarn errors when installing themes (#2104) 2026-01-14 08:23:24 +01:00
Charles
eadbe6e8fd fix client side view database unlimited state (#2047)
Co-authored-by: Boy132 <mail@boy132.de>
2026-01-13 05:33:20 -05:00
Boy132
53aa49b11a Add changes from upstream (#2076)
Co-authored-by: DaneEveritt <dane@daneeveritt.com>
2026-01-13 08:39:50 +01:00
Boy132
6ae4f007c8 Make sure custom pages/relations don't override default pages/relations (#2099) 2026-01-12 18:00:37 +01:00
Boy132
6b9d683f06 Update database config to remove deprecation warning on php 8.5 (#2089) 2026-01-09 14:39:22 +01:00
Boy132
3b24e22316 Set plugin status to "errored" if it errored (#2084) 2026-01-08 17:43:31 +01:00
Boy132
bd012f52a9 Add tests for php 8.5 (#2079) 2026-01-08 17:32:23 +01:00
Boy132
af202d9827 Add user to shouldLink and shouldCreate oauth functions (#2083) 2026-01-08 15:13:15 +01:00
Boy132
6ebeb40ba0 Make rule for user language less restrictive (#2075) 2026-01-06 08:45:53 +01:00
Boy132
333eeda065 Disable field if server variable is not user_editable (#2074) 2026-01-06 08:45:40 +01:00
MartinOscar
fcfafadec7 Return if no egg was selected in the Installer (#2073) 2026-01-05 14:21:34 +01:00
Boy132
76b6118fd1 Fix typo in method name (#2062) 2026-01-04 15:17:48 -05:00
PalmarHealer
3141fe61b4 fix: plugin migration rollback and cache clearing on uninstall (#2033)
Co-authored-by: Boy132 <mail@boy132.de>
2026-01-03 23:44:33 +01:00
Charles
bed9dbeb2b Add Eggs to Installer (#2004)
Co-authored-by: Boy132 <mail@boy132.de>
2025-12-29 17:24:02 -05:00
Boy132
976cb00c0d Replace Artisan::call in plugin service for better error handling (#2031) 2025-12-28 14:44:39 +01:00
Quinten
e3534bbb29 Bungeecord: Fix Download (#2055) 2025-12-28 13:48:22 +01:00
xDev789
5740c93032 Per request cache for permission checks (#2029)
Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>
Co-authored-by: Lance Pioch <lancepioch@gmail.com>
2025-12-28 02:00:59 +01:00
MartinOscar
d72e075977 chore: Prevent users from caching Config (#2048) 2025-12-28 01:50:36 +01:00
Boy132
9af608f808 Fix relation managers for admin server resource (#2050) 2025-12-25 00:44:30 +01:00
Boy132
ac36e7a4b5 Fix oauth providers with no color (#2044) 2025-12-24 14:38:47 +01:00
Boy132
b1c64e2ef1 Add error notification when plugin install, update or uninstall fails (#2032) 2025-12-24 14:38:25 +01:00
PalmarHealer
da2e930d4d Correct bounty link (#2039)
Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>
2025-12-23 15:46:00 -05:00
Charles
460a5dfaf8 composer update (#2030) 2025-12-22 19:01:52 -05:00
killerbite95
576f04be58 fix: use correct log path for upload action (#2016)
Co-authored-by: Charles <charles@pelican.dev>
2025-12-22 19:01:44 -05:00
Boy132
43fb030133 Don't log yarn exceptions as error but warning (#2022) 2025-12-21 15:37:21 +01:00
Boy132
ae054f6e9b Fix actions when plugin is "errored" (#2027) 2025-12-21 15:37:07 +01:00
Boy132
fef91791c3 Fix plugin settings not showing on non-admin plugins (#2023) 2025-12-21 15:36:39 +01:00
Boy132
1d5ace3a6d Clear filament cache when installing a plugin (#2017) 2025-12-20 02:00:57 +01:00
90 changed files with 876 additions and 2041 deletions

View File

@@ -26,7 +26,7 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [8.2, 8.3, 8.4]
php: [8.2, 8.3, 8.4, 8.5]
env:
DB_CONNECTION: sqlite
DB_DATABASE: testing.sqlite
@@ -76,7 +76,7 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [8.2, 8.3, 8.4]
php: [8.2, 8.3, 8.4, 8.5]
database: ["mysql:8"]
services:
database:
@@ -138,7 +138,7 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [8.2, 8.3, 8.4]
php: [8.2, 8.3, 8.4, 8.5]
database: ["mariadb:10.6", "mariadb:10.11", "mariadb:11.4"]
services:
database:
@@ -200,7 +200,7 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [8.2, 8.3, 8.4]
php: [8.2, 8.3, 8.4, 8.5]
database: ["postgres:14"]
services:
database:

View File

@@ -3,7 +3,7 @@ name: Lint
on:
pull_request:
branches:
- '**'
- "**"
jobs:
pint:
@@ -16,7 +16,7 @@ jobs:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.3"
php-version: "8.4"
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none
@@ -35,7 +35,7 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [ 8.2, 8.3, 8.4 ]
php: [8.2, 8.3, 8.4, 8.5]
steps:
- name: Code Checkout
uses: actions/checkout@v4

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Console\Commands\Overrides;
use Illuminate\Foundation\Console\ConfigCacheCommand as BaseConfigCacheCommand;
class ConfigCacheCommand extends BaseConfigCacheCommand
{
/**
* Prevent config from being cached
*/
public function handle()
{
$this->components->warn('Configuration caching has been disabled.');
$this->line(' Reason: This application uses dynamic plugins. Caching config');
$this->line(' prevents /plugins/config/*.php files from being loaded correctly.');
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Console\Commands\Overrides;
use Illuminate\Foundation\Console\OptimizeCommand as BaseOptimizeCommand;
class OptimizeCommand extends BaseOptimizeCommand
{
/**
* Prevent config from being cached
*
* @return array<string, string>
*/
protected function getOptimizeTasks()
{
return array_except(parent::getOptimizeTasks(), 'config');
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Console\Commands\Plugin;
use App\Enums\PluginStatus;
use App\Models\Plugin;
use App\Services\Helpers\PluginService;
use Exception;
use Illuminate\Console\Command;
class InstallPluginCommand extends Command
@@ -31,8 +32,12 @@ class InstallPluginCommand extends Command
return;
}
$pluginService->installPlugin($plugin);
try {
$pluginService->installPlugin($plugin);
$this->info('Plugin installed and enabled.');
$this->info('Plugin installed and enabled.');
} catch (Exception $exception) {
$this->error('Could not install plugin: ' . $exception->getMessage());
}
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Console\Commands\Plugin;
use App\Enums\PluginStatus;
use App\Models\Plugin;
use App\Services\Helpers\PluginService;
use Exception;
use Illuminate\Console\Command;
class UninstallPluginCommand extends Command
@@ -36,8 +37,12 @@ class UninstallPluginCommand extends Command
$deleteFiles = $this->confirm('Do you also want to delete the plugin files?');
}
$pluginService->uninstallPlugin($plugin, $deleteFiles);
try {
$pluginService->uninstallPlugin($plugin, $deleteFiles);
$this->info('Plugin uninstalled' . ($deleteFiles ? ' and files deleted' : '') . '.');
$this->info('Plugin uninstalled' . ($deleteFiles ? ' and files deleted' : '') . '.');
} catch (Exception $exception) {
$this->error('Could not uninstall plugin: ' . $exception->getMessage());
}
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Console\Commands\Plugin;
use App\Models\Plugin;
use App\Services\Helpers\PluginService;
use Exception;
use Illuminate\Console\Command;
class UpdatePluginCommand extends Command
@@ -30,8 +31,12 @@ class UpdatePluginCommand extends Command
return;
}
$pluginService->updatePlugin($plugin);
try {
$pluginService->updatePlugin($plugin);
$this->info('Plugin updated.');
$this->info('Plugin updated.');
} catch (Exception $exception) {
$this->error('Could not update plugin: ' . $exception->getMessage());
}
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Enums;
use App\Models\Server;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Support\Facades\RateLimiter;
use Webmozart\Assert\Assert;
/**
* A basic resource throttler for individual servers. This is applied in addition
* to existing rate limits and allows the code to slow down speedy users that might
* be creating resources a little too quickly for comfort. This throttle generally
* only applies to creation flows, and not general view/edit/delete flows.
*/
enum ResourceLimit: string
{
case Websocket = 'websocket';
case AllocationCreate = 'allocation-create';
case BackupRestore = 'backup-restore';
case DatabaseCreate = 'database-create';
case ScheduleCreate = 'schedule-create';
case SubuserCreate = 'subuser-create';
case FilePull = 'file-pull';
public function throttleKey(): string
{
return "api.client:server-resource:{$this->name}";
}
/**
* Returns a middleware that will throttle the specific resource by server. This
* throttle applies to any user making changes to that resource on the specific
* server, it is NOT per-user.
*/
public function middleware(): string
{
return ThrottleRequests::using($this->throttleKey());
}
public function limit(): Limit
{
return match ($this) {
self::Websocket => Limit::perMinute(5),
self::BackupRestore => Limit::perMinutes(15, 3),
self::DatabaseCreate => Limit::perMinute(2),
self::SubuserCreate => Limit::perMinutes(15, 10),
self::FilePull => Limit::perMinutes(10, 5),
default => Limit::perMinute(2),
};
}
public static function boot(): void
{
foreach (self::cases() as $case) {
RateLimiter::for($case->throttleKey(), function (Request $request) use ($case) {
Assert::isInstanceOf($server = $request->route()->parameter('server'), Server::class);
return $case->limit()->by($server->uuid);
});
}
}
}

View File

@@ -2,8 +2,10 @@
namespace App\Extensions\OAuth;
use App\Models\User;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Wizard\Step;
use Laravel\Socialite\Contracts\User as OAuthUser;
interface OAuthSchemaInterface
{
@@ -33,7 +35,7 @@ interface OAuthSchemaInterface
public function isEnabled(): bool;
public function shouldCreateMissingUsers(): bool;
public function shouldCreateMissingUser(OAuthUser $user): bool;
public function shouldLinkMissingUsers(): bool;
public function shouldLinkMissingUser(User $user, OAuthUser $oauthUser): bool;
}

View File

@@ -18,11 +18,11 @@ final class GithubSchema extends OAuthSchema
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new Github OAuth App')
Step::make('Register new GitHub OAuth App')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://github.com/settings/developers" target="_blank">Github Developer Dashboard</x-filament::link>, go to <b>OAuth Apps</b> and click on <b>New OAuth App</b>.</p><p>Enter an <b>Application name</b> (e.g. your panel name), set <b>Homepage URL</b> to your panel url and enter the below url as <b>Authorization callback URL</b>.</p>'))),
->state(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://github.com/settings/developers" target="_blank">GitHub Developer Dashboard</x-filament::link>, go to <b>OAuth Apps</b> and click on <b>New OAuth App</b>.</p><p>Enter an <b>Application name</b> (e.g. your panel name), set <b>Homepage URL</b> to your panel url and enter the below url as <b>Authorization callback URL</b>.</p>'))),
TextInput::make('_noenv_callback')
->label('Authorization callback URL')
->dehydrated()

View File

@@ -3,12 +3,14 @@
namespace App\Extensions\OAuth\Schemas;
use App\Extensions\OAuth\OAuthSchemaInterface;
use App\Models\User;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Str;
use Laravel\Socialite\Contracts\User as OAuthUser;
abstract class OAuthSchema implements OAuthSchemaInterface
{
@@ -121,14 +123,14 @@ abstract class OAuthSchema implements OAuthSchemaInterface
return env("OAUTH_{$id}_ENABLED", false);
}
public function shouldCreateMissingUsers(): bool
public function shouldCreateMissingUser(OAuthUser $user): bool
{
$id = Str::upper($this->getId());
return env("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS", false);
}
public function shouldLinkMissingUsers(): bool
public function shouldLinkMissingUser(User $user, OAuthUser $oauthUser): bool
{
$id = Str::upper($this->getId());

View File

@@ -56,7 +56,9 @@ class ListLogs extends BaseListLogs
->modalHeading(trans('admin/log.actions.upload_logs'))
->modalDescription(fn ($record) => trans('admin/log.actions.upload_logs_description', ['file' => $record['date'], 'url' => 'https://logs.pelican.dev']))
->action(function ($record) {
$logPath = storage_path('logs/' . $record['date']);
$prefix = config('filament-log-viewer.pattern.prefix', 'laravel-');
$extension = config('filament-log-viewer.pattern.extension', '.log');
$logPath = storage_path('logs/' . $prefix . $record['date'] . $extension);
if (!file_exists($logPath)) {
Notification::make()
@@ -73,18 +75,12 @@ class ListLogs extends BaseListLogs
$uploadLines = $totalLines <= 1000 ? $lines : array_slice($lines, -1000);
$content = implode("\n", $uploadLines);
$logUrl = 'https://logs.pelican.dev';
try {
$response = Http::timeout(10)->asMultipart()->post($logUrl, [
[
'name' => 'c',
'contents' => $content,
],
[
'name' => 'e',
'contents' => '14d',
],
]);
$response = Http::timeout(10)
->asMultipart()
->attach('c', $content)
->attach('e', '14d')
->post('https://logs.pelican.dev');
if ($response->failed()) {
Notification::make()

View File

@@ -256,7 +256,7 @@ class Settings extends Page implements HasSchemas
->connectTimeout(3)
->get('https://api.cloudflare.com/client/v4/ips');
if ($response->getStatusCode() === 200) {
if ($response->status() === 200) {
$result = $response->json('result');
foreach (['ipv4_cidrs', 'ipv6_cidrs'] as $value) {
$ips->push(...data_get($result, $value));
@@ -823,7 +823,6 @@ class Settings extends Page implements HasSchemas
$this->writeToEnvironment($data);
Artisan::call('config:clear');
Artisan::call('queue:restart');
$this->redirect($this->getUrl());

View File

@@ -33,7 +33,9 @@ class ViewLogs extends BaseViewLog
->modalHeading(trans('admin/log.actions.upload_logs'))
->modalDescription(fn () => trans('admin/log.actions.upload_logs_description', ['file' => $this->resolveRecordDate(), 'url' => 'https://logs.pelican.dev']))
->action(function () {
$logPath = storage_path('logs/' . $this->resolveRecordDate());
$prefix = config('filament-log-viewer.pattern.prefix', 'laravel-');
$extension = config('filament-log-viewer.pattern.extension', '.log');
$logPath = storage_path('logs/' . $prefix . $this->resolveRecordDate() . $extension);
if (!file_exists($logPath)) {
Notification::make()
@@ -50,18 +52,12 @@ class ViewLogs extends BaseViewLog
$uploadLines = $totalLines <= 1000 ? $lines : array_slice($lines, -1000);
$content = implode("\n", $uploadLines);
$logUrl = 'https://logs.pelican.dev';
try {
$response = Http::timeout(10)->asMultipart()->post($logUrl, [
[
'name' => 'c',
'contents' => $content,
],
[
'name' => 'e',
'contents' => '14d',
],
]);
$response = Http::timeout(10)
->asMultipart()
->attach('c', $content)
->attach('e', '14d')
->post('https://logs.pelican.dev');
if ($response->failed()) {
Notification::make()

View File

@@ -37,7 +37,7 @@ class DatabasesRelationManager extends RelationManager
->formatStateUsing(fn (Database $record) => $record->remote === '%' ? trans('admin/databasehost.anywhere'). ' ( % )' : $record->remote),
TextInput::make('max_connections')
->label(trans('admin/databasehost.table.max_connections'))
->formatStateUsing(fn (Database $record) => $record->max_connections === 0 ? trans('admin/databasehost.unlimited') : $record->max_connections),
->formatStateUsing(fn (Database $record) => $record->max_connections ?: trans('admin/databasehost.unlimited')),
TextInput::make('jdbc')
->label(trans('admin/databasehost.table.connection_string'))
->columnSpanFull()
@@ -63,7 +63,7 @@ class DatabasesRelationManager extends RelationManager
->url(fn (Database $database) => route('filament.admin.resources.servers.edit', ['record' => $database->server_id])),
TextColumn::make('max_connections')
->label(trans('admin/databasehost.table.max_connections'))
->formatStateUsing(fn ($record) => $record->max_connections === 0 ? trans('admin/databasehost.unlimited') : $record->max_connections),
->formatStateUsing(fn ($record) => $record->max_connections ?: trans('server/database.unlimited')),
DateTimeColumn::make('created_at')
->label(trans('admin/databasehost.table.created_at')),
])

View File

@@ -253,13 +253,11 @@ class CreateNode extends CreateRecord
->columnSpan(2),
TextInput::make('upload_size')
->label(trans('admin/node.upload_limit'))
->helperText(trans('admin/node.upload_limit_help.0'))
->hintIcon('tabler-question-mark', trans('admin/node.upload_limit_help.1'))
->hintIcon('tabler-question-mark', trans('admin/node.upload_limit_help'))
->columnSpan(1)
->numeric()->required()
->default(256)
->minValue(1)
->maxValue(1024)
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'),
TextInput::make('daemon_sftp')
->columnSpan(1)

View File

@@ -319,10 +319,10 @@ class EditNode extends EditRecord
'lg' => 1,
])
->label(trans('admin/node.upload_limit'))
->hintIcon('tabler-question-mark', trans('admin/node.upload_limit_help.0') . trans('admin/node.upload_limit_help.1'))
->numeric()->required()
->hintIcon('tabler-question-mark', trans('admin/node.upload_limit_help'))
->numeric()
->required()
->minValue(1)
->maxValue(1024)
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'),
TextInput::make('daemon_sftp')
->columnSpan([
@@ -684,16 +684,10 @@ class EditNode extends EditRecord
->icon('tabler-cloud-upload')->iconButton()->iconSize(IconSize::ExtraLarge)
->action(function (Get $get, Set $set) {
try {
$response = Http::asMultipart()->post('https://logs.pelican.dev', [
[
'name' => 'c',
'contents' => $get('log'),
],
[
'name' => 'e',
'contents' => '14d',
],
]);
$response = Http::asMultipart()
->attach('c', $get('log'))
->attach('e', '14d')
->post('https://logs.pelican.dev');
if ($response->failed()) {
Notification::make()

View File

@@ -119,14 +119,22 @@ class PluginResource extends Resource
->color('success')
->hidden(fn (Plugin $plugin) => $plugin->status !== PluginStatus::NotInstalled)
->action(function (Plugin $plugin, $livewire, PluginService $pluginService) {
$pluginService->installPlugin($plugin, !$plugin->isTheme() || !$pluginService->hasThemePluginEnabled());
try {
$pluginService->installPlugin($plugin, !$plugin->isTheme() || !$pluginService->hasThemePluginEnabled());
redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab]));
redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab]));
Notification::make()
->success()
->title(trans('admin/plugin.notifications.installed'))
->send();
Notification::make()
->success()
->title(trans('admin/plugin.notifications.installed'))
->send();
} catch (Exception $exception) {
Notification::make()
->danger()
->title(trans('admin/plugin.notifications.install_error'))
->body($exception->getMessage())
->send();
}
}),
Action::make('update')
->label(trans('admin/plugin.update'))
@@ -135,14 +143,22 @@ class PluginResource extends Resource
->color('success')
->visible(fn (Plugin $plugin) => $plugin->status !== PluginStatus::NotInstalled && $plugin->isUpdateAvailable())
->action(function (Plugin $plugin, $livewire, PluginService $pluginService) {
$pluginService->updatePlugin($plugin);
try {
$pluginService->updatePlugin($plugin);
redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab]));
redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab]));
Notification::make()
->success()
->title(trans('admin/plugin.notifications.updated'))
->send();
Notification::make()
->success()
->title(trans('admin/plugin.notifications.updated'))
->send();
} catch (Exception $exception) {
Notification::make()
->danger()
->title(trans('admin/plugin.notifications.update_error'))
->body($exception->getMessage())
->send();
}
}),
Action::make('enable')
->label(trans('admin/plugin.enable'))
@@ -160,7 +176,7 @@ class PluginResource extends Resource
Notification::make()
->success()
->title(trans('admin/plugin.notifications.updated'))
->title(trans('admin/plugin.notifications.enabled'))
->send();
}),
Action::make('disable')
@@ -202,16 +218,24 @@ class PluginResource extends Resource
->icon('tabler-terminal')
->color('danger')
->requiresConfirmation()
->hidden(fn (Plugin $plugin) => $plugin->status === PluginStatus::NotInstalled)
->hidden(fn (Plugin $plugin) => $plugin->status === PluginStatus::NotInstalled || $plugin->status === PluginStatus::Errored)
->action(function (Plugin $plugin, $livewire, PluginService $pluginService) {
$pluginService->uninstallPlugin($plugin);
try {
$pluginService->uninstallPlugin($plugin);
redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab]));
redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab]));
Notification::make()
->success()
->title(trans('admin/plugin.notifications.uninstalled'))
->send();
Notification::make()
->success()
->title(trans('admin/plugin.notifications.uninstalled'))
->send();
} catch (Exception $exception) {
Notification::make()
->danger()
->title(trans('admin/plugin.notifications.uninstall_error'))
->body($exception->getMessage())
->send();
}
}),
]),
])

View File

@@ -480,6 +480,7 @@ class CreateServer extends CreateRecord
->schema([
StartupVariable::make('variable_value')
->fromForm()
->disabled(false)
->afterStateUpdated(function (Set $set, Get $get, $state) {
$environment = $get($envPath = '../../environment');
$environment[$get('env_variable')] = $state;

View File

@@ -3,8 +3,6 @@
namespace App\Filament\Admin\Resources\Servers\Pages;
use App\Enums\SuspendAction;
use App\Filament\Admin\Resources\Servers\RelationManagers\AllocationsRelationManager;
use App\Filament\Admin\Resources\Servers\RelationManagers\DatabasesRelationManager;
use App\Filament\Admin\Resources\Servers\ServerResource;
use App\Filament\Components\Actions\DeleteServerIcon;
use App\Filament\Components\Actions\PreviewStartupAction;
@@ -824,7 +822,8 @@ class EditServer extends EditRecord
->reorderable(false)->addable(false)->deletable(false)
->schema([
StartupVariable::make('variable_value')
->fromRecord(),
->fromRecord()
->disabled(false),
])
->columnSpan(6),
]),
@@ -1197,12 +1196,4 @@ class EditServer extends EditRecord
Storage::disk('public')->put(Server::ICON_STORAGE_PATH . "/$server->uuid.$normalizedExtension", $data);
}
public function getRelationManagers(): array
{
return [
AllocationsRelationManager::class,
DatabasesRelationManager::class,
];
}
}

View File

@@ -49,7 +49,7 @@ class DatabasesRelationManager extends RelationManager
->formatStateUsing(fn (Database $record) => $record->remote === '%' ? trans('admin/databasehost.anywhere'). ' ( % )' : $record->remote),
TextInput::make('max_connections')
->label(trans('admin/databasehost.table.max_connections'))
->formatStateUsing(fn (Database $record) => $record->max_connections === 0 ? trans('admin/databasehost.unlimited') : $record->max_connections),
->formatStateUsing(fn (Database $record) => $record->max_connections ?: trans('admin/databasehost.unlimited')),
TextInput::make('jdbc')
->label(trans('admin/databasehost.table.connection_string'))
->columnSpanFull()
@@ -75,7 +75,7 @@ class DatabasesRelationManager extends RelationManager
->url(fn (Database $database) => route('filament.admin.resources.servers.edit', ['record' => $database->server_id])),
TextColumn::make('max_connections')
->label(trans('admin/databasehost.table.max_connections'))
->formatStateUsing(fn ($record) => $record->max_connections === 0 ? trans('admin/databasehost.unlimited') : $record->max_connections),
->formatStateUsing(fn ($record) => $record->max_connections ?: trans('admin/databasehost.unlimited')),
DateTimeColumn::make('created_at')
->label(trans('admin/databasehost.table.created_at')),
])

View File

@@ -7,6 +7,7 @@ use App\Filament\Admin\Resources\Servers\Pages\CreateServer;
use App\Filament\Admin\Resources\Servers\Pages\EditServer;
use App\Filament\Admin\Resources\Servers\Pages\ListServers;
use App\Filament\Admin\Resources\Servers\RelationManagers\AllocationsRelationManager;
use App\Filament\Admin\Resources\Servers\RelationManagers\DatabasesRelationManager;
use App\Models\Mount;
use App\Models\Server;
use App\Traits\Filament\CanCustomizePages;
@@ -86,6 +87,7 @@ class ServerResource extends Resource
{
return [
AllocationsRelationManager::class,
DatabasesRelationManager::class,
];
}

View File

@@ -299,11 +299,15 @@ class UserResource extends Resource
$id = $schema->getId();
$name = $schema->getName();
$color = $schema->getHexColor();
$color = is_string($color) ? Color::hex($color) : null;
$actions[] = Action::make("oauth_$id")
->label(trans('profile.unlink', ['name' => $name]))
->icon('tabler-unlink')
->requiresConfirmation()
->color(Color::hex($schema->getHexColor()))
->color($color)
->action(function ($livewire) use ($oauthService, $user, $name, $schema) {
$oauthService->unlinkUser($user, $schema);
$livewire->form->fill($user->attributesToArray());

View File

@@ -83,6 +83,8 @@ class StartupVariable extends Field
$this->variableDefault(fn (Get $get) => $get('default_value'));
$this->variableRules(fn (Get $get) => $get('rules'));
$this->disabled(fn (Get $get) => !$get('user_editable'));
return $this;
}
@@ -94,6 +96,8 @@ class StartupVariable extends Field
$this->variableDefault(fn (?ServerVariable $record) => $record?->variable->default_value);
$this->variableRules(fn (?ServerVariable $record) => $record?->variable->rules);
$this->disabled(fn (?ServerVariable $record) => !$record?->variable->user_editable);
return $this;
}

View File

@@ -37,13 +37,20 @@ use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\View;
use Filament\Schemas\Schema;
use Filament\Support\Colors\Color;
use Filament\Support\Enums\IconSize;
use Filament\Support\Enums\Width;
use Illuminate\Database\Eloquent\Builder;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Table;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage;
// Filament Tables imports
use Illuminate\Support\HtmlString;
use Illuminate\Validation\Rules\Password;
use Laravel\Socialite\Facades\Socialite;
@@ -52,10 +59,11 @@ use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
/**
* @method User getUser()
*/
class EditProfile extends BaseEditProfile
class EditProfile extends BaseEditProfile implements HasTable
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
use InteractsWithTable;
protected OAuthService $oauthService;
@@ -158,15 +166,22 @@ class EditProfile extends BaseEditProfile
if ($fileUpload->getDisk()->exists($path)) {
return $path;
}
return null; // explicit return to satisfy static analysis
})
->deleteUploadedFileUsing(function (FileUpload $fileUpload, $file) {
if ($file instanceof TemporaryUploadedFile) {
return $file->delete();
// Ensure we return a boolean even if delete() doesn't return one
$file->delete();
return true;
}
if ($fileUpload->getDisk()->exists($file)) {
return $fileUpload->getDisk()->delete($file);
}
return false; // explicit return
}),
]),
Tab::make('oauth')
@@ -181,12 +196,15 @@ class EditProfile extends BaseEditProfile
$id = $schema->getId();
$name = $schema->getName();
$color = $schema->getHexColor();
$color = is_string($color) ? Color::hex($color) : null;
$unlink = array_key_exists($id, $this->getUser()->oauth ?? []);
$actions[] = Action::make("oauth_$id")
->label(trans('profile.' . ($unlink ? 'unlink' : 'link'), ['name' => $name]))
->icon($unlink ? 'tabler-unlink' : 'tabler-link')
->color(Color::hex($schema->getHexColor()))
->color($color)
->action(function (UserUpdateService $updateService) use ($id, $name, $unlink) {
if ($unlink) {
$oauth = user()?->oauth;
@@ -409,19 +427,8 @@ class EditProfile extends BaseEditProfile
->label(trans('profile.tabs.activity'))
->icon('tabler-history')
->schema([
Repeater::make('activity')
->hiddenLabel()
->inlineLabel(false)
->deletable(false)
->addable(false)
->relationship(null, function (Builder $query) {
$query->orderBy('timestamp', 'desc');
})
->schema([
TextEntry::make('log')
->hiddenLabel()
->state(fn (ActivityLog $log) => new HtmlString($log->htmlable())),
]),
View::make('filament.pages.profile.activity-table')
->columnSpanFull(),
]),
Tab::make('customization')
->label(trans('profile.tabs.customization'))
@@ -545,6 +552,64 @@ class EditProfile extends BaseEditProfile
->inlineLabel(!static::isSimple());
}
/**
* Configure the Filament Table that will be rendered in the Activity tab.
*/
public function table(Table $table): Table
{
$user = $this->getUser();
return $table
->query(fn () => ActivityLog::query()
->with('actor')
->whereHas('subjects', fn ($q) => $q->where('subject_id', $user->getKey())->where('subject_type', $user->getMorphClass()))
->orderBy('timestamp', 'desc')
)
->columns([
ImageColumn::make('avatar')
->label('')
->getStateUsing(fn ($record) => Filament::getUserAvatarUrl($record->actor ?? $user))
->circular()
->imageWidth(50)->imageHeight(50)
->toggleable()
->alignCenter(),
TextColumn::make('timestamp')
->label(trans('profile.activity.timestamp'))
->tooltip(fn ($state) => $state->format('M j, Y g:ia'))
->formatStateUsing(fn ($state) => $state->diffForHumans())
->toggleable(),
TextColumn::make('actor.username')
->label(trans('profile.activity.actor'))
->formatStateUsing(fn ($state, $record) => $record->actor?->username ?? trans('activity.system'))
->toggleable()
->searchable(),
TextColumn::make('ip')
->label(trans('profile.activity.ip'))
->visible(fn () => user()?->can('seeIps activityLog'))
->toggleable()
->searchable(),
TextColumn::make('event')
->label(trans('profile.activity.event'))
->toggleable()
->toggledHiddenByDefault(true)
->searchable(),
TextColumn::make('properties')
->label(trans('activity.metadata'))
->formatStateUsing(function ($state, $record) {
$label = strip_tags(($record->getLabel() ?? ''));
if (trim($label) !== '') {
return $label;
}
})
->limit(200)
->wrap()
->searchable(),
])
->defaultSort('timestamp', 'desc')
->paginated();
}
protected function getFormActions(): array
{
return [];

View File

@@ -76,10 +76,13 @@ class Login extends BaseLogin
$id = $schema->getId();
$color = $schema->getHexColor();
$color = is_string($color) ? Color::hex($color) : null;
$actions[] = Action::make("oauth_$id")
->label($schema->getName())
->icon($schema->getIcon())
->color(Color::hex($schema->getHexColor()))
->color($color)
->url(route('auth.oauth.redirect', ['driver' => $id], false));
}

View File

@@ -44,7 +44,7 @@ abstract class ServerFormPage extends Page
protected function authorizeAccess(): void {}
protected function fillform(): void
protected function fillForm(): void
{
$data = $this->getRecord()->attributesToArray();

View File

@@ -147,12 +147,15 @@ class Startup extends ServerFormPage
return parent::canAccess() && user()?->can(SubuserPermission::StartupRead, Filament::getTenant());
}
public function update(?string $state, ServerVariable $serverVariable): null
public function update(?string $state, ServerVariable $serverVariable): void
{
if (!$serverVariable->variable->user_editable) {
return;
}
$original = $serverVariable->variable_value;
try {
$validator = Validator::make(
['variable_value' => $state],
['variable_value' => $serverVariable->variable->rules]
@@ -165,7 +168,7 @@ class Startup extends ServerFormPage
->danger()
->send();
return null;
return;
}
ServerVariable::query()->updateOrCreate([
@@ -184,6 +187,7 @@ class Startup extends ServerFormPage
])
->log();
}
Notification::make()
->title(trans('server/startup.update', ['variable' => $serverVariable->variable->name]))
->body(fn () => $original . ' -> ' . $state)
@@ -196,8 +200,6 @@ class Startup extends ServerFormPage
->danger()
->send();
}
return null;
}
public function getTitle(): string

View File

@@ -98,7 +98,7 @@ class DatabaseResource extends Resource
->label(trans('server/database.remote')),
TextInput::make('max_connections')
->label(trans('server/database.max_connections'))
->formatStateUsing(fn (Database $database) => $database->max_connections === 0 ? $database->max_connections : 'Unlimited'),
->formatStateUsing(fn (Database $database) => $database->max_connections ?: trans('server/database.unlimited')),
TextInput::make('jdbc')
->label(trans('server/database.jdbc'))
->password()->revealable()

View File

@@ -83,15 +83,21 @@ class BackupController extends ClientApiController
// how best to allow a user to create a backup that is locked without also preventing
// them from just filling up a server with backups that can never be deleted?
if ($request->user()->can(SubuserPermission::BackupDelete, $server)) {
$action->setIsLocked((bool) $request->input('is_locked'));
$action->setIsLocked($request->boolean('is_locked'));
}
$backup = $action->handle($server, $request->input('name'));
$backup = Activity::event('server:backup.start')->transaction(function ($log) use ($action, $server, $request) {
$server->backups()->lockForUpdate();
Activity::event('server:backup.start')
->subject($backup)
->property(['name' => $backup->name, 'locked' => (bool) $request->input('is_locked')])
->log();
$backup = $action->handle($server, $request->input('name'));
$log->subject($backup)->property([
'name' => $backup->name,
'locked' => $request->boolean('is_locked'),
]);
return $backup;
});
return $this->fractal->item($backup)
->transformWith($this->getTransformer(BackupTransformer::class))

View File

@@ -59,12 +59,15 @@ class DatabaseController extends ClientApiController
*/
public function store(StoreDatabaseRequest $request, Server $server): array
{
$database = $this->deployDatabaseService->handle($server, $request->validated());
$database = Activity::event('server:database.create')->transaction(function ($log) use ($request, $server) {
$server->databases()->lockForUpdate();
Activity::event('server:database.create')
->subject($database)
->property('name', $database->database)
->log();
$database = $this->deployDatabaseService->handle($server, $request->validated());
$log->subject($database)->property('name', $database->database);
return $database;
});
return $this->fractal->item($database)
->parseIncludes(['password'])

View File

@@ -107,16 +107,19 @@ class NetworkAllocationController extends ClientApiController
*/
public function store(NewAllocationRequest $request, Server $server): array
{
if ($server->allocations()->count() >= $server->allocation_limit) {
throw new DisplayException('Cannot assign additional allocations to this server: limit has been reached.');
}
$allocation = Activity::event('server:allocation.create')->transaction(function ($log) use ($server) {
$server->allocations()->lockForUpdate();
$allocation = $this->assignableAllocationService->handle($server);
if ($server->allocations->count() >= $server->allocation_limit) {
throw new DisplayException('Cannot assign additional allocations to this server: limit has been reached.');
}
Activity::event('server:allocation.create')
->subject($allocation)
->property('allocation', $allocation->address)
->log();
$allocation = $this->assignableAllocationService->handle($server);
$log->subject($allocation)->property('allocation', $allocation->address);
return $allocation;
});
return $this->fractal->item($allocation)
->transformWith($this->getTransformer(AllocationTransformer::class))

View File

@@ -131,7 +131,7 @@ class BackupRemoteUploadController extends Controller
*/
private function getConfiguredMaxPartSize(): int
{
$maxPartSize = (int) config('backups.max_part_size', self::DEFAULT_MAX_PART_SIZE);
$maxPartSize = config('backups.max_part_size', self::DEFAULT_MAX_PART_SIZE);
if ($maxPartSize <= 0) {
$maxPartSize = self::DEFAULT_MAX_PART_SIZE;
}

View File

@@ -79,13 +79,13 @@ class OAuthController extends Controller
$user = User::whereEmail($email)->first();
if ($user) {
if (!$driver->shouldLinkMissingUsers()) {
if (!$driver->shouldLinkMissingUser($user, $oauthUser)) {
return $this->errorRedirect();
}
$user = $this->oauthService->linkUser($user, $driver, $oauthUser);
} else {
if (!$driver->shouldCreateMissingUsers()) {
if (!$driver->shouldCreateMissingUser($oauthUser)) {
return $this->errorRedirect();
}

33
app/Jobs/InstallEgg.php Normal file
View File

@@ -0,0 +1,33 @@
<?php
namespace App\Jobs;
use App\Services\Eggs\Sharing\EggImporterService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Throwable;
class InstallEgg implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $timeout = 15;
public function __construct(public string $downloadUrl) {}
/**
* @throws Throwable
*/
public function handle(EggImporterService $eggImporterService): void
{
try {
$eggImporterService->fromUrl($this->downloadUrl);
} catch (Throwable $e) {
Log::error('Failed to install egg from URL: ' . $this->downloadUrl, ['exception' => $e]);
}
}
}

View File

@@ -2,8 +2,10 @@
namespace App\Livewire\Installer;
use App\Jobs\InstallEgg;
use App\Livewire\Installer\Steps\CacheStep;
use App\Livewire\Installer\Steps\DatabaseStep;
use App\Livewire\Installer\Steps\EggSelectionStep;
use App\Livewire\Installer\Steps\EnvironmentStep;
use App\Livewire\Installer\Steps\QueueStep;
use App\Livewire\Installer\Steps\RequirementsStep;
@@ -53,7 +55,7 @@ class PanelInstaller extends SimplePage implements HasForms
public function getMaxContentWidth(): Width|string
{
return Width::SevenExtraLarge;
return Width::ScreenTwoExtraLarge;
}
public static function isInstalled(): bool
@@ -82,6 +84,7 @@ class PanelInstaller extends SimplePage implements HasForms
RequirementsStep::make(),
EnvironmentStep::make($this),
DatabaseStep::make($this),
EggSelectionStep::make(),
CacheStep::make($this),
QueueStep::make($this),
SessionStep::make(),
@@ -99,7 +102,7 @@ class PanelInstaller extends SimplePage implements HasForms
wire:loading.attr="disabled"
>
{{ trans('installer.finish') }}
<span wire:loading><x-filament::loading-indicator class="h-4 w-4" /></span>
<x-filament::loading-indicator wire:loading class="h-4 w-4" />
</x-filament::button>
BLADE))),
];
@@ -141,6 +144,9 @@ class PanelInstaller extends SimplePage implements HasForms
// Write session data at the very end to avoid "page expired" errors
$this->writeToEnv('env_session');
// Install selected eggs
$this->installEggs();
// Redirect to admin panel
$this->redirect(Filament::getPanel('admin')->getUrl());
} catch (Halt) {
@@ -165,8 +171,6 @@ class PanelInstaller extends SimplePage implements HasForms
throw new Halt(trans('installer.exceptions.write_env'));
}
Artisan::call('config:clear');
}
public function runMigrations(): void
@@ -220,4 +224,36 @@ class PanelInstaller extends SimplePage implements HasForms
throw new Halt(trans('installer.exceptions.create_user'));
}
}
public function installEggs(): void
{
try {
$selectedEggs = array_get($this->data, 'eggs', []);
if (!$selectedEggs) {
return;
}
foreach ($selectedEggs as $category => $eggs) {
foreach ($eggs as $downloadUrl) {
InstallEgg::dispatch($downloadUrl);
}
}
Notification::make()
->title(trans('installer.egg.background_install_started'))
->body(trans('installer.egg.background_install_description', ['count' => array_sum(array_map('count', $selectedEggs))]))
->success()
->persistent()
->send();
} catch (Exception $exception) {
report($exception);
Notification::make()
->title(trans('installer.egg.exceptions.installation_failed'))
->body($exception->getMessage())
->danger()
->persistent()
->send();
}
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace App\Livewire\Installer\Steps;
use App\Console\Commands\Egg\UpdateEggIndexCommand;
use Exception;
use Filament\Forms\Components\CheckboxList;
use Filament\Infolists\Components\TextEntry;
use Filament\Notifications\Notification;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Artisan;
class EggSelectionStep
{
public static function make(): Step
{
try {
Artisan::call(UpdateEggIndexCommand::class);
} catch (Exception $exception) {
Notification::make()
->title(trans('installer.egg.exceptions.failed_to_update'))
->icon('tabler-egg')
->body($exception->getMessage())
->danger()
->persistent()
->send();
}
$eggs = cache()->get('eggs.index', []);
$categories = array_keys($eggs);
$tabs = array_map(function (string $label) use ($eggs) {
$id = str_slug($label, '_');
$eggCount = count($eggs[$label]);
return Tab::make($id)
->label($label)
->badge($eggCount)
->schema([
CheckboxList::make("eggs.$id")
->hiddenLabel()
->options(fn () => array_sort($eggs[$label]))
->searchable($eggCount > 0)
->bulkToggleable($eggCount > 0)
->columns(4),
]);
}, $categories);
if (empty($tabs)) {
$tabs[] = Tab::make('no_eggs')
->label(trans('installer.egg.no_eggs'))
->schema([
TextEntry::make('no_eggs')
->hiddenLabel()
->state(trans('installer.egg.exceptions.no_eggs')),
]);
}
return Step::make('egg')
->label(trans('installer.egg.title'))
->columnSpanFull()
->schema([
Tabs::make('egg_tabs')
->tabs($tabs),
]);
}
}

View File

@@ -168,33 +168,33 @@ class ActivityLog extends Model implements HasIcon, HasLabel
return user()?->can('seeIps activityLog') ? $this->ip : null;
}
public function htmlable(): string
{
$user = $this->actor;
if (!$user instanceof User) {
$user = new User([
'email' => 'system@pelican.dev',
'username' => 'system',
]);
}
$avatarUrl = Filament::getUserAvatarUrl($user);
$username = str($user->username)->stripTags();
$ip = $this->getIp();
$ip = $ip ? $ip . ' — ' : '';
return "
<div style='display: flex; align-items: center;'>
<img width='50px' height='50px' src='{$avatarUrl}' style='margin-right: 15px; border-radius: 50%;' />
<div>
<p>$username$this->event</p>
<p>{$this->getLabel()}</p>
<p>$ip<span title='{$this->timestamp->format('M j, Y g:ia')}'>{$this->timestamp->diffForHumans()}</span></p>
</div>
</div>
";
}
// public function htmlable(): string
// {
// $user = $this->actor;
// if (!$user instanceof User) {
// $user = new User([
// 'email' => 'system@pelican.dev',
// 'username' => 'system',
// ]);
// }
//
// $avatarUrl = Filament::getUserAvatarUrl($user);
// $username = str($user->username)->stripTags();
// $ip = $this->getIp();
// $ip = $ip ? $ip . ' — ' : '';
//
// return "
// <div style='display: flex; align-items: center;'>
// <img width='50px' height='50px' src='{$avatarUrl}' style='margin-right: 15px; border-radius: 50%;' />
//
// <div>
// <p>$username — $this->event</p>
// <p>{$this->getLabel()}</p>
// <p>$ip<span title='{$this->timestamp->format('M j, Y g:ia')}'>{$this->timestamp->diffForHumans()}</span></p>
// </div>
// </div>
// ";
// }
/**
* @return array<string, string>

View File

@@ -19,7 +19,7 @@ use PDOException;
* @property string $username
* @property string $remote
* @property string $password
* @property int $max_connections
* @property ?int $max_connections
* @property string $jdbc
* @property Carbon $created_at
* @property Carbon $updated_at

View File

@@ -110,7 +110,7 @@ class Node extends Model implements Validatable
'daemon_listen' => ['required', 'numeric', 'between:1,65535'],
'daemon_connect' => ['required', 'numeric', 'between:1,65535'],
'maintenance_mode' => ['boolean'],
'upload_size' => ['int', 'between:1,1024'],
'upload_size' => ['int', 'min:1'],
'tags' => ['array'],
];

View File

@@ -205,7 +205,7 @@ class Plugin extends Model implements HasPluginSettings
public function canDisable(): bool
{
return $this->status !== PluginStatus::Disabled && $this->status !== PluginStatus::NotInstalled && $this->isCompatible();
return $this->status === PluginStatus::Enabled || $this->status === PluginStatus::Incompatible;
}
public function isCompatible(): bool
@@ -306,11 +306,10 @@ class Plugin extends Model implements HasPluginSettings
public function hasSettings(): bool
{
try {
$pluginObject = filament($this->id);
$pluginObject = new ($this->fullClass());
return $pluginObject instanceof HasPluginSettings;
} catch (Exception) {
// Plugin is not loaded on the current panel, so no settings
}
return false;
@@ -320,13 +319,12 @@ class Plugin extends Model implements HasPluginSettings
public function getSettingsForm(): array
{
try {
$pluginObject = filament($this->id);
$pluginObject = new ($this->fullClass());
if ($pluginObject instanceof HasPluginSettings) {
return $pluginObject->getSettingsForm();
}
} catch (Exception) {
// Plugin is not loaded on the current panel, so no settings
}
return [];
@@ -336,13 +334,12 @@ class Plugin extends Model implements HasPluginSettings
public function saveSettings(array $data): void
{
try {
$pluginObject = filament($this->id);
$pluginObject = new ($this->fullClass());
if ($pluginObject instanceof HasPluginSettings) {
$pluginObject->saveSettings($data);
}
} catch (Exception) {
// Plugin is not loaded on the current panel, so no settings
}
}

View File

@@ -39,6 +39,7 @@ use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Context;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\In;
use ResourceBundle;
@@ -215,7 +216,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
{
$rules = self::getValidationRules();
$rules['language'][] = new In(array_values(array_filter(ResourceBundle::getLocales(''), fn ($lang) => preg_match('/^[a-z]{2}$/', $lang))));
$rules['language'][] = new In(ResourceBundle::getLocales(''));
$rules['timezone'][] = new In(DateTimeZone::listIdentifiers());
return $rules;
@@ -333,12 +334,8 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
return !$key ? $customization : $customization[$key->value];
}
protected function checkPermission(Server $server, string|SubuserPermission $permission = ''): bool
protected function hasPermission(Server $server, string $permission = ''): bool
{
if ($permission instanceof SubuserPermission) {
$permission = $permission->value;
}
if ($this->canned('update', $server) || $server->owner_id === $this->id) {
return true;
}
@@ -356,6 +353,17 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
return in_array($permission, $subuser->permissions);
}
protected function checkPermission(Server $server, string|SubuserPermission $permission = ''): bool
{
if ($permission instanceof SubuserPermission) {
$permission = $permission->value;
}
$contextKey = "users.$this->id.servers.$server->id.$permission";
return Context::remember($contextKey, fn () => $this->hasPermission($server, $permission));
}
/**
* Laravel's policies strictly check for the existence of a real method,
* this checks if the ability is one of our permissions and then checks if the user can do it or not

View File

@@ -2,6 +2,7 @@
namespace App\Providers;
use App\Enums\ResourceLimit;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Http\Middleware\TrimStrings;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
@@ -98,5 +99,7 @@ class RouteServiceProvider extends ServiceProvider
config('http.rate_limit.application')
)->by($key);
});
ResourceLimit::boot();
}
}

View File

@@ -31,11 +31,11 @@ class DaemonFileRepository extends DaemonRepository
throw new FileSizeTooLargeException();
}
if ($response->getStatusCode() === 400) {
if ($response->status() === 400) {
throw new FileNotEditableException();
}
if ($response->getStatusCode() === 404) {
if ($response->status() === 404) {
throw new FileNotFoundException();
}
@@ -56,7 +56,7 @@ class DaemonFileRepository extends DaemonRepository
->withBody($content)
->post("/api/servers/{$this->server->uuid}/files/write");
if ($response->getStatusCode() === 400) {
if ($response->status() === 400) {
throw new FileExistsException();
}
@@ -92,7 +92,7 @@ class DaemonFileRepository extends DaemonRepository
]
);
if ($response->getStatusCode() === 400) {
if ($response->status() === 400) {
throw new FileExistsException();
}

View File

@@ -133,6 +133,9 @@ class DaemonServerRepository extends DaemonRepository
* make it easier to revoke tokens on the fly. This ensures that the JTI key is formatted
* correctly and avoids any costly mistakes in the codebase.
*
* @deprecated
* @see self::deauthorize()
*
* @throws ConnectionException
*/
public function revokeUserJTI(int $id): void
@@ -143,6 +146,21 @@ class DaemonServerRepository extends DaemonRepository
]);
}
/**
* Deauthorizes a user (disconnects websockets and SFTP) on the Wings instance for the server.
*
* @throws ConnectionException
*/
public function deauthorize(string $user): void
{
$this->getHttpClient()->post('/api/deauthorize-user', [
'json' => [
'user' => $user,
'servers' => [$this->server->uuid],
],
]);
}
public function getInstallLogs(): string
{
return $this->getHttpClient()

View File

@@ -7,17 +7,21 @@ use App\Exceptions\Service\InvalidFileUploadException;
use App\Models\Plugin;
use Composer\Autoload\ClassLoader;
use Exception;
use Filament\Facades\Filament;
use Filament\Panel;
use Illuminate\Console\Application as ConsoleApplication;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Migrations\Migrator;
use Illuminate\Foundation\Application;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
use JsonException;
use Spatie\TemporaryDirectory\TemporaryDirectory;
use ZipArchive;
@@ -157,6 +161,8 @@ class PluginService
/**
* @param null|array<string, string> $newPackages
* @param null|array<string, string> $oldPackages
*
* @throws Exception
*/
public function manageComposerPackages(?array $newPackages = [], ?array $oldPackages = null): void
{
@@ -208,48 +214,55 @@ class PluginService
}
}
/** @throws Exception */
public function runPluginMigrations(Plugin $plugin): void
{
$migrations = plugin_path($plugin->id, 'database', 'migrations');
if (file_exists($migrations)) {
$success = Artisan::call('migrate', ['--realpath' => true, '--path' => $migrations, '--force' => true]) === 0;
if (!$success) {
throw new Exception("Could not run migrations for plugin '{$plugin->id}'");
try {
$migrator = $this->app->make(Migrator::class);
$migrator->run($migrations);
} catch (Exception $exception) {
throw new Exception("Could not run migrations': " . $exception->getMessage());
}
}
}
/** @throws Exception */
public function rollbackPluginMigrations(Plugin $plugin): void
{
$migrations = plugin_path($plugin->id, 'database', 'migrations');
if (file_exists($migrations)) {
$success = Artisan::call('migrate:rollback', ['--realpath' => true, '--path' => $migrations, '--force' => true]) === 0;
if (!$success) {
throw new Exception("Could not rollback migrations for plugin '{$plugin->id}'");
try {
$migrator = $this->app->make(Migrator::class);
$migrator->reset($migrations);
} catch (Exception $exception) {
throw new Exception("Could not rollback migrations': " . $exception->getMessage());
}
}
}
/** @throws Exception */
public function runPluginSeeder(Plugin $plugin): void
{
$seeder = $plugin->getSeeder();
if ($seeder) {
$success = Artisan::call('db:seed', ['--class' => $seeder, '--force' => true]) === 0;
try {
$seederObject = $this->app->make($seeder)->setContainer($this->app);
if (!$success) {
throw new Exception("Could not run seeder for plugin '{$plugin->id}'");
Model::unguarded(fn () => $seederObject->__invoke());
} catch (Exception $exception) {
throw new Exception('Could not run seeder: ' . $exception->getMessage());
}
}
}
public function buildAssets(): bool
public function buildAssets(bool $throw = false): bool
{
try {
$result = Process::path(base_path())->timeout(300)->run('yarn install');
if ($result->failed()) {
throw new Exception('Could not install dependencies: ' . $result->errorOutput());
throw new Exception('Could not install yarn dependencies: ' . $result->errorOutput());
}
$result = Process::path(base_path())->timeout(600)->run('yarn build');
@@ -259,16 +272,17 @@ class PluginService
return true;
} catch (Exception $exception) {
if ($this->isDevModeActive()) {
if ($throw || $this->isDevModeActive()) {
throw ($exception);
}
report($exception);
Log::warning($exception->getMessage(), ['exception' => $exception]);
}
return false;
}
/** @throws Exception */
public function installPlugin(Plugin $plugin, bool $enable = true): void
{
try {
@@ -282,32 +296,40 @@ class PluginService
}
}
$this->buildAssets();
$this->buildAssets($plugin->isTheme());
$this->runPluginMigrations($plugin);
$this->runPluginSeeder($plugin);
foreach (Filament::getPanels() as $panel) {
$panel->clearCachedComponents();
}
} catch (Exception $exception) {
$this->handlePluginException($plugin, $exception);
$this->handlePluginException($plugin, $exception, true);
}
}
/** @throws Exception */
public function updatePlugin(Plugin $plugin): void
{
try {
$downloadUrl = $plugin->getDownloadUrlForUpdate();
if ($downloadUrl) {
$this->downloadPluginFromUrl($downloadUrl, true);
$this->installPlugin($plugin, false);
cache()->forget("plugins.$plugin->id.update");
if (!$downloadUrl) {
throw new Exception('No download url found.');
}
$this->downloadPluginFromUrl($downloadUrl, true);
$this->installPlugin($plugin, false);
cache()->forget("plugins.$plugin->id.update");
} catch (Exception $exception) {
$this->handlePluginException($plugin, $exception);
$this->handlePluginException($plugin, $exception, true);
}
}
/** @throws Exception */
public function uninstallPlugin(Plugin $plugin, bool $deleteFiles = false): void
{
try {
@@ -324,11 +346,17 @@ class PluginService
$this->buildAssets();
$this->manageComposerPackages(oldPackages: $pluginPackages);
// This throws an error when not called with qualifier
foreach (\Filament\Facades\Filament::getPanels() as $panel) {
$panel->clearCachedComponents();
}
} catch (Exception $exception) {
$this->handlePluginException($plugin, $exception);
$this->handlePluginException($plugin, $exception, true);
}
}
/** @throws Exception */
public function downloadPluginFromFile(UploadedFile $file, bool $cleanDownload = false): void
{
// Validate file size to prevent zip bombs
@@ -368,6 +396,7 @@ class PluginService
$zip->close();
}
/** @throws Exception */
public function downloadPluginFromUrl(string $url, bool $cleanDownload = false): void
{
$info = pathinfo($url);
@@ -429,7 +458,11 @@ class PluginService
]);
}
/** @param array<int, string> $order */
/**
* @param array<int, string> $order
*
* @throws JsonException
*/
public function updateLoadOrder(array $order): void
{
foreach ($order as $i => $plugin) {
@@ -473,14 +506,14 @@ class PluginService
return config('panel.plugin.dev_mode', false);
}
private function handlePluginException(string|Plugin $plugin, Exception $exception): void
private function handlePluginException(string|Plugin $plugin, Exception $exception, bool $throw = false): void
{
if ($this->isDevModeActive()) {
$this->setStatus($plugin, PluginStatus::Errored, $exception->getMessage());
if ($throw || $this->isDevModeActive()) {
throw ($exception);
}
report($exception);
$this->setStatus($plugin, PluginStatus::Errored, $exception->getMessage());
}
}

View File

@@ -48,7 +48,7 @@ class DetailsModificationService
// websockets.
if ($server->owner_id !== $owner) {
try {
$this->serverRepository->setServer($server)->revokeUserJTI($owner);
$this->serverRepository->setServer($server)->deauthorize($server->user->uuid);
} catch (ConnectionException) {
// Do nothing. A failure here is not ideal, but it is likely to be caused by daemon
// being offline, or in an entirely broken state. Remember, these tokens reset every

View File

@@ -78,7 +78,10 @@ class ServerDeletionService
}
}
$server->allocations()->update(['server_id' => null]);
$server->allocations()->update([
'server_id' => null,
'notes' => null,
]);
$server->delete();
});

View File

@@ -28,7 +28,7 @@ class SubuserDeletionService
event(new SubUserRemoved($subuser->server, $subuser->user));
try {
$this->serverRepository->setServer($server)->revokeUserJTI($subuser->user_id);
$this->serverRepository->setServer($server)->deauthorize($subuser->user->uuid);
} catch (ConnectionException $exception) {
// Don't block this request if we can't connect to the daemon instance.
logger()->warning($exception, ['user_id' => $subuser->user_id, 'server_id' => $server->id]);

View File

@@ -46,7 +46,7 @@ class SubuserUpdateService
$subuser->update(['permissions' => $cleanedPermissions]);
try {
$this->serverRepository->setServer($server)->revokeUserJTI($subuser->user_id);
$this->serverRepository->setServer($server)->deauthorize($subuser->user->uuid);
} catch (ConnectionException $exception) {
// Don't block this request if we can't connect to the daemon instance. Chances are it is
// offline and the token will be invalid once daemon boots back.

View File

@@ -3,6 +3,7 @@
namespace App\Traits;
use Illuminate\Support\Env;
use Illuminate\Support\Facades\Artisan;
use RuntimeException;
trait EnvironmentWriterTrait
@@ -17,5 +18,6 @@ trait EnvironmentWriterTrait
public function writeToEnvironment(array $values = []): void
{
Env::writeVariables($values, base_path('.env'), true);
Artisan::call('config:clear');
}
}

View File

@@ -24,6 +24,6 @@ trait CanCustomizePages
/** @return array<string, PageRegistration> */
public static function getPages(): array
{
return array_unique(array_merge(static::getDefaultPages(), static::$customPages), SORT_REGULAR);
return array_unique(array_merge(static::$customPages, static::getDefaultPages()), SORT_REGULAR);
}
}

View File

@@ -23,6 +23,6 @@ trait CanCustomizeRelations
/** @return class-string<RelationManager>[] */
public static function getRelations(): array
{
return array_unique(array_merge(static::getDefaultRelations(), static::$customRelations));
return array_unique(array_merge(static::$customRelations, static::getDefaultRelations()));
}
}

View File

@@ -44,6 +44,10 @@ return Application::configure(basePath: dirname(__DIR__))
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'node.maintenance' => \App\Http\Middleware\MaintenanceMiddleware::class,
]);
$middleware->priority([
\Illuminate\Routing\Middleware\SubstituteBindings::class,
]);
})
->withSingletons([
\Illuminate\Contracts\Console\Kernel::class => \App\Console\Kernel::class,

View File

@@ -1,4 +1,4 @@
# [Bounties](https://github.com/pelican-dev/panel/issues?q=is%3Aopen+is%3Aissue+label%3A%22%F0%9F%92%B5+bounty%22)
# [Bounties](https://github.com/pelican-dev/panel/issues?q=state%3Aopen%20is%3Aissue%20label%3A%22%F0%9F%92%B0%20fund%22)
Get paid to improve Pelican!
@@ -15,6 +15,6 @@ This is still valuable work, so we'll pay out $50 for getting any bounty closed
## Issue bounties
We've tagged bounty-eligible issues across openpilot and the rest of our repos; check out all the open ones [here](https://github.com/pelican-dev/panel/issues?q=is%3Aopen+is%3Aissue+label%3A%22%F0%9F%92%B5+bounty%22).
We've tagged bounty-eligible issues across openpilot and the rest of our repos; check out all the open ones [here](https://github.com/pelican-dev/panel/issues?q=state%3Aopen%20is%3Aissue%20label%3A%22%F0%9F%92%B0%20fund%22).
New bounties can be proposed in the [**#feedback**](https://discord.com/channels/1218730176297439332/1218732581797892220) channel in Discord.

View File

@@ -3,7 +3,7 @@
"description": "The free, open-source game management panel. Supporting Minecraft, Spigot, BungeeCord, and SRCDS servers.",
"license": "AGPL-3.0-only",
"require": {
"php": "^8.2 || ^8.3 || ^8.4",
"php": "^8.2 || ^8.3 || ^8.4 || ^8.5",
"ext-intl": "*",
"ext-json": "*",
"ext-mbstring": "*",

160
composer.lock generated
View File

@@ -128,16 +128,16 @@
},
{
"name": "aws/aws-sdk-php",
"version": "3.366.4",
"version": "3.369.0",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "1861cc8eede21cdaab0732fd44f43f19ddf1effd"
"reference": "2bbe45aaaaa23a863a5daadcda326cf1c8b4a15b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/1861cc8eede21cdaab0732fd44f43f19ddf1effd",
"reference": "1861cc8eede21cdaab0732fd44f43f19ddf1effd",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/2bbe45aaaaa23a863a5daadcda326cf1c8b4a15b",
"reference": "2bbe45aaaaa23a863a5daadcda326cf1c8b4a15b",
"shasum": ""
},
"require": {
@@ -219,9 +219,9 @@
"support": {
"forum": "https://github.com/aws/aws-sdk-php/discussions",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.366.4"
"source": "https://github.com/aws/aws-sdk-php/tree/3.369.0"
},
"time": "2025-12-09T19:21:22+00:00"
"time": "2025-12-19T19:08:40+00:00"
},
{
"name": "blade-ui-kit/blade-heroicons",
@@ -1944,16 +1944,16 @@
},
{
"name": "gboquizosanchez/filament-log-viewer",
"version": "2.2.0",
"version": "2.2.1",
"source": {
"type": "git",
"url": "https://github.com/gboquizosanchez/filament-log-viewer.git",
"reference": "f92ce531d6a1ba2800658bffa56b4643dfbef5cf"
"reference": "00a31ea3eff8d5ee629c4b6beebe10a503177116"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/gboquizosanchez/filament-log-viewer/zipball/f92ce531d6a1ba2800658bffa56b4643dfbef5cf",
"reference": "f92ce531d6a1ba2800658bffa56b4643dfbef5cf",
"url": "https://api.github.com/repos/gboquizosanchez/filament-log-viewer/zipball/00a31ea3eff8d5ee629c4b6beebe10a503177116",
"reference": "00a31ea3eff8d5ee629c4b6beebe10a503177116",
"shasum": ""
},
"require": {
@@ -2002,9 +2002,9 @@
],
"support": {
"issues": "https://github.com/gboquizosanchez/filament-log-viewer/issues",
"source": "https://github.com/gboquizosanchez/filament-log-viewer/tree/2.2.0"
"source": "https://github.com/gboquizosanchez/filament-log-viewer/tree/2.2.1"
},
"time": "2025-11-22T16:14:10+00:00"
"time": "2025-12-14T18:47:00+00:00"
},
{
"name": "graham-campbell/result-type",
@@ -2481,16 +2481,16 @@
},
{
"name": "kirschbaum-development/eloquent-power-joins",
"version": "4.2.10",
"version": "4.2.11",
"source": {
"type": "git",
"url": "https://github.com/kirschbaum-development/eloquent-power-joins.git",
"reference": "ccda351a75701f5b0a6f94586d9a40f1114302b4"
"reference": "0e3e3372992e4bf82391b3c7b84b435c3db73588"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/kirschbaum-development/eloquent-power-joins/zipball/ccda351a75701f5b0a6f94586d9a40f1114302b4",
"reference": "ccda351a75701f5b0a6f94586d9a40f1114302b4",
"url": "https://api.github.com/repos/kirschbaum-development/eloquent-power-joins/zipball/0e3e3372992e4bf82391b3c7b84b435c3db73588",
"reference": "0e3e3372992e4bf82391b3c7b84b435c3db73588",
"shasum": ""
},
"require": {
@@ -2538,22 +2538,22 @@
],
"support": {
"issues": "https://github.com/kirschbaum-development/eloquent-power-joins/issues",
"source": "https://github.com/kirschbaum-development/eloquent-power-joins/tree/4.2.10"
"source": "https://github.com/kirschbaum-development/eloquent-power-joins/tree/4.2.11"
},
"time": "2025-11-13T14:57:49+00:00"
"time": "2025-12-17T00:37:48+00:00"
},
{
"name": "laravel/framework",
"version": "v12.42.0",
"version": "v12.43.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "509b33095564c5165366d81bbaa0afaac28abe75"
"reference": "195b893593a9298edee177c0844132ebaa02102f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/509b33095564c5165366d81bbaa0afaac28abe75",
"reference": "509b33095564c5165366d81bbaa0afaac28abe75",
"url": "https://api.github.com/repos/laravel/framework/zipball/195b893593a9298edee177c0844132ebaa02102f",
"reference": "195b893593a9298edee177c0844132ebaa02102f",
"shasum": ""
},
"require": {
@@ -2762,7 +2762,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2025-12-09T15:51:23+00:00"
"time": "2025-12-16T18:53:08+00:00"
},
{
"name": "laravel/helpers",
@@ -4264,16 +4264,16 @@
},
{
"name": "livewire/livewire",
"version": "v3.7.1",
"version": "v3.7.3",
"source": {
"type": "git",
"url": "https://github.com/livewire/livewire.git",
"reference": "214da8f3a1199a88b56ab2fe901d4a607f784805"
"reference": "a5384df9fbd3eaf02e053bc49aabc8ace293fc1c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/livewire/livewire/zipball/214da8f3a1199a88b56ab2fe901d4a607f784805",
"reference": "214da8f3a1199a88b56ab2fe901d4a607f784805",
"url": "https://api.github.com/repos/livewire/livewire/zipball/a5384df9fbd3eaf02e053bc49aabc8ace293fc1c",
"reference": "a5384df9fbd3eaf02e053bc49aabc8ace293fc1c",
"shasum": ""
},
"require": {
@@ -4328,7 +4328,7 @@
"description": "A front-end framework for Laravel.",
"support": {
"issues": "https://github.com/livewire/livewire/issues",
"source": "https://github.com/livewire/livewire/tree/v3.7.1"
"source": "https://github.com/livewire/livewire/tree/v3.7.3"
},
"funding": [
{
@@ -4336,7 +4336,7 @@
"type": "github"
}
],
"time": "2025-12-03T22:41:13+00:00"
"time": "2025-12-19T02:00:29+00:00"
},
{
"name": "masterminds/html5",
@@ -5777,16 +5777,16 @@
},
{
"name": "phpseclib/phpseclib",
"version": "3.0.47",
"version": "3.0.48",
"source": {
"type": "git",
"url": "https://github.com/phpseclib/phpseclib.git",
"reference": "9d6ca36a6c2dd434765b1071b2644a1c683b385d"
"reference": "64065a5679c50acb886e82c07aa139b0f757bb89"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/9d6ca36a6c2dd434765b1071b2644a1c683b385d",
"reference": "9d6ca36a6c2dd434765b1071b2644a1c683b385d",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/64065a5679c50acb886e82c07aa139b0f757bb89",
"reference": "64065a5679c50acb886e82c07aa139b0f757bb89",
"shasum": ""
},
"require": {
@@ -5867,7 +5867,7 @@
],
"support": {
"issues": "https://github.com/phpseclib/phpseclib/issues",
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.47"
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.48"
},
"funding": [
{
@@ -5883,7 +5883,7 @@
"type": "tidelift"
}
],
"time": "2025-10-06T01:07:24+00:00"
"time": "2025-12-15T11:51:42+00:00"
},
{
"name": "phpstan/phpdoc-parser",
@@ -6527,16 +6527,16 @@
},
{
"name": "psy/psysh",
"version": "v0.12.16",
"version": "v0.12.18",
"source": {
"type": "git",
"url": "https://github.com/bobthecow/psysh.git",
"reference": "ee6d5028be4774f56c6c2c85ec4e6bc9acfe6b67"
"reference": "ddff0ac01beddc251786fe70367cd8bbdb258196"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/ee6d5028be4774f56c6c2c85ec4e6bc9acfe6b67",
"reference": "ee6d5028be4774f56c6c2c85ec4e6bc9acfe6b67",
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/ddff0ac01beddc251786fe70367cd8bbdb258196",
"reference": "ddff0ac01beddc251786fe70367cd8bbdb258196",
"shasum": ""
},
"require": {
@@ -6600,9 +6600,9 @@
],
"support": {
"issues": "https://github.com/bobthecow/psysh/issues",
"source": "https://github.com/bobthecow/psysh/tree/v0.12.16"
"source": "https://github.com/bobthecow/psysh/tree/v0.12.18"
},
"time": "2025-12-07T03:39:01+00:00"
"time": "2025-12-17T14:35:46+00:00"
},
{
"name": "ralouphie/getallheaders",
@@ -6726,20 +6726,20 @@
},
{
"name": "ramsey/uuid",
"version": "4.9.1",
"version": "4.9.2",
"source": {
"type": "git",
"url": "https://github.com/ramsey/uuid.git",
"reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440"
"reference": "8429c78ca35a09f27565311b98101e2826affde0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/81f941f6f729b1e3ceea61d9d014f8b6c6800440",
"reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0",
"reference": "8429c78ca35a09f27565311b98101e2826affde0",
"shasum": ""
},
"require": {
"brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14",
"brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14",
"php": "^8.0",
"ramsey/collection": "^1.2 || ^2.0"
},
@@ -6798,9 +6798,9 @@
],
"support": {
"issues": "https://github.com/ramsey/uuid/issues",
"source": "https://github.com/ramsey/uuid/tree/4.9.1"
"source": "https://github.com/ramsey/uuid/tree/4.9.2"
},
"time": "2025-09-04T20:59:21+00:00"
"time": "2025-12-14T04:43:48+00:00"
},
{
"name": "ryangjchandler/blade-capture-directive",
@@ -7015,16 +7015,16 @@
},
{
"name": "secondnetwork/blade-tabler-icons",
"version": "v3.35.0",
"version": "v3.36.0",
"source": {
"type": "git",
"url": "https://github.com/secondnetwork/blade-tabler-icons.git",
"reference": "637a4ae739daa13a27522fe91dd36f0716321616"
"reference": "ad5bb5dc6d609c27dd6c00d2c0edf29cc220581f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/secondnetwork/blade-tabler-icons/zipball/637a4ae739daa13a27522fe91dd36f0716321616",
"reference": "637a4ae739daa13a27522fe91dd36f0716321616",
"url": "https://api.github.com/repos/secondnetwork/blade-tabler-icons/zipball/ad5bb5dc6d609c27dd6c00d2c0edf29cc220581f",
"reference": "ad5bb5dc6d609c27dd6c00d2c0edf29cc220581f",
"shasum": ""
},
"require": {
@@ -7067,9 +7067,9 @@
],
"support": {
"issues": "https://github.com/secondnetwork/blade-tabler-icons/issues",
"source": "https://github.com/secondnetwork/blade-tabler-icons/tree/v3.35.0"
"source": "https://github.com/secondnetwork/blade-tabler-icons/tree/v3.36.0"
},
"time": "2025-09-18T12:39:56+00:00"
"time": "2025-12-17T16:01:09+00:00"
},
{
"name": "socialiteproviders/authentik",
@@ -7813,16 +7813,16 @@
},
{
"name": "spatie/laravel-permission",
"version": "6.23.0",
"version": "6.24.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-permission.git",
"reference": "9e41247bd512b1e6c229afbc1eb528f7565ae3bb"
"reference": "76adb1fc8d07c16a0721c35c4cc330b7a12598d7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-permission/zipball/9e41247bd512b1e6c229afbc1eb528f7565ae3bb",
"reference": "9e41247bd512b1e6c229afbc1eb528f7565ae3bb",
"url": "https://api.github.com/repos/spatie/laravel-permission/zipball/76adb1fc8d07c16a0721c35c4cc330b7a12598d7",
"reference": "76adb1fc8d07c16a0721c35c4cc330b7a12598d7",
"shasum": ""
},
"require": {
@@ -7884,7 +7884,7 @@
],
"support": {
"issues": "https://github.com/spatie/laravel-permission/issues",
"source": "https://github.com/spatie/laravel-permission/tree/6.23.0"
"source": "https://github.com/spatie/laravel-permission/tree/6.24.0"
},
"funding": [
{
@@ -7892,7 +7892,7 @@
"type": "github"
}
],
"time": "2025-11-03T20:16:13+00:00"
"time": "2025-12-13T21:45:21+00:00"
},
{
"name": "spatie/laravel-query-builder",
@@ -11287,23 +11287,23 @@
},
{
"name": "tijsverkoyen/css-to-inline-styles",
"version": "v2.3.0",
"version": "v2.4.0",
"source": {
"type": "git",
"url": "https://github.com/tijsverkoyen/CssToInlineStyles.git",
"reference": "0d72ac1c00084279c1816675284073c5a337c20d"
"reference": "f0292ccf0ec75843d65027214426b6b163b48b41"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/0d72ac1c00084279c1816675284073c5a337c20d",
"reference": "0d72ac1c00084279c1816675284073c5a337c20d",
"url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41",
"reference": "f0292ccf0ec75843d65027214426b6b163b48b41",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-libxml": "*",
"php": "^7.4 || ^8.0",
"symfony/css-selector": "^5.4 || ^6.0 || ^7.0"
"symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
@@ -11336,9 +11336,9 @@
"homepage": "https://github.com/tijsverkoyen/CssToInlineStyles",
"support": {
"issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues",
"source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.3.0"
"source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0"
},
"time": "2024-12-21T16:25:41+00:00"
"time": "2025-12-02T11:56:42+00:00"
},
{
"name": "ueberdosis/tiptap-php",
@@ -11629,16 +11629,16 @@
"packages-dev": [
{
"name": "barryvdh/laravel-ide-helper",
"version": "v3.6.0",
"version": "v3.6.1",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-ide-helper.git",
"reference": "8d00250cba25728373e92c1d8dcebcbf64623d29"
"reference": "b106f7ee85f263c4f103eca49e7bf3862c2e5e75"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/8d00250cba25728373e92c1d8dcebcbf64623d29",
"reference": "8d00250cba25728373e92c1d8dcebcbf64623d29",
"url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/b106f7ee85f263c4f103eca49e7bf3862c2e5e75",
"reference": "b106f7ee85f263c4f103eca49e7bf3862c2e5e75",
"shasum": ""
},
"require": {
@@ -11707,7 +11707,7 @@
],
"support": {
"issues": "https://github.com/barryvdh/laravel-ide-helper/issues",
"source": "https://github.com/barryvdh/laravel-ide-helper/tree/v3.6.0"
"source": "https://github.com/barryvdh/laravel-ide-helper/tree/v3.6.1"
},
"funding": [
{
@@ -11719,7 +11719,7 @@
"type": "github"
}
],
"time": "2025-07-17T20:11:57+00:00"
"time": "2025-12-10T09:11:07+00:00"
},
{
"name": "barryvdh/reflection-docblock",
@@ -12363,16 +12363,16 @@
},
{
"name": "larastan/larastan",
"version": "v3.8.0",
"version": "v3.8.1",
"source": {
"type": "git",
"url": "https://github.com/larastan/larastan.git",
"reference": "d13ef96d652d1b2a8f34f1760ba6bf5b9c98112e"
"reference": "ff3725291bc4c7e6032b5a54776e3e5104c86db9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/larastan/larastan/zipball/d13ef96d652d1b2a8f34f1760ba6bf5b9c98112e",
"reference": "d13ef96d652d1b2a8f34f1760ba6bf5b9c98112e",
"url": "https://api.github.com/repos/larastan/larastan/zipball/ff3725291bc4c7e6032b5a54776e3e5104c86db9",
"reference": "ff3725291bc4c7e6032b5a54776e3e5104c86db9",
"shasum": ""
},
"require": {
@@ -12386,7 +12386,7 @@
"illuminate/pipeline": "^11.44.2 || ^12.4.1",
"illuminate/support": "^11.44.2 || ^12.4.1",
"php": "^8.2",
"phpstan/phpstan": "^2.1.29"
"phpstan/phpstan": "^2.1.32"
},
"require-dev": {
"doctrine/coding-standard": "^13",
@@ -12441,7 +12441,7 @@
],
"support": {
"issues": "https://github.com/larastan/larastan/issues",
"source": "https://github.com/larastan/larastan/tree/v3.8.0"
"source": "https://github.com/larastan/larastan/tree/v3.8.1"
},
"funding": [
{
@@ -12449,7 +12449,7 @@
"type": "github"
}
],
"time": "2025-10-27T23:09:14+00:00"
"time": "2025-12-11T16:37:35+00:00"
},
{
"name": "laravel/pail",

View File

@@ -1,5 +1,6 @@
<?php
use App\Http\Controllers\Api\Remote\Backups\BackupRemoteUploadController;
use App\Models\Backup;
return [
@@ -10,16 +11,16 @@ return [
// This value is used to determine the lifespan of UploadPart presigned urls that daemon
// uses to upload backups to S3 storage. Value is in minutes, so this would default to an hour.
'presigned_url_lifespan' => env('BACKUP_PRESIGNED_URL_LIFESPAN', 60),
'presigned_url_lifespan' => (int) env('BACKUP_PRESIGNED_URL_LIFESPAN', 60),
// This value defines the maximal size of a single part for the S3 multipart upload during backups
// The maximal part size must be given in bytes. The default value is 5GB.
// Note that 5GB is the maximum for a single part when using AWS S3.
'max_part_size' => env('BACKUP_MAX_PART_SIZE', 5 * 1024 * 1024 * 1024),
'max_part_size' => (int) env('BACKUP_MAX_PART_SIZE', BackupRemoteUploadController::DEFAULT_MAX_PART_SIZE),
// The time to wait before automatically failing a backup, time is in minutes and defaults
// to 6 hours. To disable this feature, set the value to `0`.
'prune_age' => env('BACKUP_PRUNE_AGE', 360),
'prune_age' => (int) env('BACKUP_PRUNE_AGE', 360),
// Defines the backup creation throttle limits for users. In this default example, we allow
// a user to create two (successful or pending) backups per 10 minutes. Even if they delete
@@ -27,8 +28,8 @@ return [
//
// Set the period to "0" to disable this throttle. The period is defined in seconds.
'throttles' => [
'limit' => env('BACKUP_THROTTLE_LIMIT', 2),
'period' => env('BACKUP_THROTTLE_PERIOD', 600),
'limit' => (int) env('BACKUP_THROTTLE_LIMIT', 2),
'period' => (int) env('BACKUP_THROTTLE_PERIOD', 600),
],
'disks' => [

View File

@@ -3,7 +3,7 @@
use Illuminate\Support\Str;
$database = env('DB_DATABASE', 'database.sqlite');
$datapasePath = database_path($database);
$databasePath = database_path($database);
if (str_starts_with($database, '/') || $database === ':memory:') {
$databasePath = $database;
@@ -41,7 +41,7 @@ return [
'sqlite' => [
'driver' => 'sqlite',
'url' => env('DB_URL'),
'database' => $datapasePath,
'database' => $databasePath,
'prefix' => '',
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
'busy_timeout' => null,
@@ -65,7 +65,7 @@ return [
'strict' => env('DB_STRICT_MODE', false),
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
@@ -85,7 +85,7 @@ return [
'strict' => env('DB_STRICT_MODE', false),
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],

View File

@@ -13,7 +13,7 @@ return [
*/
'rate_limit' => [
'client_period' => 1,
'client' => env('APP_API_CLIENT_RATELIMIT', 720),
'client' => env('APP_API_CLIENT_RATELIMIT', 120),
'application_period' => 1,
'application' => env('APP_API_APPLICATION_RATELIMIT', 240),

View File

@@ -8,7 +8,7 @@ To start contributing to Pelican Panel, you need to have a basic understanding o
* [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)
* [Git](https://git-scm.com) & [GitHub](https://github.com)
## Dev Environment Setup

View File

@@ -13,8 +13,6 @@ class DatabaseSeeder extends Seeder
*/
public function run(): void
{
$this->call(EggSeeder::class);
Role::firstOrCreate(['name' => Role::ROOT_ADMIN]);
$plugins = Plugin::query()->orderBy('load_order')->get();

View File

@@ -1,101 +0,0 @@
<?php
namespace Database\Seeders;
use App\Models\Egg;
use App\Services\Eggs\Sharing\EggImporterService;
use DirectoryIterator;
use Illuminate\Database\Seeder;
use Illuminate\Http\UploadedFile;
use Symfony\Component\Yaml\Yaml;
use Throwable;
class EggSeeder extends Seeder
{
protected EggImporterService $importerService;
/**
* @var string[]
*/
public static array $imports = [
'Minecraft',
'Source Engine',
'Voice Servers',
'Rust',
];
/**
* EggSeeder constructor.
*/
public function __construct(
EggImporterService $importerService
) {
$this->importerService = $importerService;
}
/**
* Run the egg seeder.
*/
public function run(): void
{
foreach (static::$imports as $import) {
$this->parseEggFiles($import);
}
}
/**
* Loop through the list of egg files and import them.
*/
protected function parseEggFiles($name): void
{
$path = database_path('Seeders/eggs/' . kebab_case($name));
$files = new DirectoryIterator($path);
$this->command->alert('Updating Eggs for: ' . $name);
/** @var DirectoryIterator $file */
foreach ($files as $file) {
if (!$file->isFile() || !$file->isReadable()) {
continue;
}
$extension = strtolower($file->getExtension());
$filePath = $file->getRealPath();
try {
$decoded = match ($extension) {
'json' => json_decode(file_get_contents($filePath), true, 512, JSON_THROW_ON_ERROR),
'yaml', 'yml' => Yaml::parseFile($filePath),
default => null,
};
} catch (Throwable) {
$this->command->warn("Failed to parse {$file->getFilename()}, skipping.");
continue;
}
if (!is_array($decoded) || !isset($decoded['name'], $decoded['author'])) {
$this->command->warn("Invalid structure in {$file->getFilename()}, skipping.");
continue;
}
$uploaded = new UploadedFile($filePath, $file->getFilename());
$egg = Egg::query()
->where('author', $decoded['author'])
->where('name', $decoded['name'])
->first();
if ($egg instanceof Egg) {
$this->importerService->fromFile($uploaded, $egg);
$this->command->info('Updated ' . $decoded['name']);
} else {
$this->importerService->fromFile($uploaded);
$this->command->comment('Created ' . $decoded['name']);
}
}
$this->command->line('');
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,159 +0,0 @@
_comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL'
meta:
version: PLCN_v3
update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/minecraft/egg-sponge.yaml'
exported_at: '2025-10-31T12:41:03+00:00'
name: Sponge
author: panel@example.com
uuid: f0d2f88f-1ff3-42a0-b03f-ac44c5571e6d
description: 'A community-driven open source Minecraft: Java Edition modding platform.'
image: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBmaWxsPSIjRjdDRjBEIj48cGF0aCBkPSJNMTkwIDBIMTBDNC41IDAgMCA0LjUgMCAxMHYxODBjMCA1LjUgNC41IDEwIDEwIDEwaDE2LjFjLTEuNy00NS43LS4xLTUyLjUgMy4xLTU3IDMuOS01LjYgNS41LTYuMyAxMS40LTExIDUtNCAzLjItMTAuNS0uNC0xNS4yLTIuMi0yLjktNS4zLTYuMy03LjctOS42LTEuNS0yLjIgMi4yLTE1LjEgMy42LTE5LjggMS40LTQuNyAzLjgtMjAgMjQuOC0yNC4xIDcuOS0xLjYgMjkuNi0yLjcgNDQuNS0xLjgtLjEtLjYtLjMtMS4zLS40LTItLjMtMS4yLS41LTIuNS0uOC0zLjktLjMtMS4zLS42LTIuNy0uOS00LjEtLjMtMS40LS43LTIuOC0xLTQuMy0uNC0xLjUtLjctMi45LTEuMi00LjQtLjgtMy0xLjgtNS45LTMtOC43LS42LTEuNC0xLjItMi43LTEuOS0zLjktLjctMS4xLTEuNC0yLjEtMi0yLjUtLjEtLjEtLjItLjItLjMtLjJoLS4xLjJzLjEgMCAwIDBsLS4zLS4xaC0uMmwtLjQtLjFoLS41Yy0xLjMtLjEtMi43LS4xLTQuMiAwLTIuOS4yLTYgLjgtOSAxLjVzLTUuOSAxLjYtOC43IDIuNGMtMS4yLjQtMi4zLjgtMy40IDEuMS4xLjkuMiAxLjcuMiAyLjYgMCAxMy0xMC41IDIzLjUtMjMuNSAyMy41UzIwLjYgNDcuOSAyMC42IDM0LjlzMTAuNS0yMy41IDIzLjUtMjMuNWM4LjcgMCAxNi4zIDQuNyAyMC40IDExLjggMS0uNCAyLjEtLjggMy4yLTEuMiAyLjgtMS4xIDUuOS0yLjIgOS4xLTMuMiAzLjMtMSA2LjctMiAxMC41LTIuNSAxLjktLjMgMy45LS40IDYuMS0uM2guOGMuMyAwIC42LjEuOC4xSDk1LjdsLjMuMWguMWwuMy4xcy4yIDAgLjMuMWwuNC4xYy42LjIuOS4zIDEuMy41cy43LjMgMS4xLjVjLjcuNCAxLjMuOCAxLjkgMS4yIDEuMS45IDIgMS44IDIuNyAyLjcuOC45IDEuNCAxLjggMiAyLjcgMS4yIDEuOCAyLjEgMy41IDIuOSA1LjIgMS42IDMuNCAyLjkgNi44IDMuOSAxMGwxLjUgNC44Yy41IDEuNi44IDMuMSAxLjIgNC42LjIuNy40IDEuNS41IDIuMi4yLjcuMyAxLjQuNSAyLjEuMyAxLjQuNiAyLjguOSA0LjEuNCAyIC43IDMuOSAxIDUuNiAyMi40IDIuMiAzOS41IDUuMSA0Ny4yIDEyLjggMTEuMyAxMSAyMCA2MSAxNC4zIDEyNC41aDEwYzUuNSAwIDEwLTQuNSAxMC0xMFYxMGMwLTUuNS00LjUtMTAtMTAtMTB6Ii8+PHBhdGggZD0iTTkxLjQgMTQwLjhjLTEuMyAzLjYtMi40IDQ1LjcgMTAgNDUuN3MxMi41LTQzLjIgMTIuMS00NS43Yy0uNC0yLjQtMjAuOC0zLjUtMjIuMSAwek03NSAxMDBjLTguNS0xLjItMTMuNiA0MC4yLTEuNyA0Mi42IDExLjIgMi4yIDEwLjEtNDEuNCAxLjctNDIuNnpNMTMwLjggMTAwYy04LjUtMS4yLTEzLjYgNDAuMi0xLjcgNDIuNiAxMS4yIDIuMiAxMC4yLTQxLjQgMS43LTQyLjZ6Ii8+PC9zdmc+'
tags:
- minecraft
features:
- eula
- java_version
- pid_limit
docker_images:
'Java 21': 'ghcr.io/pelican-eggs/yolks:java_21'
'Java 17': 'ghcr.io/pelican-eggs/yolks:java_17'
'Java 16': 'ghcr.io/pelican-eggs/yolks:java_16'
'Java 11': 'ghcr.io/pelican-eggs/yolks:java_11'
'Java 8': 'ghcr.io/pelican-eggs/yolks:java_8'
file_denylist: { }
startup_commands:
Default: 'java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}'
config:
files:
server.properties:
parser: properties
find:
server-ip: ''
server-port: '{{server.allocations.default.port}}'
query.port: '{{server.allocations.default.port}}'
startup:
done: ')! For help, type '
logs: { }
stop: stop
scripts:
installation:
script: |-
#!/bin/ash
# Sponge Installation Script
#
# Server Files: /mnt/server
cd /mnt/server
if [ $MINECRAFT_VERSION = 'latest' ] || [ -z $MINECRAFT_VERSION ]; then
TARGET_VERSION_JSON=$(curl -sSL https://dl-api.spongepowered.org/v2/groups/org.spongepowered/artifacts/${SPONGE_TYPE}/latest?recommended=true)
if [ -z "${TARGET_VERSION_JSON}" ]; then
echo -e "Failed to find latest recommended version!"
exit 1
fi
echo -e "Found latest version for ${SPONGE_TYPE}"
else
if [ $SPONGE_TYPE = 'spongevanilla' ]; then
VERSIONS_JSON=$(curl -sSL https://dl-api.spongepowered.org/v2/groups/org.spongepowered/artifacts/${SPONGE_TYPE}/versions?tags=,minecraft:${MINECRAFT_VERSION}&offset=0&limit=1)
else
FORGETAG='forge'
if [ $SPONGE_TYPE = 'spongeneo' ]; then
FORGETAG='neoforge'
fi
VERSIONS_JSON=$(curl -sSL https://dl-api.spongepowered.org/v2/groups/org.spongepowered/artifacts/${SPONGE_TYPE}/versions?tags=,minecraft:${MINECRAFT_VERSION},${FORGETAG}:${FORGE_VERSION}&offset=0&limit=1)
fi
if [ -z "${VERSIONS_JSON}" ]; then
echo -e "Failed to find recommended ${MINECRAFT_VERSION} version for ${SPONGE_TYPE} ${FORGE_VERSION}!"
exit 1
fi
VERSION_KEY=$(echo $VERSIONS_JSON | jq -r '.artifacts | to_entries[0].key')
TARGET_VERSION_JSON=$(curl -sSL https://dl-api.spongepowered.org/v2/groups/org.spongepowered/artifacts/${SPONGE_TYPE}/versions/${VERSION_KEY})
if [ -z "${TARGET_VERSION_JSON}" ]; then
echo -e "Failed to find ${VERSION_KEY} for ${SPONGE_TYPE} ${FORGE_VERSION}!"
exit 1
fi
echo -e "Found ${MINECRAFT_VERSION} for ${SPONGE_TYPE}"
fi
TARGET_VERSION=`echo $TARGET_VERSION_JSON | jq '.assets[] | select(.classifier == "universal")'`
if [ -z "${TARGET_VERSION}" ]; then
TARGET_VERSION=`echo $TARGET_VERSION_JSON | jq '.assets[] | select(.classifier == "" and .extension == "jar")'`
fi
if [ -z "${TARGET_VERSION}" ]; then
echo -e "Failed to get download url data from the selected version"
exit 1
fi
SPONGE_URL=$(echo $TARGET_VERSION | jq -r '.downloadUrl')
CHECKSUM=$(echo $TARGET_VERSION | jq -r '.md5')
echo -e "Found file at ${SPONGE_URL} with checksum ${CHECKSUM}"
echo -e "running: curl -o ${SERVER_JARFILE} ${SPONGE_URL}"
curl -o ${SERVER_JARFILE} ${SPONGE_URL}
if [ $(basename $(md5sum ${SERVER_JARFILE})) = ${CHECKSUM} ] ; then
echo "Checksum passed"
else
echo "Checksum failed"
fi
echo -e "Install Complete"
container: 'ghcr.io/pelican-eggs/installers:alpine'
entrypoint: ash
variables:
-
name: 'Forge/Neoforge Version'
description: |-
The modding api target version if set to `spongeforge` or `spongeneo`. Leave blank if using
`spongevanilla`
env_variable: FORGE_VERSION
default_value: ''
user_viewable: true
user_editable: true
rules:
- string
sort: 3
-
name: 'Minecraft Version'
description: |-
The version of Minecraft to target. Use "latest" to install the latest version. Go to Settings >
Reinstall Server to apply.
env_variable: MINECRAFT_VERSION
default_value: latest
user_viewable: true
user_editable: true
rules:
- required
- string
- 'between:3,15'
sort: 1
-
name: 'Server Jar File'
description: 'The name of the Jarfile to use when running Sponge.'
env_variable: SERVER_JARFILE
default_value: server.jar
user_viewable: true
user_editable: true
rules:
- required
- 'regex:/^([\w\d._-]+)(\.jar)$/'
sort: 4
-
name: 'Sponge Type'
description: |-
SpongeVanilla if you are only using Sponge plugins.
SpongeForge when using Forge mods and Sponge plugins.
SpongeNeo when using NeoForge mods and Sponge plugins.
env_variable: SPONGE_TYPE
default_value: spongevanilla
user_viewable: true
user_editable: true
rules:
- required
- 'in:spongevanilla,spongeforge,spongeneo'
sort: 2

File diff suppressed because one or more lines are too long

View File

@@ -1,155 +0,0 @@
_comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL'
meta:
version: PLCN_v3
update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/source-engine/egg-custom-source-engine-game.yaml'
exported_at: '2025-10-31T12:43:00+00:00'
name: 'Custom Source Engine Game'
author: panel@example.com
uuid: 2a42d0c2-c0ba-4067-9a0a-9b95d77a3490
description: |-
This option allows modifying the startup arguments and other details to run a custom SRCDS based
game on the panel.
image: 'data:image/webp;base64,UklGRoAMAABXRUJQVlA4THMMAAAv+QATECUwbtvIkdx/2ZNnLnwjYgLYqi8KnDuYekQOBtxU1VNGcZdt8CWWtnXLbdOCTsNlE6Ix5eSRaVdUVr1HA0Dlx+NLkmTVtm3binROhla09D55Lsi/tN5reIlWJ3xHSKBt7dii9/uN8Cv8yLaxjGXbrrFt20YY2whj2zZy/bbVK8FtI0mS6JnZe9d1ZWVW7QskSZIcN1L9/zwbHDaPlYyAqtAy1NVBG0mOnCyW92E8+9FhG0mKVBkf/fjgKXVZkCSbttXXtm3btm3bfrZt27b5dW3btm3uuyBIkuSoyWLwSHjPBwr8a2Y+MUMVHtiDh/iHYtShBa1oQCUqUYv6V2h4HQO0ogXt6EbXK/Rj4BUGMfYOk5h5hwUsYRGLWMb6O2yBgGATs2jDT6RbYlG4eWMvZksTu/EbXVgF+bEwjZsQvbV53+lhjBtoBwXyw2EUAVfG8CEddaBAviBs48h9QRSXMAbyLeHAZc3HiwfHMAHyPWHLcrsqGKIT5KtCh8V1UWZ4XvNl4dBFIfbjOYYVdOE/bmEPwuEBB9jBGQFIxll8QC1mzkPXhP2h2DWzJtas8qjUohQoemVdDC7uoR8vEQrFf+dh2h+rP78/jSUCe5xHJTYOL+D8UDZVX+9qfHHqdc0oUMvrzcVgzwbm8I5xAlvumP153XRQx5mzt0Rcex7bwtelohao2/WqmPWhbl8Mo4D5JwzjBCNROXJjsSIQpQch86kIdaVohdSJ2lLPanOdv9kOiMfmGyhcsXirx25eQw8fNByDeoPhqa491cUKrpMVXp1RuFljT2M8RQ7ejD3tThy9cU4W7MPcIWj3xPpQuFpdlgVqVTEKFLvWR6GF5owics9A4zgnUxQ6aj4PRpwHdQJyotBUCMHCAbjXlSjACpN1hk9bogBDjFVhZM7w9iUKMKv+R9yPQmfBq/YsGlswaE0UsK8EuVFoLnyuQHh3IIK+PPRxcRe9ozYeiQUyAvSLNgYBGQn+R+Rv4gc0EtIb+mTjEJGRFvZFG7XxSEhwvyIv1EJgHi4XeAFH5bbr9E4vPjG+Pu912qoYa4itQlv1+GyQEE+vl85bIGYRs03i2IIDlbFrbTihPKFzaiUW4YKlUsd8wMX3XmD3f4S7Jh0e+G2E0Kgf7juobrkMvWGFmeuc1/rwcf310gkt3H5GCgyDn1lYg9rYG0aRW5RgDpmrUr9HjhYixhmi2MyYP1Yvh0QRLdkl5AZYZ27RXOvQW/ukle7agULC50llit5rfelXD5xi92jBFCp3lW7cGYrpPjKlZjRuSRuCFpaT8D89Aj0CBzGmk8sBT5eYz9VIrdjkTkU4VCiZZ72ObU9O9A475Ylz2FCpF2K8As/xTvLB94hqfecd4/J2KjAKniTBN2vlkYKzGBUbqSWZVfOGX6M8HIUDTztbnxzvdnIBAfrB7BeUhfTgJIzWbUyyKU6NmhmROgJVrObe8J3zYk1aMRTgNAQCA4vH1fDF/RV2wmb4C2ZfIPaB0zj2wHSYXg2LUSssMAm+pOBUct7w3oHzeAHXmPNCNUJhfwFJzoAHtoPz2N5sqtfjYhRiJ+CUgWVLMUk12JE7Y4ZShbvGcnfpQhPNV7KYImYaOjDGj824HaPNDAcawIiBlinXUHQB5XK++GjyJen3YiSMczKhJQE/opCks/OVZ2siytfOy4vF8B/smNzWLJIXPylW+A2GaL0dMzkYScbEC+idUyKIl3A1rtIHviF+Mi3da5xSLpQnP2k2Gg5N7C0wCC4nwC3pQTHUKQhdeJM0dHw+WZ8GW7mtGbJ1zOH0EH3bD463BAKfPPTYJ3wx6Tf5L761XQkGQhfVH7DxwlW//FqzZeNK+HcFDcDyMzSNgzlp+dH2I70CUhlteYu3PUEYTFZwqmYu2OPSX4LRRambdpl5CaLqDPMKdYVTdHo+2NJLePZmFZohuN/LODIADvSf+vxWFNijJlFmVN8jXDM/mAF2GnPhcNHT8PoBeDJ7M2vBTlnsa324rbEpgGGkD398YzOhtA3J5hQ4B78+wRyk6gwKDR5oli2L8CHMdrxZqExoGAbMvh+5yDAR9r3zoEDEO0wkxRRQ0ys4WCDH1V5H0wAOfYI3UcjilgY6Cq07fLRM4UTRmXoq5H7nzv1MLtAJYQrXc/ZAeUyEdwm53AIudFIO1HYuD8D5E9ikfYt4my97P7Sq57vIJ3J6KVo+n1I7G0S5my/v+MD43oWY/WGbWGKgX4Kx8Ij+92YoB8uJtvFvk8/mjBrW9lDx/7bSRSGdKeApTHp1WSIZNbGw829jTySF2wOfu1mYimnvPC8wDm+G/k846ZjjKY45Td3PEh5n4MPEHtKikPeO83C0TCJwRR36eae4W+BkO4RBQNrNjAmNi8jOoTwp4OuRdvbr/6dD9xbGx7mEopCo/GQczwa2F651zhyhEoW70c2MpGowPnRW5Km9gcZBV1Rt4VEUSrG0iw6cxbDcw+mg/VzXYGfCzXxMvd4IfnVm5Zl0AzMGkL8DyjKKQjH+9uLOtoVR/Qe3O21HLAx0Js55NuNugux/Z1qeyTewYADZOyiIQj00+dp8mPxxahFPrndOnX9A2Kkbe0e8mdGVIPjZWTSn4QbmDiBvB7EFbskbZrkqs9ULYvAQ3U8Od24dbTZNJyshR41gfJ1xPYH2rrNzTnaYsitXoHAWJVwHULH7KMviK3Bftr0Mu/2LaHkUsDSsRH9An4gHO34J1bKPEAy+rICusDA4gUNhfQkKrMAw02Bd4Lb8fyy06NjArSjcm01U7H36UF5AQbjkeEZ5HWEEvYDSjp53hEPxOts7A5t0YDElwSUU5m8ybh/shNzH4MLYCpvQKdk0tGvykYR3KjC7ALewijP+xM3OwwIF5IWiMiPp4OuUhGYSD9xJ6ydPegUtN/7XDSzvvoxU4j+u0c8QeIkQyyw2UWd+gQJehNXpn8jhPkQT54ISAosiO2/AgbRzNEpHmXp7pqvczRMxlpj/GYtVDsB2sxnBNXr7wMAlgvYtTREKWJAuuSycj/QCnvilx8A/otPUzOjU8EkUrbDZxed1gQI7iKFP5yVkb4Q+FFg1gJ0r9Mw5OGp0zW5K7Ak7GR/nA0kYSHjNZja6PwfkUBTU0H2DOA7eQX6o5jXrQCZ9ONXzFM7XbZc80H0yT8l9AJ9WOBeFGtM6IqFBOrPJsQ8Xi14JfT1vEUPHfHkV4WNa9Ktof8YZ8BYwnoVuHnLsz2NIwr7A+SAtNPM5nI+89VReaDCs6yGxMvZULuIQhhJcVQt3tUiWMB5ucptlKD2TdUk5L0zsHW522sGgk5eu9saHwp5ZqVmzRS6SgvcoIIU5vVewSRwOJm+1bqUm1l+qOR960Lighj8WJm/lzlCG48tzAPrYbvArCtVsTvvFN9tUSZOqyq44kZDTR/64Q6locw29i3UhDRzkLgVsyf3oKJnzk/RNvZ4UBvPylYbK2GHKPIAT62Z4lKH6DhZyoXWz6oVmZO3lzsJ4P8LowS02TieYjkpcPx+p5tVc/xE94P2iXV8honm8TuYolBOCP+fLsuvdbBwjlRGGyBFtim5RYFHcnKJLtzAFlFNebFqBBJiuqzgchQMJ9XfKS47xiRHVjG/CUzNbB1x7IEruUWCTtA5fFcrOM3T/ZtxELzCBZwvMW9JROBGGg7QTlDYsi9xNsDuTq8C2vCzaiC3MB8z2IBZQNnt7rXOy4Yr9PmN29bQAYquVfvAyz9IHvsAEZDC/wMcoHEqAfQZODNjK7VgNWprrVJkhu8/DuUyv4wDohfTLkMz4CLT5ydHewZXRdrMy2UfmfCxdk7yRczXBDhMeHlXZs9wKzIJr6+axp0MUjvWwlGKZSz7qxycmxtPnvfMWiWcxqvYVwlG9Vs/9xyUiNOyHm9bKYDkptrFirXXHb6NERAa9sk3845PnZbE9tvuj7fHxupNd74A53O6NFW6BK74aJiDG0+uVdo2chtUWQ7EdHvprlIgY118P7VCwaQWGgTxmFqiezxd9FI72J7LJ23axsbYN0ovdHoTNwGK/KRv9Sq93zKZjYrIs8FcFijX7xd42i/hiNTh0TCx0uFd64dW6ioQodBucQC3QaU+ubs0ZHjSvq0hvdxVP1kGLGfZuIQFkhcBuBzZYXuFvtwOtzdt+mIVKszjV0AfS9WFguBt8aXagiLHNoHmcU6BZeLAZTEE7Cs1C9ebn8XCNQrdQvMAGQqPQLpxaBhFR6Ne/c/Gg9gEZUeg4QwRvsQiC9rM/jCoAAA=='
tags:
- source
- steamcmd
features:
- steam_disk_space
docker_images:
Source: 'ghcr.io/pelican-eggs/games:source'
file_denylist: { }
startup_commands:
Default: './srcds_run -game {{SRCDS_GAME}} -console -port {{SERVER_PORT}} +map {{SRCDS_MAP}} +ip 0.0.0.0 -strictportbind -norestart'
config:
files: { }
startup:
done: 'gameserver Steam ID'
logs: { }
stop: quit
scripts:
installation:
script: |-
#!/bin/bash
# steamcmd Base Installation Script
#
# Server Files: /mnt/server
##
#
# Variables
# STEAM_USER, STEAM_PASS, STEAM_AUTH - Steam user setup. If a user has 2fa enabled it will most likely fail due to timeout. Leave blank for anon install.
# WINDOWS_INSTALL - if it's a windows server you want to install set to 1
# SRCDS_APPID - steam app id ffound here - https://developer.valvesoftware.com/wiki/Dedicated_Servers_List
# EXTRA_FLAGS - when a server has extra glas for things like beta installs or updates.
#
##
## just in case someone removed the defaults.
if [ "${STEAM_USER}" == "" ]; then
echo -e "steam user is not set.
"
echo -e "Using anonymous user.
"
STEAM_USER=anonymous
STEAM_PASS=""
STEAM_AUTH=""
else
echo -e "user set to ${STEAM_USER}"
fi
## download and install steamcmd
cd /tmp
mkdir -p /mnt/server/steamcmd
curl -sSL -o steamcmd.tar.gz https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz
tar -xzvf steamcmd.tar.gz -C /mnt/server/steamcmd
mkdir -p /mnt/server/steamapps # Fix steamcmd disk write error when this folder is missing
cd /mnt/server/steamcmd
# SteamCMD fails otherwise for some reason, even running as root.
# This is changed at the end of the install process anyways.
chown -R root:root /mnt
export HOME=/mnt/server
## install game using steamcmd
./steamcmd.sh +force_install_dir /mnt/server +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} $( [[ "${WINDOWS_INSTALL}" == "1" ]] && printf %s '+@sSteamCmdForcePlatformType windows' ) +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} validate +quit ## other flags may be needed depending on install. looking at you cs 1.6
## set up 32 bit libraries
mkdir -p /mnt/server/.steam/sdk32
cp -v linux32/steamclient.so ../.steam/sdk32/steamclient.so
## set up 64 bit libraries
mkdir -p /mnt/server/.steam/sdk64
cp -v linux64/steamclient.so ../.steam/sdk64/steamclient.so
container: 'ghcr.io/pelican-eggs/installers:debian'
entrypoint: bash
variables:
-
name: 'Game ID'
description: 'The ID corresponding to the game to download and run using SRCDS.'
env_variable: SRCDS_APPID
default_value: ''
user_viewable: true
user_editable: false
rules:
- required
- numeric
- 'digits_between:1,6'
sort: 1
-
name: 'Game Name'
description: 'The name corresponding to the game to download and run using SRCDS.'
env_variable: SRCDS_GAME
default_value: ''
user_viewable: true
user_editable: false
rules:
- required
- alpha_dash
- 'between:1,100'
sort: 2
-
name: Map
description: 'The default map for the server.'
env_variable: SRCDS_MAP
default_value: ''
user_viewable: true
user_editable: true
rules:
- required
- string
- alpha_dash
sort: 3
-
name: 'Steam Auth'
description: ''
env_variable: STEAM_AUTH
default_value: ''
user_viewable: true
user_editable: true
rules:
- nullable
- string
sort: 6
-
name: 'Steam Password'
description: ''
env_variable: STEAM_PASS
default_value: ''
user_viewable: true
user_editable: true
rules:
- nullable
- string
sort: 5
-
name: 'Steam Username'
description: ''
env_variable: STEAM_USER
default_value: ''
user_viewable: true
user_editable: true
rules:
- nullable
- string
sort: 4

View File

@@ -1,229 +0,0 @@
_comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL'
meta:
version: PLCN_v3
update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/source-engine/egg-garrys-mod.yaml'
exported_at: '2025-10-31T12:37:53+00:00'
name: 'Garrys Mod'
author: panel@example.com
uuid: 60ef81d4-30a2-4d98-ab64-f59c69e2f915
description: |-
Garrys Mod, is a sandbox physics game created by Garry Newman, and developed by his company,
Facepunch Studios.
image: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyNC4zLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCAzODQgMzg0IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAzODQgMzg0OyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPg0KCS5zdDB7ZmlsbDojMDA4MUZGO30NCgkuc3Qxe2ZpbGw6I0ZGRkZGRjt9DQo8L3N0eWxlPg0KPHBhdGggY2xhc3M9InN0MCIgZD0iTTM0My45MiwzODRjLTEwMS42LDAtMjAzLjIsMC0zMDQuOCwwYy0wLjg5LTAuNDQtMS44NC0wLjMxLTIuNzktMC4zOGMtNC41MS0wLjM2LTguNjEtMi4wNy0xMi41Ni00LjEzDQoJYy01LjktMy4wNy0xMC44NC03LjMxLTE0LjkyLTEyLjU0Yy0zLjkzLTUuMDQtNi43My0xMC42NS04LjE3LTE2LjljLTAuMzktMS42OC0wLjMxLTMuNDEtMC40MS01LjEyYy0wLjAyLTAuMzQsMC4wOS0wLjc0LTAuMjctMQ0KCUMwLDI0Mi42NCwwLDE0MS4zNiwwLDQwLjA4YzAuNDMtMC42NywwLjI1LTEuNDQsMC4yNy0yLjE1YzAuMTEtNC42NCwxLjU3LTguOTIsMy42My0xMi45N0M5LjIyLDE0LjQ4LDE3LjI1LDYuODgsMjguMjUsMi40Ng0KCWMzLjM2LTEuMzUsNi44MS0yLjA5LDEwLjQtMi4yN0MzOS4xMywwLjE2LDM5LjY3LDAuNiw0MC4wOCwwQzE0MS45MiwwLDI0My43NiwwLDM0NS42LDBjMC43MSwwLjc3LDEuNjgsMC4zNiwyLjQ5LDAuNDINCgljMS45MiwwLjE1LDMuNzQsMC42NCw1LjUzLDEuMjVjNy4zMSwyLjQ2LDEzLjU4LDYuNTYsMTguODIsMTIuMmM1LjM5LDUuOCw5LjEyLDEyLjUsMTAuOSwyMC4yNWMwLjM4LDEuNjYsMC4yOSwzLjM3LDAuMzksNS4wNg0KCWMwLjAyLDAuMzItMC4wNywwLjY3LDAuMjYsMC45YzAsMTAxLjI4LDAsMjAyLjU2LDAsMzAzLjg0Yy0wLjU4LDAuOC0wLjM1LDEuNzYtMC4zNCwyLjU5YzAuMDMsMi43LTAuNjMsNS4yNC0xLjUzLDcuNzENCgljLTQuMjMsMTEuNTUtMTEuODgsMjAuMTItMjIuOCwyNS43M2MtMy4xMywxLjYxLTYuNDIsMi45MS05Ljk2LDMuNDNjLTEuNTIsMC4yMi0zLjA0LDAuMjYtNC41NiwwLjMzDQoJQzM0NC40OCwzODMuNzMsMzQ0LjE1LDM4My42OCwzNDMuOTIsMzg0eiIvPg0KPHBhdGggY2xhc3M9InN0MSIgZD0iTTIzNS41NywyMjEuOTFjLTIuODQsMy40LTUuNjgsNy41MS05LjIyLDEwLjg4Yy03LjIsNi44NC0xNi4yLDEwLjQxLTI1Ljc3LDEyLjQ5DQoJYy0xMS41MywyLjUxLTIzLjEzLDIuMTgtMzQuNzYsMC42MWMtMTQuNTItMS45NS0yNy4yNy03LjgzLTM4LjM4LTE3LjIyYy05LjM1LTcuOTEtMTYuMDYtMTcuOC0yMC44NC0yOS4wNA0KCWMtNC44OC0xMS40Ni03LjQtMjMuNTEtOC42OC0zNS44MWMtMS44NS0xNy43OC0wLjk5LTM1LjQyLDQuMzMtNTIuNmM0LjM3LTE0LjExLDExLjAxLTI3LDIxLjA4LTM4LjAzDQoJQzEzNSw2MC40LDE0OS4xOCw1Mi4wOCwxNjYuMSw0OC41OWM5LjQ2LTEuOTUsMTkuMDEtMS45NSwyOC41Ny0wLjRjMTcsMi43NywzMC4xMywxMS40OSwzOS43NSwyNS42OGMwLjI1LDAuMzIsMS4xOSwxLjcyLDEuMTksMS43Mg0KCXMwLTQuNzgsMC02LjgzYy0wLjAxLTUuMTYsMC0xNi41MSwwLTE2LjUxczEuMDksMCwxLjM1LDBjMTUuMjgtMC4wNSw0OS45Ny0wLjAxLDQ5Ljk3LTAuMDFzLTAuMDEsMS45Ni0wLjAxLDQuMjgNCgljMCw2NC40LDAuMDksMTI4LjgtMC4wNSwxOTMuMTljLTAuMDQsMjAuMzYtNS43OSwzOS0xOC44NCw1NC45NWMtMTAuMTUsMTIuNC0yMy4yNSwyMC41Ny0zOC4yNSwyNS44NQ0KCWMtMTQuNiw1LjE0LTI5LjcsNi45MS00NS4wOCw2LjI2Yy0xNi44MS0wLjcxLTMyLjk5LTQuMzUtNDcuNzUtMTIuNjRjLTIyLjIyLTEyLjQ5LTM1LjcyLTMxLjIyLTM5LjA3LTU2Ljc4DQoJYy0wLjQ1LTMuMzktMC41Ni02Ljg0LTAuNjktMTAuMjZjLTAuMDEtMC4yMS0wLjA3LTEuMzEtMC4wNy0xLjMxczEuMDQsMCwxLjI2LDBjMTUuMjQtMC4wNywzMC40OC0wLjA4LDQ1LjcyLTAuMDMNCgljMC4xOCwwLDAuOSwwLjAxLDAuOSwwLjAxcy0wLjAxLDAuOTYtMC4wMSwxLjEyYzAuMTYsOC44NiwyLjYyLDE2Ljg2LDguNzcsMjMuNDRjNC41OSw0LjkxLDEwLjI2LDguMTYsMTYuNyw5Ljg5DQoJYzE0Ljk2LDQuMDMsMjkuNTksMy4xOSw0My40OS0zLjk4YzEyLjUtNi40NSwyMC40LTE2LjU0LDIxLjMtMzAuODVjMC42Ny0xMC43OSwwLjI3LTIxLjY2LDAuMzQtMzIuNDkNCglDMjM1LjU3LDIyMi4zNSwyMzUuNTcsMjIxLjc5LDIzNS41NywyMjEuOTF6Ii8+DQo8cGF0aCBjbGFzcz0ic3QwIiBkPSJNMTUwLjc3LDE0OS44MmMtMC4zLTExLjc5LDAuOTMtMjMuMzcsNS41NS0zNC4zNWM1LjQzLTEyLjg5LDE0LjkxLTIwLjc0LDI4LjktMjIuODgNCgljOS42Ny0xLjQ4LDE5LjA4LTAuODUsMjcuOTksMy40N2M4LjksNC4zMiwxNC4zOSwxMS43LDE3Ljk5LDIwLjY0YzMuNTMsOC43NSw1LjMzLDE3Ljk0LDUuNDYsMjcuMzFjMC4xMiw4LjM4LDAuMDksMTYuODktMS4yLDI1LjE0DQoJYy0yLjU5LDE2LjU3LTE0LjA5LDMyLjA4LTMxLjczLDM2LjRjLTE2LjA5LDMuOTQtMzIuNjktMi4yMy00Mi4yOC0xNS44MWMtNi43NC05LjUzLTkuODUtMjAuMjEtMTAuNjYtMzEuNjgNCglDMTUwLjYxLDE1NS4zMywxNTAuNzcsMTUyLjU3LDE1MC43NywxNDkuODJ6Ii8+DQo8L3N2Zz4NCg=='
tags:
- source
- steamcmd
features:
- gsl_token
- steam_disk_space
docker_images:
Source: 'ghcr.io/pelican-eggs/games:source'
file_denylist: { }
startup_commands:
Default: './srcds_run -game garrysmod -console -port {{SERVER_PORT}} +ip 0.0.0.0 +host_workshop_collection {{WORKSHOP_ID}} +map {{SRCDS_MAP}} +gamemode {{GAMEMODE}} -strictportbind -norestart +sv_setsteamaccount {{STEAM_ACC}} +maxplayers {{MAX_PLAYERS}} -tickrate {{TICKRATE}} $( [ "$LUA_REFRESH" == "1" ] || printf %s ''-disableluarefresh'' )'
config:
files: { }
startup:
done: 'gameserver Steam ID'
logs: { }
stop: quit
scripts:
installation:
script: |-
#!/bin/bash
# steamcmd Base Installation Script
#
# Server Files: /mnt/server
## just in case someone removed the defaults.
if [ "${STEAM_USER}" == "" ]; then
echo -e "steam user is not set.
"
echo -e "Using anonymous user.
"
STEAM_USER=anonymous
STEAM_PASS=""
STEAM_AUTH=""
else
echo -e "user set to ${STEAM_USER}"
fi
## download and install steamcmd
cd /tmp
mkdir -p /mnt/server/steamcmd
curl -sSL -o steamcmd.tar.gz https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz
tar -xzvf steamcmd.tar.gz -C /mnt/server/steamcmd
mkdir -p /mnt/server/steamapps # Fix steamcmd disk write error when this folder is missing
cd /mnt/server/steamcmd
# SteamCMD fails otherwise for some reason, even running as root.
# This is changed at the end of the install process anyways.
chown -R root:root /mnt
export HOME=/mnt/server
## install game using steamcmd
./steamcmd.sh +force_install_dir /mnt/server +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} $( [[ "${WINDOWS_INSTALL}" == "1" ]] && printf %s '+@sSteamCmdForcePlatformType windows' ) +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} validate +quit ## other flags may be needed depending on install. looking at you cs 1.6
## set up 32 bit libraries
mkdir -p /mnt/server/.steam/sdk32
cp -v linux32/steamclient.so ../.steam/sdk32/steamclient.so
## set up 64 bit libraries
mkdir -p /mnt/server/.steam/sdk64
cp -v linux64/steamclient.so ../.steam/sdk64/steamclient.so
# Creating needed default files for the game
cd /mnt/server/garrysmod/lua/autorun/server
echo '
-- Docs: https://wiki.garrysmod.com/page/resource/AddWorkshop
-- Place the ID of the workshop addon you want to be downloaded to people who join your server, not the collection ID
-- Use https://beta.configcreator.com/create/gmod/resources.lua to easily create a list based on your collection ID
resource.AddWorkshop( "" )
' > workshop.lua
cd /mnt/server/garrysmod/cfg
echo '
// Please do not set RCon in here, use the startup parameters.
hostname "New Gmod Server"
sv_password ""
sv_loadingurl ""
sv_downloadurl ""
// Steam Server List Settings
// sv_location "eu"
sv_region "255"
sv_lan "0"
sv_max_queries_sec_global "30000"
sv_max_queries_window "45"
sv_max_queries_sec "5"
// Server Limits
sbox_maxprops 100
sbox_maxragdolls 5
sbox_maxnpcs 10
sbox_maxballoons 10
sbox_maxeffects 10
sbox_maxdynamite 10
sbox_maxlamps 10
sbox_maxthrusters 10
sbox_maxwheels 10
sbox_maxhoverballs 10
sbox_maxvehicles 20
sbox_maxbuttons 10
sbox_maxsents 20
sbox_maxemitters 5
sbox_godmode 0
sbox_noclip 0
// Network Settings - Please keep these set to default.
sv_minrate 75000
sv_maxrate 0
gmod_physiterations 2
net_splitpacket_maxrate 45000
decalfrequency 12
// Execute Ban Files - Please do not edit
exec banned_ip.cfg
exec banned_user.cfg
// Add custom lines under here
' > server.cfg
container: 'ghcr.io/pelican-eggs/installers:debian'
entrypoint: bash
variables:
-
name: Gamemode
description: 'The gamemode of your server.'
env_variable: GAMEMODE
default_value: sandbox
user_viewable: true
user_editable: true
rules:
- required
- string
sort: 5
-
name: 'Lua Refresh'
description: "0 = disable Lua refresh,\r\n1 = enable Lua refresh"
env_variable: LUA_REFRESH
default_value: 0
user_viewable: true
user_editable: true
rules:
- boolean
sort: 8
-
name: Map
description: 'The default map for the server.'
env_variable: SRCDS_MAP
default_value: gm_flatgrass
user_viewable: true
user_editable: true
rules:
- required
- string
- alpha_dash
sort: 1
-
name: 'Max Players'
description: 'The maximum amount of players allowed on your game server.'
env_variable: MAX_PLAYERS
default_value: 32
user_viewable: true
user_editable: true
rules:
- required
- integer
- 'max:128'
sort: 6
-
name: 'Source AppID'
description: 'Required for game to update on server restart. Do not modify this.'
env_variable: SRCDS_APPID
default_value: 4020
user_viewable: false
user_editable: false
rules:
- required
- string
- 'max:20'
sort: 3
-
name: 'Steam Account Token'
description: 'The Steam Account Token required for the server to be displayed publicly.'
env_variable: STEAM_ACC
default_value: ''
user_viewable: true
user_editable: true
rules:
- nullable
- string
- alpha_num
- 'size:32'
sort: 2
-
name: Tickrate
description: "The tickrate defines how fast the server will update each entity's location."
env_variable: TICKRATE
default_value: 22
user_viewable: true
user_editable: true
rules:
- required
- integer
- 'max:100'
sort: 7
-
name: 'Workshop ID'
description: 'The ID of your workshop collection (the numbers at the end of the URL)'
env_variable: WORKSHOP_ID
default_value: ''
user_viewable: true
user_editable: true
rules:
- nullable
- integer
sort: 4

File diff suppressed because one or more lines are too long

View File

@@ -1,124 +0,0 @@
_comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL'
meta:
version: PLCN_v3
update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/source-engine/egg-team-fortress2.yaml'
exported_at: '2025-10-31T12:31:09+00:00'
name: 'Team Fortress 2'
author: panel@example.com
uuid: 7f8eb681-b2c8-4bf8-b9f4-d79ff70b6e5d
description: |-
Team Fortress 2 is a team-based first-person shooter multiplayer video game developed and published
by Valve Corporation. It is the sequel to the 1996 mod Team Fortress for Quake and its 1999 remake.
image: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNi4wLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iNTAwcHgiIGhlaWdodD0iNTAwLjAwOXB4IiB2aWV3Qm94PSItNTAgLTUwLjAwNSA1MDAgNTAwLjAwOSIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAtNTAgLTUwLjAwNSA1MDAgNTAwLjAwOSINCgkgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8bGluZWFyR3JhZGllbnQgaWQ9IlNWR0lEXzFfIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjI1NS4zOTk0IiB5MT0iLTQzMy44NDM4IiB4Mj0iMTQ0LjU5ODkiIHkyPSI1My44NDUxIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIDAgMTApIj4NCgk8c3RvcCAgb2Zmc2V0PSIwIiBzdHlsZT0ic3RvcC1jb2xvcjojNUQxRjBFIi8+DQoJPHN0b3AgIG9mZnNldD0iMSIgc3R5bGU9InN0b3AtY29sb3I6I0QwOTczNyIvPg0KPC9saW5lYXJHcmFkaWVudD4NCjxwYXRoIGZpbGw9InVybCgjU1ZHSURfMV8pIiBkPSJNMjYyLjA0NC00Mi4yMzlDMzcwLjA5Ny0xNC42NDQsNDUwLDgzLjM0NCw0NTAsMTk5Ljk5OGMwLDIuNjYxLTAuMDU3LDUuMzExLTAuMTQzLDcuOTUxDQoJbC0xNjguMTUxLTIzLjkxNmMtNC45MjItMjUuMzM4LTIxLjMzMy00Ni41NTYtNDMuNTk0LTU4LjA0NUwyNjIuMDQ0LTQyLjIzOXogTTEyNS45OSwxNjEuODgNCgljMTEuNDg4LTIyLjI2MSwzMi43MDctMzguNjcsNTguMDQzLTQzLjU5M2wyMy45Mi0xNjguMTUzYy0yLjY0My0wLjA4My01LjI5LTAuMTM5LTcuOTUzLTAuMTM5DQoJYy0xMTYuNjUyLDAtMjE0LjYzOSw3OS44OTgtMjQyLjIzNSwxODcuOTUzTDEyNS45OSwxNjEuODh6IE0xNjEuODgzLDI3NC4wMDhjLTIyLjI1OS0xMS40ODktMzguNjctMzIuNzEtNDMuNTkyLTU4LjA0NQ0KCWwtMTY4LjE1Mi0yMy45MmMtMC4wODMsMi42NDMtMC4xMzksNS4yOTMtMC4xMzksNy45NTVjMCwxMTYuNjQ4LDc5Ljg5OCwyMTQuNjM3LDE4Ny45NTIsMjQyLjIzM0wxNjEuODgzLDI3NC4wMDh6IE0yNzQuMDEsMjM4LjExMw0KCWMtMTEuNDksMjIuMjYxLTMyLjcwNywzOC42NjktNTguMDQ2LDQzLjU5M2wtMjMuOTE5LDE2OC4xNThjMi42NDMsMC4wODMsNS4yOTIsMC4xNCw3Ljk1NCwwLjE0DQoJYzExNi42NTMsMCwyMTQuNjQtNzkuOTAxLDI0Mi4yMzItMTg3Ljk1NUwyNzQuMDEsMjM4LjExM3oiLz4NCjxyYWRpYWxHcmFkaWVudCBpZD0iU1ZHSURfMl8iIGN4PSI5OC4xOTE5IiBjeT0iLTE5OC4zNTYiIHI9IjQyNS45ODcxIiBmeD0iOTIuNzU2NSIgZnk9Ii0xOTkuNDgzNCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgwLjk3NzQgMC4yMTE0IDAuMTI2NiAtMC41ODUxIC03Ny4wMzAxIC0xNjcuMzcwNykiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4NCgk8c3RvcCAgb2Zmc2V0PSIwIiBzdHlsZT0ic3RvcC1jb2xvcjojRkZGRkZGO3N0b3Atb3BhY2l0eTowIi8+DQoJPHN0b3AgIG9mZnNldD0iMC40NjU3IiBzdHlsZT0ic3RvcC1jb2xvcjojRkZGRkZGO3N0b3Atb3BhY2l0eTowLjA4MDQiLz4NCgk8c3RvcCAgb2Zmc2V0PSIwLjk4NjEiIHN0eWxlPSJzdG9wLWNvbG9yOiNGRkZGRkY7c3RvcC1vcGFjaXR5OjAuMjE0MyIvPg0KCTxzdG9wICBvZmZzZXQ9IjEiIHN0eWxlPSJzdG9wLWNvbG9yOiNGRkZGRkY7c3RvcC1vcGFjaXR5OjAiLz4NCjwvcmFkaWFsR3JhZGllbnQ+DQo8cGF0aCBmaWxsPSJ1cmwoI1NWR0lEXzJfKSIgZD0iTTI2Mi4wNDQtNDIuMjM5QzM3MC4wOTctMTQuNjQ0LDQ1MCw4My4zNDQsNDUwLDE5OS45OThjMCwyLjY2MS0wLjA1Nyw1LjMxMS0wLjE0Myw3Ljk1MQ0KCWwtMTY4LjE1MS0yMy45MTZjLTQuOTIyLTI1LjMzOC0yMS4zMzMtNDYuNTU2LTQzLjU5NC01OC4wNDVMMjYyLjA0NC00Mi4yMzl6IE0xMjUuOTksMTYxLjg4DQoJYzExLjQ4OC0yMi4yNjEsMzIuNzA3LTM4LjY3LDU4LjA0My00My41OTNsMjMuOTItMTY4LjE1M2MtMi42NDMtMC4wODMtNS4yOS0wLjEzOS03Ljk1My0wLjEzOQ0KCWMtMTE2LjY1MiwwLTIxNC42MzksNzkuODk4LTI0Mi4yMzUsMTg3Ljk1M0wxMjUuOTksMTYxLjg4eiBNMTYxLjg4MywyNzQuMDA4Yy0yMi4yNTktMTEuNDg5LTM4LjY3LTMyLjcxLTQzLjU5Mi01OC4wNDUNCglsLTE2OC4xNTItMjMuOTJjLTAuMDgzLDIuNjQzLTAuMTM5LDUuMjkzLTAuMTM5LDcuOTU1YzAsMTE2LjY0OCw3OS44OTgsMjE0LjYzNywxODcuOTUyLDI0Mi4yMzNMMTYxLjg4MywyNzQuMDA4eiBNMjc0LjAxLDIzOC4xMTMNCgljLTExLjQ5LDIyLjI2MS0zMi43MDcsMzguNjY5LTU4LjA0Niw0My41OTNsLTIzLjkxOSwxNjguMTU4YzIuNjQzLDAuMDgzLDUuMjkyLDAuMTQsNy45NTQsMC4xNA0KCWMxMTYuNjUzLDAsMjE0LjY0LTc5LjkwMSwyNDIuMjMyLTE4Ny45NTVMMjc0LjAxLDIzOC4xMTN6Ii8+DQo8L3N2Zz4NCg=='
tags:
- source
- steamcmd
features:
- gsl_token
- steam_disk_space
docker_images:
Source: 'ghcr.io/pelican-eggs/games:source'
file_denylist: { }
startup_commands:
Default: './srcds_run -game tf -console -port {{SERVER_PORT}} +map {{SRCDS_MAP}} +ip 0.0.0.0 -strictportbind -norestart +sv_setsteamaccount {{STEAM_ACC}}'
config:
files: { }
startup:
done: 'gameserver Steam ID'
logs: { }
stop: quit
scripts:
installation:
script: |-
#!/bin/bash
# steamcmd Base Installation Script
#
# Server Files: /mnt/server
# Image to install with is 'debian:buster-slim'
##
#
# Variables
# STEAM_USER, STEAM_PASS, STEAM_AUTH - Steam user setup. If a user has 2fa enabled it will most likely fail due to timeout. Leave blank for anon install.
# WINDOWS_INSTALL - if it's a windows server you want to install set to 1
# SRCDS_APPID - steam app id ffound here - https://developer.valvesoftware.com/wiki/Dedicated_Servers_List
# EXTRA_FLAGS - when a server has extra glas for things like beta installs or updates.
#
##
## just in case someone removed the defaults.
if [ "${STEAM_USER}" == "" ]; then
echo -e "steam user is not set.
"
echo -e "Using anonymous user.
"
STEAM_USER=anonymous
STEAM_PASS=""
STEAM_AUTH=""
else
echo -e "user set to ${STEAM_USER}"
fi
## download and install steamcmd
cd /tmp
mkdir -p /mnt/server/steamcmd
curl -sSL -o steamcmd.tar.gz https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz
tar -xzvf steamcmd.tar.gz -C /mnt/server/steamcmd
mkdir -p /mnt/server/steamapps # Fix steamcmd disk write error when this folder is missing
cd /mnt/server/steamcmd
# SteamCMD fails otherwise for some reason, even running as root.
# This is changed at the end of the install process anyways.
chown -R root:root /mnt
export HOME=/mnt/server
## install game using steamcmd
./steamcmd.sh +force_install_dir /mnt/server +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} $( [[ "${WINDOWS_INSTALL}" == "1" ]] && printf %s '+@sSteamCmdForcePlatformType windows' ) +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} validate +quit ## other flags may be needed depending on install. looking at you cs 1.6
## set up 32 bit libraries
mkdir -p /mnt/server/.steam/sdk32
cp -v linux32/steamclient.so ../.steam/sdk32/steamclient.so
## set up 64 bit libraries
mkdir -p /mnt/server/.steam/sdk64
cp -v linux64/steamclient.so ../.steam/sdk64/steamclient.so
container: 'ghcr.io/pelican-eggs/installers:debian'
entrypoint: bash
variables:
-
name: 'Default Map'
description: 'The default map to use when starting the server.'
env_variable: SRCDS_MAP
default_value: cp_dustbowl
user_viewable: true
user_editable: true
rules:
- required
- 'regex:/^(\w{1,20})$/'
sort: 2
-
name: 'Game ID'
description: 'The ID corresponding to the game to download and run using SRCDS.'
env_variable: SRCDS_APPID
default_value: 232250
user_viewable: true
user_editable: false
rules:
- required
- 'in:232250'
sort: 1
-
name: Steam
description: |-
The Steam Game Server Login Token to display servers publicly. Generate one at
https://steamcommunity.com/dev/managegameservers
env_variable: STEAM_ACC
default_value: ''
user_viewable: true
user_editable: true
rules:
- required
- string
- alpha_num
- 'size:32'
sort: 3

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
DB::table('allocations')
->whereNull('server_id')
->update(['notes' => null]);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
// Not needed
}
};

View File

@@ -61,10 +61,7 @@ return [
'tags' => 'Tags',
'upload_limit' => 'Upload Limit',
'upload_limit_help' => [
'Enter the maximum size of files that can be uploaded through the web-based file manager.',
'Make sure your webserver supports file uploads of this size!',
],
'upload_limit_help' => 'Enter the maximum size of files that can be uploaded through the web-based file manager.',
'sftp_port' => 'SFTP Port',
'sftp_alias' => 'SFTP Alias',
'sftp_alias_help' => 'Display alias for the SFTP address. Leave empty to use the Node FQDN.',

View File

@@ -46,9 +46,12 @@ return [
'notifications' => [
'installed' => 'Plugin installed',
'install_error' => 'Could not install plugin',
'uninstalled' => 'Plugin uninstalled',
'uninstall_error' => 'Could not uninstall plugin',
'deleted' => 'Plugin deleted',
'updated' => 'Plugin updated',
'update_error' => 'Could not update plugin',
'enabled' => 'Plugin enabled',
'disabled' => 'Plugin disabled',
'imported' => 'Plugin imported',

View File

@@ -61,6 +61,17 @@ return [
'migration' => 'Migrations failed',
],
],
'egg' => [
'title' => 'Eggs',
'no_eggs' => 'No Eggs Available',
'background_install_started' => 'Egg Install Started',
'background_install_description' => 'Install of :count eggs has been queued and will continue in the background.',
'exceptions' => [
'failed_to_update' => 'Failed to update egg index',
'no_eggs' => 'No eggs are available to install at this time.',
'installation_failed' => 'Failed to install selected eggs. Please import them after the installation via the egg list.',
],
],
'session' => [
'title' => 'Session',
'driver' => 'Session Driver',

View File

@@ -12,6 +12,13 @@ return [
'2fa' => '2FA',
'customization' => 'Customization',
],
'activity' => [
'timestamp' => 'Timestamp',
'actor' => 'Actor',
'ip' => 'IP Address',
'event' => 'Event',
'metadata' => 'Metadata',
],
'username' => 'Username',
'admin' => 'Admin',
'exit_admin' => 'Exit Admin',

View File

@@ -11,6 +11,7 @@ return [
'username' => 'Username',
'password' => 'Password',
'remote' => 'Remote',
'unlimited' => 'Unlimited',
'created_at' => 'Created at',
'name' => 'Database Name',
'name_hint' => 'Leaving this blank will auto generate a random name',

View File

@@ -0,0 +1 @@
{{ $this->table }}

View File

@@ -1,5 +1,6 @@
<?php
use App\Enums\ResourceLimit;
use App\Http\Controllers\Api\Client;
use App\Http\Middleware\Activity\AccountSubject;
use App\Http\Middleware\Activity\ServerSubject;
@@ -50,7 +51,9 @@ Route::prefix('/account')->middleware(AccountSubject::class)->group(function ()
*/
Route::prefix('/servers/{server:uuid}')->middleware([ServerSubject::class, AuthenticateServerAccess::class, ResourceBelongsToServer::class])->group(function () {
Route::get('/', [Client\Servers\ServerController::class, 'index'])->name('api:client:server.view');
Route::get('/websocket', Client\Servers\WebsocketController::class)->name('api:client:server.ws');
Route::middleware([ResourceLimit::Websocket->middleware()])
->get('/websocket', Client\Servers\WebsocketController::class)
->name('api:client:server.ws');
Route::get('/resources', Client\Servers\ResourceUtilizationController::class)->name('api:client:server.resources');
Route::get('/activity', Client\Servers\ActivityLogController::class)->name('api:client:server.activity');
@@ -59,7 +62,8 @@ Route::prefix('/servers/{server:uuid}')->middleware([ServerSubject::class, Authe
Route::prefix('/databases')->group(function () {
Route::get('/', [Client\Servers\DatabaseController::class, 'index']);
Route::post('/', [Client\Servers\DatabaseController::class, 'store']);
Route::middleware([ResourceLimit::DatabaseCreate->middleware()])
->post('/', [Client\Servers\DatabaseController::class, 'store']);
Route::post('/{database}/rotate-password', [Client\Servers\DatabaseController::class, 'rotatePassword']);
Route::delete('/{database}', [Client\Servers\DatabaseController::class, 'delete']);
});
@@ -76,13 +80,15 @@ Route::prefix('/servers/{server:uuid}')->middleware([ServerSubject::class, Authe
Route::post('/delete', [Client\Servers\FileController::class, 'delete']);
Route::post('/create-folder', [Client\Servers\FileController::class, 'create']);
Route::post('/chmod', [Client\Servers\FileController::class, 'chmod']);
Route::post('/pull', [Client\Servers\FileController::class, 'pull'])->middleware(['throttle:10,5']);
Route::middleware([ResourceLimit::FilePull->middleware()])
->post('/pull', [Client\Servers\FileController::class, 'pull']);
Route::get('/upload', Client\Servers\FileUploadController::class);
});
Route::prefix('/schedules')->group(function () {
Route::get('/', [Client\Servers\ScheduleController::class, 'index']);
Route::post('/', [Client\Servers\ScheduleController::class, 'store']);
Route::middleware([ResourceLimit::ScheduleCreate->middleware()])
->post('/', [Client\Servers\ScheduleController::class, 'store']);
Route::get('/{schedule}', [Client\Servers\ScheduleController::class, 'view']);
Route::post('/{schedule}', [Client\Servers\ScheduleController::class, 'update']);
Route::post('/{schedule}/execute', [Client\Servers\ScheduleController::class, 'execute']);
@@ -95,7 +101,8 @@ Route::prefix('/servers/{server:uuid}')->middleware([ServerSubject::class, Authe
Route::prefix('/network/allocations')->group(function () {
Route::get('/', [Client\Servers\NetworkAllocationController::class, 'index']);
Route::post('/', [Client\Servers\NetworkAllocationController::class, 'store']);
Route::middleware([ResourceLimit::AllocationCreate->middleware()])
->post('/', [Client\Servers\NetworkAllocationController::class, 'store']);
Route::post('/{allocation}', [Client\Servers\NetworkAllocationController::class, 'update']);
Route::post('/{allocation}/primary', [Client\Servers\NetworkAllocationController::class, 'setPrimary']);
Route::delete('/{allocation}', [Client\Servers\NetworkAllocationController::class, 'delete']);
@@ -103,7 +110,8 @@ Route::prefix('/servers/{server:uuid}')->middleware([ServerSubject::class, Authe
Route::prefix('/users')->group(function () {
Route::get('/', [Client\Servers\SubuserController::class, 'index']);
Route::post('/', [Client\Servers\SubuserController::class, 'store']);
Route::middleware([ResourceLimit::SubuserCreate->middleware()])
->post('/', [Client\Servers\SubuserController::class, 'store']);
Route::get('/{user:uuid}', [Client\Servers\SubuserController::class, 'view']);
Route::post('/{user:uuid}', [Client\Servers\SubuserController::class, 'update']);
Route::delete('/{user:uuid}', [Client\Servers\SubuserController::class, 'delete']);
@@ -116,7 +124,8 @@ Route::prefix('/servers/{server:uuid}')->middleware([ServerSubject::class, Authe
Route::get('/{backup:uuid}/download', [Client\Servers\BackupController::class, 'download']);
Route::put('/{backup:uuid}/rename', [Client\Servers\BackupController::class, 'rename']);
Route::post('/{backup:uuid}/lock', [Client\Servers\BackupController::class, 'toggleLock']);
Route::post('/{backup:uuid}/restore', [Client\Servers\BackupController::class, 'restore']);
Route::middleware([ResourceLimit::BackupRestore->middleware()])
->post('/{backup:uuid}/restore', [Client\Servers\BackupController::class, 'restore']);
Route::delete('/{backup:uuid}', [Client\Servers\BackupController::class, 'delete']);
});

View File

@@ -8,8 +8,9 @@ While Pelican is in beta, we only provide security fixes for the most recent bet
## Reporting a Vulnerability
Please report any vulnerabilities via _one_ of the following methods:
- [Create a security advisory on Github](https://github.com/pelican-dev/panel/security/advisories/new)
- Send an e-mail to team@pelican.dev
- [Create a security advisory on GitHub](https://github.com/pelican-dev/panel/security/advisories/new)
- Send an e-mail to <team@pelican.dev>
Include steps to reproduce, affected versions, impact, and a proof of concept if available.

View File

@@ -42,7 +42,7 @@ class DeleteSubuserTest extends ClientApiIntegrationTestCase
'permissions' => [SubuserPermission::WebsocketConnect],
]);
$mock->expects('setServer->revokeUserJTI')->with($subuser->id)->andReturnUndefined();
$mock->expects('setServer->deauthorize')->with($subuser->uuid)->andReturnUndefined();
$this->actingAs($user)->deleteJson($this->link($server) . "/users/$subuser->uuid")->assertNoContent();
@@ -58,7 +58,7 @@ class DeleteSubuserTest extends ClientApiIntegrationTestCase
'permissions' => [SubuserPermission::WebsocketConnect],
]);
$mock->expects('setServer->revokeUserJTI')->with($subuser->id)->andReturnUndefined();
$mock->expects('setServer->deauthorize')->with($subuser->uuid)->andReturnUndefined();
$this->actingAs($user)->deleteJson($this->link($server) . "/users/$subuser->uuid")->assertNoContent();
}

View File

@@ -37,7 +37,7 @@ class SubuserAuthorizationTest extends ClientApiIntegrationTestCase
$this->instance(DaemonServerRepository::class, $mock = \Mockery::mock(DaemonServerRepository::class));
if ($method === 'DELETE') {
$mock->expects('setServer->revokeUserJTI')->with($internal->id)->andReturnUndefined();
$mock->expects('setServer->deauthorize')->with($internal->uuid)->andReturnUndefined();
}
// This route is acceptable since they're accessing a subuser on their own server.

View File

@@ -6,6 +6,7 @@ use App\Enums\SubuserPermission;
use App\Models\Subuser;
use App\Models\User;
use App\Tests\Integration\Api\Client\ClientApiIntegrationTestCase;
use Illuminate\Support\Facades\Context;
use Illuminate\Support\Facades\Http;
class UpdateSubuserTest extends ClientApiIntegrationTestCase
@@ -41,6 +42,9 @@ class UpdateSubuserTest extends ClientApiIntegrationTestCase
$this->actingAs($subuser->user)->postJson($endpoint, $data)->assertForbidden();
$this->actingAs($user)->postJson($endpoint, $data)->assertForbidden();
// When running the tests, the context is function-scoped instead of request-scoped, so we have to flush it
Context::flush();
$server->subusers()->where('user_id', $user->id)->update([
'permissions' => [
SubuserPermission::UserUpdate,

View File

@@ -70,7 +70,7 @@ class StartupModificationServiceTest extends IntegrationTestCase
public function test_server_is_properly_modified_as_admin_user(): void
{
/** @var \App\Models\Egg $nextEgg */
$nextEgg = Egg::query()->findOrFail(6);
$nextEgg = Egg::query()->findOrFail(2);
$server = $this->createServerModel(['egg_id' => 1]);

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Tests\Seeders;
use App\Exceptions\Service\InvalidFileUploadException;
use App\Services\Eggs\Sharing\EggImporterService;
use DirectoryIterator;
use Illuminate\Http\UploadedFile;
use Throwable;
class EggSeeder
{
/**
* @throws InvalidFileUploadException|Throwable
*/
public function run(): void
{
// @phpstan-ignore myCustomRules.forbiddenGlobalFunctions
$importer = app(EggImporterService::class);
$path = base_path('tests/_fixtures');
$files = new DirectoryIterator($path);
/** @var DirectoryIterator $file */
foreach ($files as $file) {
if (!$file->isFile() || !$file->isReadable()) {
continue;
}
$filePath = $file->getRealPath();
$uploaded = new UploadedFile($filePath, basename($filePath));
$importer->fromFile($uploaded);
}
}
}

View File

@@ -2,8 +2,10 @@
namespace App\Tests;
use App\Tests\Seeders\EggSeeder;
use Carbon\Carbon;
use Carbon\CarbonImmutable;
use Exception;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Spatie\Permission\PermissionRegistrar;
@@ -35,6 +37,13 @@ abstract class TestCase extends BaseTestCase
$this->setKnownUuidFactory();
$this->app->make(PermissionRegistrar::class)->forgetCachedPermissions();
try {
$seeder = new EggSeeder();
$seeder->run();
} catch (Exception) {
// Don't fail all tests if the fixture/ seeder isn't present or import fails.
}
}
/**

View File

@@ -2,7 +2,7 @@ _comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL'
meta:
version: PLCN_v3
update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/minecraft/egg-bungeecord.yaml'
exported_at: '2025-10-31T12:25:34+00:00'
exported_at: '2025-12-28T12:14:41+00:00'
name: Bungeecord
author: panel@example.com
uuid: 9e6b409e-4028-4947-aea8-50a2c404c271
@@ -26,7 +26,7 @@ docker_images:
'Java 16': 'ghcr.io/pelican-eggs/yolks:java_16'
'Java 11': 'ghcr.io/pelican-eggs/yolks:java_11'
'Java 8': 'ghcr.io/pelican-eggs/yolks:java_8'
file_denylist: { }
file_denylist: { }
startup_commands:
Default: 'java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}'
config:
@@ -40,7 +40,7 @@ config:
'regex:^(127\.0\.0\.1|localhost)(:\d{1,5})?$': '{{config.docker.interface}}$2'
startup:
done: 'Listening on '
logs: { }
logs: { }
stop: end
scripts:
installation:
@@ -56,12 +56,11 @@ scripts:
BUNGEE_VERSION="lastStableBuild"
fi
curl -o ${SERVER_JARFILE} https://ci.md-5.net/job/BungeeCord/${BUNGEE_VERSION}/artifact/bootstrap/target/BungeeCord.jar
curl -sSL -o ${SERVER_JARFILE} https://ci.md-5.net/job/BungeeCord/${BUNGEE_VERSION}/artifact/bootstrap/target/BungeeCord.jar
container: 'ghcr.io/pelican-eggs/installers:alpine'
entrypoint: ash
variables:
-
name: 'Bungeecord Jar File'
- name: 'Bungeecord Jar File'
description: 'The name of the Jarfile to use when running Bungeecord.'
env_variable: SERVER_JARFILE
default_value: bungeecord.jar
@@ -71,8 +70,7 @@ variables:
- required
- 'regex:/^([\w\d._-]+)(\.jar)$/'
sort: 2
-
name: 'Bungeecord Version'
- name: 'Bungeecord Version'
description: 'The version of Bungeecord to download and use.'
env_variable: BUNGEE_VERSION
default_value: latest

View File

@@ -24,7 +24,7 @@ docker_images:
'Java 16': 'ghcr.io/pelican-eggs/yolks:java_16'
'Java 11': 'ghcr.io/pelican-eggs/yolks:java_11'
'Java 8': 'ghcr.io/pelican-eggs/yolks:java_8'
file_denylist: { }
file_denylist: { }
startup_commands:
Default: 'java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}'
config:
@@ -37,7 +37,7 @@ config:
query.port: '{{server.allocations.default.port}}'
startup:
done: ')! For help, type '
logs: { }
logs: { }
stop: stop
scripts:
installation:
@@ -72,8 +72,7 @@ scripts:
container: 'ghcr.io/pelican-eggs/installers:alpine'
entrypoint: ash
variables:
-
name: 'Server Jar File'
- name: 'Server Jar File'
description: 'The name of the server jarfile to run the server with.'
env_variable: SERVER_JARFILE
default_value: server.jar
@@ -83,8 +82,7 @@ variables:
- required
- 'regex:/^([\w\d._-]+)(\.jar)$/'
sort: 1
-
name: 'Server Version'
- name: 'Server Version'
description: |-
The version of Minecraft Vanilla to install. Use "latest" to install the latest version, or use
"snapshot" to install the latest snapshot. Go to Settings > Reinstall Server to apply.