Merge branch 'main' into issue/68

# Conflicts:
#	app/Filament/Resources/ServerResource/Pages/CreateServer.php
#	app/Models/Node.php
This commit is contained in:
Lance Pioch 2024-06-09 15:42:49 -04:00
commit 4319f24f51
76 changed files with 1787 additions and 1097 deletions

View File

@ -30,3 +30,8 @@ MAIL_FROM_NAME="Pelican Admin"
SESSION_ENCRYPT=false SESSION_ENCRYPT=false
SESSION_PATH=/ SESSION_PATH=/
SESSION_DOMAIN=null SESSION_DOMAIN=null
# Set this to true, and set start & end ports to auto create allocations.
PANEL_CLIENT_ALLOCATIONS_ENABLED=false
PANEL_CLIENT_ALLOCATIONS_RANGE_START=
PANEL_CLIENT_ALLOCATIONS_RANGE_END=

View File

@ -59,7 +59,7 @@ jobs:
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
with: with:
php-version: ${{ matrix.php }} php-version: ${{ matrix.php }}
extensions: bcmath, cli, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2 tools: composer:v2
coverage: none coverage: none
@ -119,7 +119,7 @@ jobs:
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
with: with:
php-version: ${{ matrix.php }} php-version: ${{ matrix.php }}
extensions: bcmath, cli, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2 tools: composer:v2
coverage: none coverage: none

1
.gitignore vendored
View File

@ -4,6 +4,7 @@
/public/hot /public/hot
/public/storage /public/storage
/storage/*.key /storage/*.key
/storage/clockwork/*
/vendor /vendor
*.DS_Store* *.DS_Store*
.env .env

View File

@ -118,7 +118,9 @@ class AppSettingsCommand extends Command
} }
if ($this->variables['QUEUE_CONNECTION'] !== 'sync') { if ($this->variables['QUEUE_CONNECTION'] !== 'sync') {
Artisan::call('p:environment:queue-service', $redisUsed ? ['--use-redis'] : []); $this->call('p:environment:queue-service', [
'--use-redis' => $redisUsed,
]);
} }
$this->info($this->console->output()); $this->info($this->console->output());
@ -127,7 +129,7 @@ class AppSettingsCommand extends Command
} }
/** /**
* Request connection details and verify them. * Request redis connection details and verify them.
*/ */
private function requestRedisSettings(): void private function requestRedisSettings(): void
{ {

View File

@ -98,7 +98,7 @@ class DatabaseSettingsCommand extends Command
} elseif ($this->variables['DB_CONNECTION'] === 'sqlite') { } elseif ($this->variables['DB_CONNECTION'] === 'sqlite') {
$this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask( $this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask(
'Database Path', 'Database Path',
config('database.connections.sqlite.database', database_path('database.sqlite')) env('DB_DATABASE', 'database.sqlite')
); );
} }

View File

@ -19,17 +19,18 @@ class QueueWorkerServiceCommand extends Command
public function handle(): void public function handle(): void
{ {
$serviceName = $this->option('service-name') ?? $this->ask('Service name', 'pelican-queue'); $serviceName = $this->option('service-name') ?? $this->ask('Queue worker service name', 'pelican-queue');
$path = '/etc/systemd/system/' . $serviceName . '.service'; $path = '/etc/systemd/system/' . $serviceName . '.service';
if (file_exists($path) && !$this->option('overwrite') && !$this->confirm('The service file already exists. Do you want to overwrite it?')) { $fileExists = file_exists($path);
$this->line('Creation of queue worker service file aborted.'); if ($fileExists && !$this->option('overwrite') && !$this->confirm('The service file already exists. Do you want to overwrite it?')) {
$this->line('Creation of queue worker service file aborted because serive file already exists.');
return; return;
} }
$user = $this->option('user') ?? $this->ask('User', 'www-data'); $user = $this->option('user') ?? $this->ask('Webserver User', 'www-data');
$group = $this->option('group') ?? $this->ask('Group', 'www-data'); $group = $this->option('group') ?? $this->ask('Webserver Group', 'www-data');
$afterRedis = $this->option('use-redis') ? '\nAfter=redis-server.service' : ''; $afterRedis = $this->option('use-redis') ? '\nAfter=redis-server.service' : '';
@ -45,7 +46,7 @@ Description=Pelican Queue Service$afterRedis
User=$user User=$user
Group=$group Group=$group
Restart=always Restart=always
ExecStart=/usr/bin/php $basePath/artisan queue:work --queue=high,standard,low --tries=3 ExecStart=/usr/bin/php $basePath/artisan queue:work --tries=3
StartLimitInterval=180 StartLimitInterval=180
StartLimitBurst=30 StartLimitBurst=30
RestartSec=5s RestartSec=5s
@ -60,13 +61,24 @@ WantedBy=multi-user.target
return; return;
} }
$result = Process::run("systemctl enable --now $serviceName.service"); if ($fileExists) {
if ($result->failed()) { $result = Process::run("systemctl restart $serviceName.service");
$this->error('Error enabling service: ' . $result->errorOutput()); if ($result->failed()) {
$this->error('Error restarting service: ' . $result->errorOutput());
return; return;
}
$this->line('Queue worker service file updated successfully.');
} else {
$result = Process::run("systemctl enable --now $serviceName.service");
if ($result->failed()) {
$this->error('Error enabling service: ' . $result->errorOutput());
return;
}
$this->line('Queue worker service file created successfully.');
} }
$this->line('Queue worker service file created successfully.');
} }
} }

View File

@ -26,7 +26,7 @@ class InfoCommand extends Command
{ {
$this->output->title('Version Information'); $this->output->title('Version Information');
$this->table([], [ $this->table([], [
['Panel Version', config('app.version')], ['Panel Version', $this->versionService->versionData()['version']],
['Latest Version', $this->versionService->getPanel()], ['Latest Version', $this->versionService->getPanel()],
['Up-to-Date', $this->versionService->isLatestPanel() ? 'Yes' : $this->formatText('No', 'bg=red')], ['Up-to-Date', $this->versionService->isLatestPanel() ? 'Yes' : $this->formatText('No', 'bg=red')],
], 'compact'); ], 'compact');

View File

@ -25,6 +25,7 @@ class MakeNodeCommand extends Command
{--uploadSize= : Enter the maximum upload filesize.} {--uploadSize= : Enter the maximum upload filesize.}
{--daemonListeningPort= : Enter the daemon listening port.} {--daemonListeningPort= : Enter the daemon listening port.}
{--daemonSFTPPort= : Enter the daemon SFTP listening port.} {--daemonSFTPPort= : Enter the daemon SFTP listening port.}
{--daemonSFTPAlias= : Enter the daemon SFTP alias.}
{--daemonBase= : Enter the base folder.}'; {--daemonBase= : Enter the base folder.}';
protected $description = 'Creates a new node on the system via the CLI.'; protected $description = 'Creates a new node on the system via the CLI.';
@ -65,6 +66,7 @@ class MakeNodeCommand extends Command
$data['upload_size'] = $this->option('uploadSize') ?? $this->ask(__('commands.make_node.upload_size'), '100'); $data['upload_size'] = $this->option('uploadSize') ?? $this->ask(__('commands.make_node.upload_size'), '100');
$data['daemon_listen'] = $this->option('daemonListeningPort') ?? $this->ask(__('commands.make_node.daemonListen'), '8080'); $data['daemon_listen'] = $this->option('daemonListeningPort') ?? $this->ask(__('commands.make_node.daemonListen'), '8080');
$data['daemon_sftp'] = $this->option('daemonSFTPPort') ?? $this->ask(__('commands.make_node.daemonSFTP'), '2022'); $data['daemon_sftp'] = $this->option('daemonSFTPPort') ?? $this->ask(__('commands.make_node.daemonSFTP'), '2022');
$data['daemon_sftp_alias'] = $this->option('daemonSFTPAlias') ?? $this->ask(__('commands.make_node.daemonSFTPAlias'), '');
$data['daemon_base'] = $this->option('daemonBase') ?? $this->ask(__('commands.make_node.daemonBase'), '/var/lib/pelican/volumes'); $data['daemon_base'] = $this->option('daemonBase') ?? $this->ask(__('commands.make_node.daemonBase'), '/var/lib/pelican/volumes');
$node = $this->creationService->handle($data); $node = $this->creationService->handle($data);

View File

@ -24,7 +24,7 @@ class ProcessRunnableCommand extends Command
->whereRelation('server', fn (Builder $builder) => $builder->whereNull('status')) ->whereRelation('server', fn (Builder $builder) => $builder->whereNull('status'))
->where('is_active', true) ->where('is_active', true)
->where('is_processing', false) ->where('is_processing', false)
->whereDate('next_run_at', '<=', Carbon::now()->toDateString()) ->whereDate('next_run_at', '<=', Carbon::now()->toDateTimeString())
->get(); ->get();
if ($schedules->count() < 1) { if ($schedules->count() < 1) {

View File

@ -30,7 +30,7 @@ class MakeUserCommand extends Command
public function handle(): int public function handle(): int
{ {
try { try {
DB::select('select 1 where 1'); DB::connection()->getPdo();
} catch (Exception $exception) { } catch (Exception $exception) {
$this->error($exception->getMessage()); $this->error($exception->getMessage());

View File

@ -6,9 +6,9 @@ use App\Exceptions\DisplayException;
class TwoFactorAuthenticationTokenInvalid extends DisplayException class TwoFactorAuthenticationTokenInvalid extends DisplayException
{ {
/** public string $title = 'Invalid 2FA Code';
* TwoFactorAuthenticationTokenInvalid constructor. public string $icon = 'tabler-2fa';
*/
public function __construct() public function __construct()
{ {
parent::__construct('The provided two-factor authentication token was not valid.'); parent::__construct('The provided two-factor authentication token was not valid.');

View File

@ -7,6 +7,7 @@ use App\Models\Egg;
use App\Models\Node; use App\Models\Node;
use App\Models\Server; use App\Models\Server;
use App\Models\User; use App\Models\User;
use App\Services\Helpers\SoftwareVersionService;
use Filament\Actions\CreateAction; use Filament\Actions\CreateAction;
use Filament\Pages\Page; use Filament\Pages\Page;
@ -29,8 +30,14 @@ class Dashboard extends Page
public function getViewData(): array public function getViewData(): array
{ {
/** @var SoftwareVersionService $softwareVersionService */
$softwareVersionService = app(SoftwareVersionService::class);
return [ return [
'inDevelopment' => config('app.version') === 'canary', 'inDevelopment' => config('app.version') === 'canary',
'version' => $softwareVersionService->versionData()['version'],
'latestVersion' => $softwareVersionService->getPanel(),
'isLatest' => $softwareVersionService->isLatestPanel(),
'eggsCount' => Egg::query()->count(), 'eggsCount' => Egg::query()->count(),
'nodesList' => ListNodes::getUrl(), 'nodesList' => ListNodes::getUrl(),
'nodesCount' => Node::query()->count(), 'nodesCount' => Node::query()->count(),
@ -43,6 +50,13 @@ class Dashboard extends Page
->icon('tabler-brand-github') ->icon('tabler-brand-github')
->url('https://github.com/pelican-dev/panel/discussions', true), ->url('https://github.com/pelican-dev/panel/discussions', true),
], ],
'updateActions' => [
CreateAction::make()
->label('Read Documentation')
->icon('tabler-clipboard-text')
->url('https://pelican.dev/docs/panel/update', true)
->color('warning'),
],
'nodeActions' => [ 'nodeActions' => [
CreateAction::make() CreateAction::make()
->label(trans('dashboard/index.sections.intro-first-node.button_label')) ->label(trans('dashboard/index.sections.intro-first-node.button_label'))
@ -53,7 +67,7 @@ class Dashboard extends Page
CreateAction::make() CreateAction::make()
->label(trans('dashboard/index.sections.intro-support.button_donate')) ->label(trans('dashboard/index.sections.intro-support.button_donate'))
->icon('tabler-cash') ->icon('tabler-cash')
->url('https://pelican.dev/donate', true) ->url($softwareVersionService->getDonations(), true)
->color('success'), ->color('success'),
], ],
'helpActions' => [ 'helpActions' => [

View File

@ -3,6 +3,7 @@
namespace App\Filament\Resources\DatabaseHostResource\Pages; namespace App\Filament\Resources\DatabaseHostResource\Pages;
use App\Filament\Resources\DatabaseHostResource; use App\Filament\Resources\DatabaseHostResource;
use App\Models\DatabaseHost;
use Filament\Actions; use Filament\Actions;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
use Filament\Forms; use Filament\Forms;
@ -71,7 +72,9 @@ class EditDatabaseHost extends EditRecord
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [
Actions\DeleteAction::make(), Actions\DeleteAction::make()
->label(fn (DatabaseHost $databaseHost) => $databaseHost->databases()->count() > 0 ? 'Database Host Has Databases' : 'Delete')
->disabled(fn (DatabaseHost $databaseHost) => $databaseHost->databases()->count() > 0),
$this->getSaveFormAction()->formId('form'), $this->getSaveFormAction()->formId('form'),
]; ];
} }

View File

@ -8,6 +8,7 @@ use App\Services\Eggs\Sharing\EggImporterService;
use Exception; use Exception;
use Filament\Actions; use Filament\Actions;
use Filament\Forms; use Filament\Forms;
use Filament\Forms\Components\Tabs;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Table; use Filament\Tables\Table;
@ -62,21 +63,58 @@ class ListEggs extends ListRecords
Actions\Action::make('import') Actions\Action::make('import')
->label('Import') ->label('Import')
->form([ ->form([
Forms\Components\FileUpload::make('egg') Tabs::make('Tabs')
->acceptedFileTypes(['application/json']) ->tabs([
->storeFiles(false) Tabs\Tab::make('From File')
->multiple(), ->icon('tabler-file-upload')
->schema([
Forms\Components\FileUpload::make('egg')
->label('Egg')
->hint('This should be the json file ( egg-minecraft.json )')
->acceptedFileTypes(['application/json'])
->storeFiles(false)
->multiple(),
]),
Tabs\Tab::make('From URL')
->icon('tabler-world-upload')
->schema([
Forms\Components\TextInput::make('url')
->label('URL')
->hint('This URL should point to a single json file')
->url(),
]),
])
->contained(false),
]) ])
->action(function (array $data): void { ->action(function (array $data): void {
/** @var TemporaryUploadedFile $eggFile */
$eggFile = $data['egg'];
/** @var EggImporterService $eggImportService */ /** @var EggImporterService $eggImportService */
$eggImportService = resolve(EggImporterService::class); $eggImportService = resolve(EggImporterService::class);
foreach ($eggFile as $file) { if (!empty($data['egg'])) {
/** @var TemporaryUploadedFile[] $eggFile */
$eggFile = $data['egg'];
foreach ($eggFile as $file) {
try {
$eggImportService->fromFile($file);
} catch (Exception $exception) {
Notification::make()
->title('Import Failed')
->danger()
->send();
report($exception);
return;
}
}
}
if (!empty($data['url'])) {
try { try {
$eggImportService->handle($file); $eggImportService->fromUrl($data['url']);
} catch (Exception $exception) { } catch (Exception $exception) {
Notification::make() Notification::make()
->title('Import Failed') ->title('Import Failed')

View File

@ -4,8 +4,10 @@ namespace App\Filament\Resources\NodeResource\Pages;
use App\Filament\Resources\NodeResource; use App\Filament\Resources\NodeResource;
use Filament\Forms; use Filament\Forms;
use Filament\Forms\Components\Tabs; use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Wizard;
use Filament\Resources\Pages\CreateRecord; use Filament\Resources\Pages\CreateRecord;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString; use Illuminate\Support\HtmlString;
class CreateNode extends CreateRecord class CreateNode extends CreateRecord
@ -18,19 +20,19 @@ class CreateNode extends CreateRecord
public function form(Forms\Form $form): Forms\Form public function form(Forms\Form $form): Forms\Form
{ {
return $form->schema([ return $form
Tabs::make('Tabs') ->schema([
->columns([ Wizard::make([
'default' => 2, Wizard\Step::make('basic')
'sm' => 3, ->label('Basic Settings')
'md' => 3,
'lg' => 4,
])
->persistTabInQueryString()
->columnSpanFull()
->tabs([
Tabs\Tab::make('Basic Settings')
->icon('tabler-server') ->icon('tabler-server')
->columnSpanFull()
->columns([
'default' => 2,
'sm' => 3,
'md' => 3,
'lg' => 4,
])
->schema([ ->schema([
Forms\Components\TextInput::make('fqdn') Forms\Components\TextInput::make('fqdn')
->columnSpan(2) ->columnSpan(2)
@ -55,7 +57,7 @@ class CreateNode extends CreateRecord
return " return "
This is the domain name that points to your node's IP Address. This is the domain name that points to your node's IP Address.
If you've already set up this, you can verify it by checking the next field! If you've already set up this, you can verify it by checking the next field!
"; ";
}) })
->hintColor('danger') ->hintColor('danger')
->hint(function ($state) { ->hint(function ($state) {
@ -184,30 +186,17 @@ class CreateNode extends CreateRecord
]) ])
->default(fn () => request()->isSecure() ? 'https' : 'http'), ->default(fn () => request()->isSecure() ? 'https' : 'http'),
]), ]),
Tabs\Tab::make('Advanced Settings') Wizard\Step::make('advanced')
->label('Advanced Settings')
->icon('tabler-server-cog') ->icon('tabler-server-cog')
->columnSpanFull()
->columns([
'default' => 2,
'sm' => 3,
'md' => 3,
'lg' => 4,
])
->schema([ ->schema([
Forms\Components\TextInput::make('upload_size')
->label('Upload Limit')
->helperText('Enter the maximum size of files that can be uploaded through the web-based file manager.')
->columnSpan(1)
->numeric()->required()
->default(256)
->minValue(1)
->maxValue(1024)
->suffix('MiB'),
Forms\Components\ToggleButtons::make('public')
->label('Automatic Allocation')->inline()
->default(true)
->columnSpan(1)
->options([
true => 'Yes',
false => 'No',
])
->colors([
true => 'success',
false => 'danger',
]),
Forms\Components\ToggleButtons::make('maintenance_mode') Forms\Components\ToggleButtons::make('maintenance_mode')
->label('Maintenance Mode')->inline() ->label('Maintenance Mode')->inline()
->columnSpan(1) ->columnSpan(1)
@ -222,13 +211,46 @@ class CreateNode extends CreateRecord
true => 'danger', true => 'danger',
false => 'success', false => 'success',
]), ]),
Forms\Components\ToggleButtons::make('public')
->columnSpan(1)
->label('Automatic Allocation')->inline()
->options([
true => 'Yes',
false => 'No',
])
->colors([
true => 'success',
false => 'danger',
]),
Forms\Components\TagsInput::make('tags') Forms\Components\TagsInput::make('tags')
->label('Tags') ->label('Tags')
->disabled() ->disabled()
->placeholder('Not Implemented') ->placeholder('Not Implemented')
->hintIcon('tabler-question-mark') ->hintIcon('tabler-question-mark')
->hintIconTooltip('Not Implemented') ->hintIconTooltip('Not Implemented')
->columnSpan(1), ->columnSpan(2),
Forms\Components\TextInput::make('upload_size')
->label('Upload Limit')
->helperText('Enter the maximum size of files that can be uploaded through the web-based file manager.')
->columnSpan(1)
->numeric()->required()
->default(256)
->minValue(1)
->maxValue(1024)
->suffix('MiB'),
Forms\Components\TextInput::make('daemon_sftp')
->columnSpan(1)
->label('SFTP Port')
->minValue(0)
->maxValue(65536)
->default(2022)
->required()
->integer(),
Forms\Components\TextInput::make('daemon_sftp_alias')
->columnSpan(2)
->label('SFTP Alias')
->helperText('Display alias for the SFTP address. Leave empty to use the Node FQDN.'),
Forms\Components\Grid::make() Forms\Components\Grid::make()
->columns(6) ->columns(6)
->columnSpanFull() ->columnSpanFull()
@ -353,8 +375,17 @@ class CreateNode extends CreateRecord
->suffix('%'), ->suffix('%'),
]), ]),
]), ]),
]), ])->columnSpanFull()
]); ->nextAction(fn (Action $action) => $action->label('Next Step'))
->submitAction(new HtmlString(Blade::render(<<<'BLADE'
<x-filament::button
type="submit"
size="sm"
>
Create Node
</x-filament::button>
BLADE))),
]);
} }
protected function getRedirectUrlParameters(): array protected function getRedirectUrlParameters(): array
@ -363,4 +394,9 @@ class CreateNode extends CreateRecord
'tab' => '-configuration-tab', 'tab' => '-configuration-tab',
]; ];
} }
protected function getFormActions(): array
{
return [];
}
} }

View File

@ -215,6 +215,18 @@ class EditNode extends EditRecord
->minValue(1) ->minValue(1)
->maxValue(1024) ->maxValue(1024)
->suffix('MiB'), ->suffix('MiB'),
Forms\Components\TextInput::make('daemon_sftp')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
->label('SFTP Port')
->minValue(0)
->maxValue(65536)
->default(2022)
->required()
->integer(),
Forms\Components\TextInput::make('daemon_sftp_alias')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
->label('SFTP Alias')
->helperText('Display alias for the SFTP address. Leave empty to use the Node FQDN.'),
Forms\Components\ToggleButtons::make('public') Forms\Components\ToggleButtons::make('public')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3]) ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
->label('Automatic Allocation')->inline() ->label('Automatic Allocation')->inline()

View File

@ -113,7 +113,7 @@ class AllocationsRelationManager extends RelationManager
$start = max((int) $start, 0); $start = max((int) $start, 0);
$end = min((int) $end, 2 ** 16 - 1); $end = min((int) $end, 2 ** 16 - 1);
for ($i = $start; $i <= $end; $i++) { foreach (range($start, $end) as $i) {
$ports->push($i); $ports->push($i);
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -31,6 +31,9 @@ use Illuminate\Support\Facades\Hash;
use Illuminate\Support\HtmlString; use Illuminate\Support\HtmlString;
use Illuminate\Validation\Rules\Password; use Illuminate\Validation\Rules\Password;
/**
* @method User getUser()
*/
class EditProfile extends \Filament\Pages\Auth\EditProfile class EditProfile extends \Filament\Pages\Auth\EditProfile
{ {
protected function getForms(): array protected function getForms(): array
@ -117,6 +120,7 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
->helperText('Enter your current 2FA code to disable Two Factor Authentication'), ->helperText('Enter your current 2FA code to disable Two Factor Authentication'),
]; ];
} }
/** @var TwoFactorSetupService */
$setupService = app(TwoFactorSetupService::class); $setupService = app(TwoFactorSetupService::class);
['image_url_data' => $url, 'secret' => $secret] = cache()->remember( ['image_url_data' => $url, 'secret' => $secret] = cache()->remember(
@ -126,6 +130,7 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
$options = new QROptions([ $options = new QROptions([
'svgLogo' => public_path('pelican.svg'), 'svgLogo' => public_path('pelican.svg'),
'svgLogoScale' => 0.05,
'addLogoSpace' => true, 'addLogoSpace' => true,
'logoSpaceWidth' => 13, 'logoSpaceWidth' => 13,
'logoSpaceHeight' => 13, 'logoSpaceHeight' => 13,
@ -133,22 +138,24 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
// https://github.com/chillerlan/php-qrcode/blob/main/examples/svgWithLogo.php // https://github.com/chillerlan/php-qrcode/blob/main/examples/svgWithLogo.php
// SVG logo options (see extended class)
$options->svgLogo = public_path('pelican.svg'); // logo from: https://github.com/simple-icons/simple-icons
$options->svgLogoScale = 0.05;
// $options->svgLogoCssClass = 'dark';
// QROptions // QROptions
// @phpstan-ignore property.protected
$options->version = Version::AUTO; $options->version = Version::AUTO;
// $options->outputInterface = QRSvgWithLogo::class; // $options->outputInterface = QRSvgWithLogo::class;
// @phpstan-ignore property.protected
$options->outputBase64 = false; $options->outputBase64 = false;
// @phpstan-ignore property.protected
$options->eccLevel = EccLevel::H; // ECC level H is necessary when using logos $options->eccLevel = EccLevel::H; // ECC level H is necessary when using logos
// @phpstan-ignore property.protected
$options->addQuietzone = true; $options->addQuietzone = true;
// $options->drawLightModules = true; // $options->drawLightModules = true;
// @phpstan-ignore property.protected
$options->connectPaths = true; $options->connectPaths = true;
// @phpstan-ignore property.protected
$options->drawCircularModules = true; $options->drawCircularModules = true;
// $options->circleRadius = 0.45; // $options->circleRadius = 0.45;
// @phpstan-ignore property.protected
$options->svgDefs = '<linearGradient id="gradient" x1="100%" y2="100%"> $options->svgDefs = '<linearGradient id="gradient" x1="100%" y2="100%">
<stop stop-color="#7dd4fc" offset="0"/> <stop stop-color="#7dd4fc" offset="0"/>
<stop stop-color="#38bdf8" offset="0.5"/> <stop stop-color="#38bdf8" offset="0.5"/>
@ -196,8 +203,8 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
])->headerActions([ ])->headerActions([
Action::make('Create') Action::make('Create')
->successRedirectUrl(route('filament.admin.auth.profile', ['tab' => '-api-keys-tab'])) ->successRedirectUrl(route('filament.admin.auth.profile', ['tab' => '-api-keys-tab']))
->action(function (Get $get, Action $action) { ->action(function (Get $get, Action $action, $user) {
$token = auth()->user()->createToken( $token = $user->createToken(
$get('description'), $get('description'),
$get('allowed_ips'), $get('allowed_ips'),
); );

View File

@ -3,6 +3,7 @@
namespace App\Filament\Resources\UserResource\Pages; namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource; use App\Filament\Resources\UserResource;
use App\Services\Exceptions\FilamentExceptionHandler;
use Filament\Actions; use Filament\Actions;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
use App\Models\User; use App\Models\User;
@ -66,7 +67,9 @@ class EditUser extends EditRecord
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [
Actions\DeleteAction::make(), Actions\DeleteAction::make()
->label(fn (User $user) => auth()->user()->id === $user->id ? 'Can\'t Delete Yourself' : ($user->servers()->count() > 0 ? 'User Has Servers' : 'Delete'))
->disabled(fn (User $user) => auth()->user()->id === $user->id || $user->servers()->count() > 0),
$this->getSaveFormAction()->formId('form'), $this->getSaveFormAction()->formId('form'),
]; ];
} }
@ -75,4 +78,9 @@ class EditUser extends EditRecord
{ {
return []; return [];
} }
public function exception($exception, $stopPropagation): void
{
(new FilamentExceptionHandler())->handle($exception, $stopPropagation);
}
} }

View File

@ -46,7 +46,7 @@ class EggShareController extends Controller
*/ */
public function import(EggImportFormRequest $request): RedirectResponse public function import(EggImportFormRequest $request): RedirectResponse
{ {
$egg = $this->importerService->handle($request->file('import_file')); $egg = $this->importerService->fromFile($request->file('import_file'));
$this->alert->success(trans('admin/eggs.notices.imported'))->flash(); $this->alert->success(trans('admin/eggs.notices.imported'))->flash();
return redirect()->route('admin.eggs.view', ['egg' => $egg->id]); return redirect()->route('admin.eggs.view', ['egg' => $egg->id]);
@ -61,7 +61,7 @@ class EggShareController extends Controller
*/ */
public function update(EggImportFormRequest $request, Egg $egg): RedirectResponse public function update(EggImportFormRequest $request, Egg $egg): RedirectResponse
{ {
$this->updateImporterService->handle($egg, $request->file('import_file')); $this->updateImporterService->fromFile($egg, $request->file('import_file'));
$this->alert->success(trans('admin/eggs.notices.updated_via_import'))->flash(); $this->alert->success(trans('admin/eggs.notices.updated_via_import'))->flash();
return redirect()->route('admin.eggs.view', ['egg' => $egg]); return redirect()->route('admin.eggs.view', ['egg' => $egg]);

View File

@ -3,7 +3,6 @@
namespace App\Http\Controllers\Admin\Nodes; namespace App\Http\Controllers\Admin\Nodes;
use Illuminate\View\View; use Illuminate\View\View;
use Illuminate\Http\Request;
use App\Models\Node; use App\Models\Node;
use Spatie\QueryBuilder\QueryBuilder; use Spatie\QueryBuilder\QueryBuilder;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
@ -13,7 +12,7 @@ class NodeController extends Controller
/** /**
* Returns a listing of nodes on the system. * Returns a listing of nodes on the system.
*/ */
public function index(Request $request): View public function index(): View
{ {
$nodes = QueryBuilder::for( $nodes = QueryBuilder::for(
Node::query()->withCount('servers') Node::query()->withCount('servers')

View File

@ -3,7 +3,6 @@
namespace App\Http\Controllers\Admin\Nodes; namespace App\Http\Controllers\Admin\Nodes;
use Illuminate\View\View; use Illuminate\View\View;
use Illuminate\Http\Request;
use App\Models\Node; use App\Models\Node;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use App\Models\Allocation; use App\Models\Allocation;
@ -29,16 +28,10 @@ class NodeViewController extends Controller
/** /**
* Returns index view for a specific node on the system. * Returns index view for a specific node on the system.
*/ */
public function index(Request $request, Node $node): View public function index(Node $node): View
{ {
$node->loadCount('servers'); $node->loadCount('servers');
$stats = Node::query()
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk')
->join('servers', 'servers.node_id', '=', 'nodes.id')
->where('node_id', '=', $node->id)
->first();
return view('admin.nodes.view.index', [ return view('admin.nodes.view.index', [
'node' => $node, 'node' => $node,
'version' => $this->versionService, 'version' => $this->versionService,
@ -48,7 +41,7 @@ class NodeViewController extends Controller
/** /**
* Returns the settings page for a specific node. * Returns the settings page for a specific node.
*/ */
public function settings(Request $request, Node $node): View public function settings(Node $node): View
{ {
return view('admin.nodes.view.settings', [ return view('admin.nodes.view.settings', [
'node' => $node, 'node' => $node,
@ -58,7 +51,7 @@ class NodeViewController extends Controller
/** /**
* Return the node configuration page for a specific node. * Return the node configuration page for a specific node.
*/ */
public function configuration(Request $request, Node $node): View public function configuration(Node $node): View
{ {
return view('admin.nodes.view.configuration', compact('node')); return view('admin.nodes.view.configuration', compact('node'));
} }
@ -66,7 +59,7 @@ class NodeViewController extends Controller
/** /**
* Return the node allocation management page. * Return the node allocation management page.
*/ */
public function allocations(Request $request, Node $node): View public function allocations(Node $node): View
{ {
$node->setRelation( $node->setRelation(
'allocations', 'allocations',
@ -92,7 +85,7 @@ class NodeViewController extends Controller
/** /**
* Return a listing of servers that exist for this specific node. * Return a listing of servers that exist for this specific node.
*/ */
public function servers(Request $request, Node $node): View public function servers(Node $node): View
{ {
$this->plainInject([ $this->plainInject([
'node' => Collection::wrap($node->makeVisible(['daemon_token_id', 'daemon_token'])) 'node' => Collection::wrap($node->makeVisible(['daemon_token_id', 'daemon_token']))

View File

@ -3,7 +3,6 @@
namespace App\Http\Controllers\Admin\Servers; namespace App\Http\Controllers\Admin\Servers;
use Illuminate\View\View; use Illuminate\View\View;
use Illuminate\Http\Request;
use App\Models\Server; use App\Models\Server;
use Spatie\QueryBuilder\QueryBuilder; use Spatie\QueryBuilder\QueryBuilder;
use Spatie\QueryBuilder\AllowedFilter; use Spatie\QueryBuilder\AllowedFilter;
@ -16,7 +15,7 @@ class ServerController extends Controller
* Returns all the servers that exist on the system using a paginated result set. If * Returns all the servers that exist on the system using a paginated result set. If
* a query is passed along in the request it is also passed to the repository function. * a query is passed along in the request it is also passed to the repository function.
*/ */
public function index(Request $request): View public function index(): View
{ {
$servers = QueryBuilder::for(Server::query()->with('node', 'user', 'allocation')) $servers = QueryBuilder::for(Server::query()->with('node', 'user', 'allocation'))
->allowedFilters([ ->allowedFilters([

View File

@ -3,13 +3,13 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Enums\ServerState; use App\Enums\ServerState;
use Filament\Notifications\Notification;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Models\User; use App\Models\User;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use App\Models\Mount; use App\Models\Mount;
use App\Models\Server; use App\Models\Server;
use App\Models\Database; use App\Models\Database;
use App\Models\MountServer;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag; use Prologue\Alerts\AlertsMessageBag;
use App\Exceptions\DisplayException; use App\Exceptions\DisplayException;
@ -70,7 +70,7 @@ class ServersController extends Controller
* @throws \App\Exceptions\DisplayException * @throws \App\Exceptions\DisplayException
* @throws \App\Exceptions\Model\DataValidationException * @throws \App\Exceptions\Model\DataValidationException
*/ */
public function toggleInstall(Server $server): RedirectResponse public function toggleInstall(Server $server)
{ {
if ($server->status === ServerState::InstallFailed) { if ($server->status === ServerState::InstallFailed) {
throw new DisplayException(trans('admin/server.exceptions.marked_as_failed')); throw new DisplayException(trans('admin/server.exceptions.marked_as_failed'));
@ -79,9 +79,13 @@ class ServersController extends Controller
$server->status = $server->isInstalled() ? ServerState::Installing : null; $server->status = $server->isInstalled() ? ServerState::Installing : null;
$server->save(); $server->save();
$this->alert->success(trans('admin/server.alerts.install_toggled'))->flash(); Notification::make()
->title('Success!')
->body(trans('admin/server.alerts.install_toggled'))
->success()
->send();
return redirect()->route('admin.servers.view.manage', $server->id); return null;
} }
/** /**
@ -90,12 +94,15 @@ class ServersController extends Controller
* @throws \App\Exceptions\DisplayException * @throws \App\Exceptions\DisplayException
* @throws \App\Exceptions\Model\DataValidationException * @throws \App\Exceptions\Model\DataValidationException
*/ */
public function reinstallServer(Server $server): RedirectResponse public function reinstallServer(Server $server)
{ {
$this->reinstallService->handle($server); $this->reinstallService->handle($server);
$this->alert->success(trans('admin/server.alerts.server_reinstalled'))->flash();
return redirect()->route('admin.servers.view.manage', $server->id); Notification::make()
->title('Success!')
->body(trans('admin/server.alerts.server_reinstalled'))
->success()
->send();
} }
/** /**
@ -228,12 +235,7 @@ class ServersController extends Controller
*/ */
public function addMount(Request $request, Server $server): RedirectResponse public function addMount(Request $request, Server $server): RedirectResponse
{ {
$mountServer = (new MountServer())->forceFill([ $server->mounts()->attach($request->input('mount_id'));
'mount_id' => $request->input('mount_id'),
'server_id' => $server->id,
]);
$mountServer->saveOrFail();
$this->alert->success('Mount was added successfully.')->flash(); $this->alert->success('Mount was added successfully.')->flash();
@ -245,7 +247,7 @@ class ServersController extends Controller
*/ */
public function deleteMount(Server $server, Mount $mount): RedirectResponse public function deleteMount(Server $server, Mount $mount): RedirectResponse
{ {
MountServer::where('mount_id', $mount->id)->where('server_id', $server->id)->delete(); $server->mounts()->detach($mount);
$this->alert->success('Mount was removed successfully.')->flash(); $this->alert->success('Mount was removed successfully.')->flash();

View File

@ -37,7 +37,7 @@ class UserController extends Controller
/** /**
* Display user index page. * Display user index page.
*/ */
public function index(Request $request): View public function index(): View
{ {
$users = QueryBuilder::for( $users = QueryBuilder::for(
User::query()->select('users.*') User::query()->select('users.*')

View File

@ -33,10 +33,9 @@ class StoreNodeRequest extends ApplicationApiRequest
'upload_size', 'upload_size',
'daemon_listen', 'daemon_listen',
'daemon_sftp', 'daemon_sftp',
'daemon_sftp_alias',
'daemon_base', 'daemon_base',
])->mapWithKeys(function ($value, $key) { ])->mapWithKeys(function ($value, $key) {
$key = ($key === 'daemon_sftp') ? 'daemon_sftp' : $key;
return [snake_case($key) => $value]; return [snake_case($key) => $value];
})->toArray(); })->toArray();
} }
@ -60,12 +59,8 @@ class StoreNodeRequest extends ApplicationApiRequest
public function validated($key = null, $default = null): array public function validated($key = null, $default = null): array
{ {
$response = parent::validated(); $response = parent::validated();
$response['daemon_listen'] = $response['daemon_listen'];
$response['daemon_sftp'] = $response['daemon_sftp'];
$response['daemon_base'] = $response['daemon_base'] ?? (new Node())->getAttribute('daemon_base'); $response['daemon_base'] = $response['daemon_base'] ?? (new Node())->getAttribute('daemon_base');
unset($response['daemon_base'], $response['daemon_listen'], $response['daemon_sftp']);
return $response; return $response;
} }
} }

View File

@ -1,16 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class MountServer extends Model
{
protected $table = 'mount_server';
public $timestamps = false;
protected $primaryKey = null;
public $incrementing = false;
}

View File

@ -44,7 +44,7 @@ class Node extends Model
'memory', 'memory_overallocate', 'disk', 'memory', 'memory_overallocate', 'disk',
'disk_overallocate', 'cpu', 'cpu_overallocate', 'disk_overallocate', 'cpu', 'cpu_overallocate',
'upload_size', 'daemon_base', 'upload_size', 'daemon_base',
'daemon_sftp', 'daemon_listen', 'daemon_sftp', 'daemon_sftp_alias', 'daemon_listen',
'description', 'maintenance_mode', 'description', 'maintenance_mode',
]; ];
@ -63,6 +63,7 @@ class Node extends Model
'cpu_overallocate' => 'required|numeric|min:-1', 'cpu_overallocate' => 'required|numeric|min:-1',
'daemon_base' => 'sometimes|required|regex:/^([\/][\d\w.\-\/]+)$/', 'daemon_base' => 'sometimes|required|regex:/^([\/][\d\w.\-\/]+)$/',
'daemon_sftp' => 'required|numeric|between:1,65535', 'daemon_sftp' => 'required|numeric|between:1,65535',
'daemon_sftp_alias' => 'nullable|string',
'daemon_listen' => 'required|numeric|between:1,65535', 'daemon_listen' => 'required|numeric|between:1,65535',
'maintenance_mode' => 'boolean', 'maintenance_mode' => 'boolean',
'upload_size' => 'int|between:1,1024', 'upload_size' => 'int|between:1,1024',
@ -103,6 +104,10 @@ class Node extends Model
]; ];
} }
public int $servers_sum_memory = 0;
public int $servers_sum_disk = 0;
public int $servers_sum_cpu = 0;
public function getRouteKeyName(): string public function getRouteKeyName(): string
{ {
return 'id'; return 'id';

View File

@ -5,6 +5,7 @@ namespace App\Models;
use App\Enums\ServerState; use App\Enums\ServerState;
use App\Exceptions\Http\Connection\DaemonConnectionException; use App\Exceptions\Http\Connection\DaemonConnectionException;
use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Query\JoinClause; use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
@ -13,7 +14,6 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphToMany; use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use App\Exceptions\Http\Server\ServerStateConflictException; use App\Exceptions\Http\Server\ServerStateConflictException;
class Server extends Model class Server extends Model
@ -224,12 +224,9 @@ class Server extends Model
return $this->hasMany(Backup::class); return $this->hasMany(Backup::class);
} }
/** public function mounts(): BelongsToMany
* Returns all mounts that have this server has mounted.
*/
public function mounts(): HasManyThrough
{ {
return $this->hasManyThrough(Mount::class, MountServer::class, 'server_id', 'id', 'id', 'mount_id'); return $this->belongsToMany(Mount::class);
} }
/** /**

View File

@ -6,6 +6,7 @@ use App\Extensions\Themes\Theme;
use App\Models; use App\Models;
use App\Models\ApiKey; use App\Models\ApiKey;
use App\Models\Node; use App\Models\Node;
use App\Services\Helpers\SoftwareVersionService;
use Dedoc\Scramble\Scramble; use Dedoc\Scramble\Scramble;
use Dedoc\Scramble\Support\Generator\OpenApi; use Dedoc\Scramble\Support\Generator\OpenApi;
use Dedoc\Scramble\Support\Generator\SecurityScheme; use Dedoc\Scramble\Support\Generator\SecurityScheme;
@ -30,8 +31,9 @@ class AppServiceProvider extends ServiceProvider
{ {
Schema::defaultStringLength(191); Schema::defaultStringLength(191);
View::share('appVersion', $this->versionData()['version'] ?? 'undefined'); $versionData = app(SoftwareVersionService::class)->versionData();
View::share('appIsGit', $this->versionData()['is_git'] ?? false); View::share('appVersion', $versionData['version'] ?? 'undefined');
View::share('appIsGit', $versionData['is_git'] ?? false);
Paginator::useBootstrap(); Paginator::useBootstrap();
@ -96,34 +98,6 @@ class AppServiceProvider extends ServiceProvider
Scramble::ignoreDefaultRoutes(); Scramble::ignoreDefaultRoutes();
} }
/**
* Return version information for the footer.
*/
protected function versionData(): array
{
return cache()->remember('git-version', 5, function () {
if (file_exists(base_path('.git/HEAD'))) {
$head = explode(' ', file_get_contents(base_path('.git/HEAD')));
if (array_key_exists(1, $head)) {
$path = base_path('.git/' . trim($head[1]));
}
}
if (isset($path) && file_exists($path)) {
return [
'version' => substr(file_get_contents($path), 0, 8),
'is_git' => true,
];
}
return [
'version' => config('app.version'),
'is_git' => false,
];
});
}
public function bootAuth(): void public function bootAuth(): void
{ {
Sanctum::usePersonalAccessTokenModel(ApiKey::class); Sanctum::usePersonalAccessTokenModel(ApiKey::class);

View File

@ -124,38 +124,6 @@ class EggConfigurationService
return $response; return $response;
} }
/**
* Replaces the legacy modifies from eggs with their new counterpart. The legacy Daemon would
* set SERVER_MEMORY, SERVER_IP, and SERVER_PORT with their respective values on the Daemon
* side. Ensure that anything referencing those properly replaces them with the matching config
* value.
*/
protected function replaceLegacyModifiers(string $key, string $value): string
{
switch ($key) {
case 'config.docker.interface':
$replace = 'config.docker.network.interface';
break;
case 'server.build.env.SERVER_MEMORY':
case 'env.SERVER_MEMORY':
$replace = 'server.build.memory';
break;
case 'server.build.env.SERVER_IP':
case 'env.SERVER_IP':
$replace = 'server.build.default.ip';
break;
case 'server.build.env.SERVER_PORT':
case 'env.SERVER_PORT':
$replace = 'server.build.default.port';
break;
default:
// By default, we don't need to change anything, only if we ended up matching a specific legacy item.
$replace = $key;
}
return str_replace("{{{$key}}}", "{{{$replace}}}", $value);
}
protected function matchAndReplaceKeys(mixed $value, array $structure): mixed protected function matchAndReplaceKeys(mixed $value, array $structure): mixed
{ {
preg_match_all('/{{(?<key>[\w.-]*)}}/', $value, $matches); preg_match_all('/{{(?<key>[\w.-]*)}}/', $value, $matches);
@ -175,8 +143,6 @@ class EggConfigurationService
continue; continue;
} }
$value = $this->replaceLegacyModifiers($key, $value);
// We don't want to do anything with config keys since the Daemon will need to handle // We don't want to do anything with config keys since the Daemon will need to handle
// that. For example, the Spigot egg uses "config.docker.interface" to identify the Docker // that. For example, the Spigot egg uses "config.docker.interface" to identify the Docker
// interface to proxy through, but the Panel would be unaware of that. // interface to proxy through, but the Panel would be unaware of that.
@ -198,7 +164,7 @@ class EggConfigurationService
// variable from the server configuration. // variable from the server configuration.
$plucked = Arr::get( $plucked = Arr::get(
$structure, $structure,
preg_replace('/^env\./', 'build.env.', $key), preg_replace('/^env\./', 'build.environment.', $key),
'' ''
); );

View File

@ -10,6 +10,17 @@ use App\Exceptions\Service\InvalidFileUploadException;
class EggParserService class EggParserService
{ {
public const UPGRADE_VARIABLES = [
'server.build.env.SERVER_IP' => 'server.allocations.default.ip',
'server.build.default.ip' => 'server.allocations.default.ip',
'server.build.env.SERVER_PORT' => 'server.allocations.default.port',
'server.build.default.port' => 'server.allocations.default.port',
'server.build.env.SERVER_MEMORY' => 'server.build.memory_limit',
'server.build.memory' => 'server.build.memory_limit',
'server.build.env.' => 'server.environment.',
'server.build.environment.' => 'server.environment.',
];
/** /**
* Takes an uploaded file and parses out the egg configuration from within. * Takes an uploaded file and parses out the egg configuration from within.
* *
@ -26,11 +37,20 @@ class EggParserService
$version = $parsed['meta']['version'] ?? ''; $version = $parsed['meta']['version'] ?? '';
return match ($version) { $parsed = match ($version) {
'PTDL_v1' => $this->convertToV2($parsed), 'PTDL_v1' => $this->convertToV2($parsed),
'PTDL_v2' => $parsed, 'PTDL_v2' => $parsed,
default => throw new InvalidFileUploadException('The JSON file provided is not in a format that can be recognized.') default => throw new InvalidFileUploadException('The JSON file provided is not in a format that can be recognized.')
}; };
// Make sure we only use recent variable format from now on
$parsed['config']['files'] = str_replace(
array_keys(self::UPGRADE_VARIABLES),
array_values(self::UPGRADE_VARIABLES),
$parsed['config']['files'] ?? '',
);
return $parsed;
} }
/** /**

View File

@ -9,6 +9,7 @@ use Illuminate\Http\UploadedFile;
use App\Models\EggVariable; use App\Models\EggVariable;
use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionInterface;
use App\Services\Eggs\EggParserService; use App\Services\Eggs\EggParserService;
use Spatie\TemporaryDirectory\TemporaryDirectory;
class EggImporterService class EggImporterService
{ {
@ -21,7 +22,7 @@ class EggImporterService
* *
* @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable * @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable
*/ */
public function handle(UploadedFile $file): Egg public function fromFile(UploadedFile $file): Egg
{ {
$parsed = $this->parser->handle($file); $parsed = $this->parser->handle($file);
@ -45,4 +46,20 @@ class EggImporterService
return $egg; return $egg;
}); });
} }
/**
* Take an url and parse it into a new egg.
*
* @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable
*/
public function fromUrl(string $url): Egg
{
$info = pathinfo($url);
$tmpDir = TemporaryDirectory::make()->deleteWhenDestroyed();
$tmpPath = $tmpDir->path($info['basename']);
file_put_contents($tmpPath, file_get_contents($url));
return $this->fromFile(new UploadedFile($tmpPath, $info['basename'], 'application/json'));
}
} }

View File

@ -8,6 +8,7 @@ use Illuminate\Support\Collection;
use App\Models\EggVariable; use App\Models\EggVariable;
use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionInterface;
use App\Services\Eggs\EggParserService; use App\Services\Eggs\EggParserService;
use Spatie\TemporaryDirectory\TemporaryDirectory;
class EggUpdateImporterService class EggUpdateImporterService
{ {
@ -23,7 +24,7 @@ class EggUpdateImporterService
* *
* @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable * @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable
*/ */
public function handle(Egg $egg, UploadedFile $file): Egg public function fromFile(Egg $egg, UploadedFile $file): Egg
{ {
$parsed = $this->parser->handle($file); $parsed = $this->parser->handle($file);
@ -47,4 +48,20 @@ class EggUpdateImporterService
return $egg->refresh(); return $egg->refresh();
}); });
} }
/**
* Update an existing Egg using an url.
*
* @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable
*/
public function fromUrl(Egg $egg, string $url): Egg
{
$info = pathinfo($url);
$tmpDir = TemporaryDirectory::make()->deleteWhenDestroyed();
$tmpPath = $tmpDir->path($info['basename']);
file_put_contents($tmpPath, file_get_contents($url));
return $this->fromFile($egg, new UploadedFile($tmpPath, $info['basename'], 'application/json'));
}
} }

View File

@ -0,0 +1,24 @@
<?php
namespace App\Services\Exceptions;
use Exception;
use Filament\Notifications\Notification;
class FilamentExceptionHandler
{
public function handle(Exception $exception, callable $stopPropagation): void
{
Notification::make()
->title($exception->title ?? null)
->body($exception->body ?? $exception->getMessage())
->color($exception->color ?? 'danger')
->icon($exception->icon ?? 'tabler-x')
->danger()
->send();
if ($this->stopPropagation ?? true) {
$stopPropagation();
}
}
}

View File

@ -49,6 +49,14 @@ class SoftwareVersionService
return Arr::get(self::$result, 'discord') ?? 'https://pelican.dev/discord'; return Arr::get(self::$result, 'discord') ?? 'https://pelican.dev/discord';
} }
/**
* Get the donation URL.
*/
public function getDonations(): string
{
return Arr::get(self::$result, 'donate') ?? 'https://pelican.dev/donate';
}
/** /**
* Determine if the current version of the panel is the latest. * Determine if the current version of the panel is the latest.
*/ */
@ -93,8 +101,28 @@ class SoftwareVersionService
}); });
} }
public function getDonations(): string public function versionData(): array
{ {
return 'https://github.com'; return cache()->remember('git-version', 5, function () {
if (file_exists(base_path('.git/HEAD'))) {
$head = explode(' ', file_get_contents(base_path('.git/HEAD')));
if (array_key_exists(1, $head)) {
$path = base_path('.git/' . trim($head[1]));
}
}
if (isset($path) && file_exists($path)) {
return [
'version' => 'canary (' . substr(file_get_contents($path), 0, 8) . ')',
'is_git' => true,
];
}
return [
'version' => config('app.version'),
'is_git' => false,
];
});
} }
} }

View File

@ -27,6 +27,8 @@ class NodeUpdateService
*/ */
public function handle(Node $node, array $data, bool $resetToken = false): Node public function handle(Node $node, array $data, bool $resetToken = false): Node
{ {
$data['id'] = $node->id;
if ($resetToken) { if ($resetToken) {
$data['daemon_token'] = Str::random(Node::DAEMON_TOKEN_LENGTH); $data['daemon_token'] = Str::random(Node::DAEMON_TOKEN_LENGTH);
$data['daemon_token_id'] = Str::random(Node::DAEMON_TOKEN_ID_LENGTH); $data['daemon_token_id'] = Str::random(Node::DAEMON_TOKEN_ID_LENGTH);
@ -35,15 +37,9 @@ class NodeUpdateService
[$updated, $exception] = $this->connection->transaction(function () use ($data, $node) { [$updated, $exception] = $this->connection->transaction(function () use ($data, $node) {
/** @var \App\Models\Node $updated */ /** @var \App\Models\Node $updated */
$updated = $node->replicate(); $updated = $node->replicate();
$updated->exists = true;
$updated->forceFill($data)->save(); $updated->forceFill($data)->save();
try { try {
// If we're changing the FQDN for the node, use the newly provided FQDN for the connection
// address. This should alleviate issues where the node gets pointed to a "valid" FQDN that
// isn't actually running the daemon software, and therefore you can't actually change it
// back.
//
// This makes more sense anyways, because only the Panel uses the FQDN for connecting, the
// node doesn't actually care about this.
$node->fqdn = $updated->fqdn; $node->fqdn = $updated->fqdn;
$this->configurationRepository->setNode($node)->update($updated); $this->configurationRepository->setNode($node)->update($updated);

View File

@ -40,7 +40,7 @@ class ServerConfigurationStructureService
*/ */
protected function returnFormat(Server $server): array protected function returnFormat(Server $server): array
{ {
return [ $response = [
'uuid' => $server->uuid, 'uuid' => $server->uuid,
'meta' => [ 'meta' => [
'name' => $server->name, 'name' => $server->name,
@ -50,7 +50,6 @@ class ServerConfigurationStructureService
'environment' => $this->environment->handle($server), 'environment' => $this->environment->handle($server),
'invocation' => $server->startup, 'invocation' => $server->startup,
'skip_egg_scripts' => $server->skip_scripts, 'skip_egg_scripts' => $server->skip_scripts,
'labels' => $server->docker_labels,
'build' => [ 'build' => [
'memory_limit' => $server->memory, 'memory_limit' => $server->memory,
'swap' => $server->swap, 'swap' => $server->swap,
@ -72,17 +71,27 @@ class ServerConfigurationStructureService
], ],
'mappings' => $server->getAllocationMappings(), 'mappings' => $server->getAllocationMappings(),
], ],
'mounts' => $server->mounts->map(function (Mount $mount) {
return [
'source' => $mount->source,
'target' => $mount->target,
'read_only' => $mount->read_only,
];
}),
'egg' => [ 'egg' => [
'id' => $server->egg->uuid, 'id' => $server->egg->uuid,
'file_denylist' => $server->egg->inherit_file_denylist, 'file_denylist' => $server->egg->inherit_file_denylist,
], ],
]; ];
if (!empty($server->docker_labels)) {
$response['labels'] = $server->docker_labels;
}
if ($server->mounts->isNotEmpty()) {
$response['mounts'] = $server->mounts->map(function (Mount $mount) {
return [
'source' => $mount->source,
'target' => $mount->target,
'read_only' => $mount->read_only,
];
})->toArray();
}
return $response;
} }
} }

View File

@ -3,6 +3,7 @@
namespace App\Services\Servers; namespace App\Services\Servers;
use App\Enums\ServerState; use App\Enums\ServerState;
use Filament\Notifications\Notification;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
use App\Models\Server; use App\Models\Server;
use App\Repositories\Daemon\DaemonServerRepository; use App\Repositories\Daemon\DaemonServerRepository;
@ -26,7 +27,7 @@ class SuspensionService
* *
* @throws \Throwable * @throws \Throwable
*/ */
public function toggle(Server $server, string $action = self::ACTION_SUSPEND): void public function toggle(Server $server, string $action = self::ACTION_SUSPEND)
{ {
Assert::oneOf($action, [self::ACTION_SUSPEND, self::ACTION_UNSUSPEND]); Assert::oneOf($action, [self::ACTION_SUSPEND, self::ACTION_UNSUSPEND]);
@ -35,11 +36,12 @@ class SuspensionService
// suspended in the database. Additionally, nothing needs to happen if the server // suspended in the database. Additionally, nothing needs to happen if the server
// is not suspended, and we try to un-suspend the instance. // is not suspended, and we try to un-suspend the instance.
if ($isSuspending === $server->isSuspended()) { if ($isSuspending === $server->isSuspended()) {
return; return Notification::make()->danger()->title('Failed!')->body('Server is already suspended!')->send();
} }
// Check if the server is currently being transferred. // Check if the server is currently being transferred.
if (!is_null($server->transfer)) { if (!is_null($server->transfer)) {
Notification::make()->danger()->title('Failed!')->body('Server is currently being transferred.')->send();
throw new ConflictHttpException('Cannot toggle suspension status on a server that is currently being transferred.'); throw new ConflictHttpException('Cannot toggle suspension status on a server that is currently being transferred.');
} }

View File

@ -53,7 +53,7 @@ class TransferServerService
{ {
$node_id = $data['node_id']; $node_id = $data['node_id'];
$allocation_id = intval($data['allocation_id']); $allocation_id = intval($data['allocation_id']);
$additional_allocations = array_map('intval', $data['allocation_additional'] ?? []); $additional_allocations = array_map(intval(...), $data['allocation_additional'] ?? []);
// Check if the node is viable for the transfer. // Check if the node is viable for the transfer.
$node = Node::query() $node = Node::query()

View File

@ -46,6 +46,7 @@ class ServerTransformer extends BaseClientTransformer
'is_node_under_maintenance' => $server->node->isUnderMaintenance(), 'is_node_under_maintenance' => $server->node->isUnderMaintenance(),
'sftp_details' => [ 'sftp_details' => [
'ip' => $server->node->fqdn, 'ip' => $server->node->fqdn,
'alias' => $server->node->daemon_sftp_alias,
'port' => $server->node->daemon_sftp, 'port' => $server->node->daemon_sftp,
], ],
'description' => $server->description, 'description' => $server->description,

View File

@ -9,10 +9,7 @@ return Application::configure(basePath: dirname(__DIR__))
\Prologue\Alerts\AlertsServiceProvider::class, \Prologue\Alerts\AlertsServiceProvider::class,
]) ])
->withRouting( ->withRouting(
web: __DIR__.'/../routes/web.php',
// api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php', commands: __DIR__.'/../routes/console.php',
// channels: __DIR__.'/../routes/channels.php',
health: '/up', health: '/up',
) )
->withMiddleware(function (Middleware $middleware) { ->withMiddleware(function (Middleware $middleware) {

View File

@ -33,6 +33,7 @@
"s1lentium/iptools": "~1.2.0", "s1lentium/iptools": "~1.2.0",
"spatie/laravel-fractal": "^6.2", "spatie/laravel-fractal": "^6.2",
"spatie/laravel-query-builder": "^5.8.1", "spatie/laravel-query-builder": "^5.8.1",
"spatie/temporary-directory": "^2.2",
"symfony/http-client": "^7.1", "symfony/http-client": "^7.1",
"symfony/mailgun-mailer": "^7.1", "symfony/mailgun-mailer": "^7.1",
"symfony/postmark-mailer": "^7.0.7", "symfony/postmark-mailer": "^7.0.7",

739
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -75,10 +75,10 @@ class EggSeeder extends Seeder
->first(); ->first();
if ($egg instanceof Egg) { if ($egg instanceof Egg) {
$this->updateImporterService->handle($egg, $file); $this->updateImporterService->fromFile($egg, $file);
$this->command->info('Updated ' . $decoded['name']); $this->command->info('Updated ' . $decoded['name']);
} else { } else {
$this->importerService->handle($file); $this->importerService->fromFile($file);
$this->command->comment('Created ' . $decoded['name']); $this->command->comment('Created ' . $decoded['name']);
} }
} }

View File

@ -4,9 +4,10 @@
"version": "PTDL_v2", "version": "PTDL_v2",
"update_url": null "update_url": null
}, },
"exported_at": "2024-05-08T11:47:32+02:00", "exported_at": "2024-06-04T22:51:49+00:00",
"name": "Bungeecord", "name": "Bungeecord",
"author": "panel@example.com", "author": "panel@example.com",
"uuid": "9e6b409e-4028-4947-aea8-50a2c404c271",
"description": "For a long time, Minecraft server owners have had a dream that encompasses a free, easy, and reliable way to connect multiple Minecraft servers together. BungeeCord is the answer to said dream. Whether you are a small server wishing to string multiple game-modes together, or the owner of the ShotBow Network, BungeeCord is the ideal solution for you. With the help of BungeeCord, you will be able to unlock your community's full potential.", "description": "For a long time, Minecraft server owners have had a dream that encompasses a free, easy, and reliable way to connect multiple Minecraft servers together. BungeeCord is the answer to said dream. Whether you are a small server wishing to string multiple game-modes together, or the owner of the ShotBow Network, BungeeCord is the ideal solution for you. With the help of BungeeCord, you will be able to unlock your community's full potential.",
"features": [ "features": [
"eula", "eula",
@ -23,7 +24,7 @@
"file_denylist": [], "file_denylist": [],
"startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}", "startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}",
"config": { "config": {
"files": "{\r\n \"config.yml\": {\r\n \"parser\": \"yaml\",\r\n \"find\": {\r\n \"listeners[0].query_port\": \"{{server.build.default.port}}\",\r\n \"listeners[0].host\": \"0.0.0.0:{{server.build.default.port}}\",\r\n \"servers.*.address\": {\r\n \"regex:^(127\\\\.0\\\\.0\\\\.1|localhost)(:\\\\d{1,5})?$\": \"{{config.docker.interface}}$2\"\r\n }\r\n }\r\n }\r\n}", "files": "{\r\n \"config.yml\": {\r\n \"parser\": \"yaml\",\r\n \"find\": {\r\n \"listeners[0].query_port\": \"{{server.allocations.default.port}}\",\r\n \"listeners[0].host\": \"0.0.0.0:{{server.allocations.default.port}}\",\r\n \"servers.*.address\": {\r\n \"regex:^(127\\\\.0\\\\.0\\\\.1|localhost)(:\\\\d{1,5})?$\": \"{{config.docker.interface}}$2\"\r\n }\r\n }\r\n }\r\n}",
"startup": "{\r\n \"done\": \"Listening on \"\r\n}", "startup": "{\r\n \"done\": \"Listening on \"\r\n}",
"logs": "{}", "logs": "{}",
"stop": "end" "stop": "end"
@ -44,6 +45,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|alpha_num|between:1,6", "rules": "required|alpha_num|between:1,6",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -54,6 +56,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/", "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/",
"sort": null,
"field_type": "text" "field_type": "text"
} }
] ]

View File

@ -4,9 +4,10 @@
"version": "PTDL_v2", "version": "PTDL_v2",
"update_url": null "update_url": null
}, },
"exported_at": "2024-05-08T11:47:58+02:00", "exported_at": "2024-06-04T22:51:58+00:00",
"name": "Forge Minecraft", "name": "Forge Minecraft",
"author": "panel@example.com", "author": "panel@example.com",
"uuid": "ed072427-f209-4603-875c-f540c6dd5a65",
"description": "Minecraft Forge Server. Minecraft Forge is a modding API (Application Programming Interface), which makes it easier to create mods, and also make sure mods are compatible with each other.", "description": "Minecraft Forge Server. Minecraft Forge is a modding API (Application Programming Interface), which makes it easier to create mods, and also make sure mods are compatible with each other.",
"features": [ "features": [
"eula", "eula",
@ -23,7 +24,7 @@
"file_denylist": [], "file_denylist": [],
"startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -Dterminal.jline=false -Dterminal.ansi=true $( [[ ! -f unix_args.txt ]] && printf %s \"-jar {{SERVER_JARFILE}}\" || printf %s \"@unix_args.txt\" )", "startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -Dterminal.jline=false -Dterminal.ansi=true $( [[ ! -f unix_args.txt ]] && printf %s \"-jar {{SERVER_JARFILE}}\" || printf %s \"@unix_args.txt\" )",
"config": { "config": {
"files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.build.default.port}}\",\r\n \"query.port\": \"{{server.build.default.port}}\"\r\n }\r\n }\r\n}", "files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.allocations.default.port}}\",\r\n \"query.port\": \"{{server.allocations.default.port}}\"\r\n }\r\n }\r\n}",
"startup": "{\r\n \"done\": \")! For help, type \"\r\n}", "startup": "{\r\n \"done\": \")! For help, type \"\r\n}",
"logs": "{}", "logs": "{}",
"stop": "stop" "stop": "stop"
@ -44,6 +45,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/", "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -54,6 +56,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|string|max:9", "rules": "required|string|max:9",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -64,6 +67,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|string|in:recommended,latest", "rules": "required|string|in:recommended,latest",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -74,6 +78,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "nullable|regex:\/^[0-9\\.\\-]+$\/", "rules": "nullable|regex:\/^[0-9\\.\\-]+$\/",
"sort": null,
"field_type": "text" "field_type": "text"
} }
] ]

View File

@ -4,9 +4,10 @@
"version": "PTDL_v2", "version": "PTDL_v2",
"update_url": null "update_url": null
}, },
"exported_at": "2024-05-08T11:48:34+02:00", "exported_at": "2024-06-04T22:51:57+00:00",
"name": "Paper", "name": "Paper",
"author": "parker@example.com", "author": "parker@example.com",
"uuid": "5da37ef6-58da-4169-90a6-e683e1721247",
"description": "High performance Spigot fork that aims to fix gameplay and mechanics inconsistencies.", "description": "High performance Spigot fork that aims to fix gameplay and mechanics inconsistencies.",
"features": [ "features": [
"eula", "eula",
@ -23,7 +24,7 @@
"file_denylist": [], "file_denylist": [],
"startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -Dterminal.jline=false -Dterminal.ansi=true -jar {{SERVER_JARFILE}}", "startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -Dterminal.jline=false -Dterminal.ansi=true -jar {{SERVER_JARFILE}}",
"config": { "config": {
"files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.build.default.port}}\",\r\n \"query.port\": \"{{server.build.default.port}}\"\r\n }\r\n }\r\n}", "files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.allocations.default.port}}\",\r\n \"query.port\": \"{{server.allocations.default.port}}\"\r\n }\r\n }\r\n}",
"startup": "{\r\n \"done\": \")! For help, type \"\r\n}", "startup": "{\r\n \"done\": \")! For help, type \"\r\n}",
"logs": "{}", "logs": "{}",
"stop": "stop" "stop": "stop"
@ -44,6 +45,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "nullable|string|max:20", "rules": "nullable|string|max:20",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -54,6 +56,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/", "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -64,6 +67,7 @@
"user_viewable": false, "user_viewable": false,
"user_editable": false, "user_editable": false,
"rules": "nullable|string", "rules": "nullable|string",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -74,6 +78,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|string|max:20", "rules": "required|string|max:20",
"sort": null,
"field_type": "text" "field_type": "text"
} }
] ]

View File

@ -4,9 +4,10 @@
"version": "PTDL_v2", "version": "PTDL_v2",
"update_url": null "update_url": null
}, },
"exported_at": "2024-05-08T11:49:03+02:00", "exported_at": "2024-06-04T22:50:55+00:00",
"name": "Sponge (SpongeVanilla)", "name": "Sponge (SpongeVanilla)",
"author": "panel@example.com", "author": "panel@example.com",
"uuid": "f0d2f88f-1ff3-42a0-b03f-ac44c5571e6d",
"description": "SpongeVanilla is the SpongeAPI implementation for Vanilla Minecraft.", "description": "SpongeVanilla is the SpongeAPI implementation for Vanilla Minecraft.",
"features": [ "features": [
"eula", "eula",
@ -23,7 +24,7 @@
"file_denylist": [], "file_denylist": [],
"startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}", "startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}",
"config": { "config": {
"files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.build.default.port}}\",\r\n \"query.port\": \"{{server.build.default.port}}\"\r\n }\r\n }\r\n}", "files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.allocations.default.port}}\",\r\n \"query.port\": \"{{server.allocations.default.port}}\"\r\n }\r\n }\r\n}",
"startup": "{\r\n \"done\": \")! For help, type \"\r\n}", "startup": "{\r\n \"done\": \")! For help, type \"\r\n}",
"logs": "{}", "logs": "{}",
"stop": "stop" "stop": "stop"
@ -44,6 +45,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|regex:\/^([a-zA-Z0-9.\\-_]+)$\/", "rules": "required|regex:\/^([a-zA-Z0-9.\\-_]+)$\/",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -54,7 +56,8 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/", "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/",
"sort": null,
"field_type": "text" "field_type": "text"
} }
] ]
} }

View File

@ -4,9 +4,10 @@
"version": "PTDL_v2", "version": "PTDL_v2",
"update_url": null "update_url": null
}, },
"exported_at": "2024-05-08T11:49:32+02:00", "exported_at": "2024-06-04T22:51:16+00:00",
"name": "Vanilla Minecraft", "name": "Vanilla Minecraft",
"author": "panel@example.com", "author": "panel@example.com",
"uuid": "9ac39f3d-0c34-4d93-8174-c52ab9e6c57b",
"description": "Minecraft is a game about placing blocks and going on adventures. Explore randomly generated worlds and build amazing things from the simplest of homes to the grandest of castles. Play in Creative Mode with unlimited resources or mine deep in Survival Mode, crafting weapons and armor to fend off dangerous mobs. Do all this alone or with friends.", "description": "Minecraft is a game about placing blocks and going on adventures. Explore randomly generated worlds and build amazing things from the simplest of homes to the grandest of castles. Play in Creative Mode with unlimited resources or mine deep in Survival Mode, crafting weapons and armor to fend off dangerous mobs. Do all this alone or with friends.",
"features": [ "features": [
"eula", "eula",
@ -23,7 +24,7 @@
"file_denylist": [], "file_denylist": [],
"startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}", "startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}",
"config": { "config": {
"files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.build.default.port}}\",\r\n \"query.port\": \"{{server.build.default.port}}\"\r\n }\r\n }\r\n}", "files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.allocations.default.port}}\",\r\n \"query.port\": \"{{server.allocations.default.port}}\"\r\n }\r\n }\r\n}",
"startup": "{\r\n \"done\": \")! For help, type \"\r\n}", "startup": "{\r\n \"done\": \")! For help, type \"\r\n}",
"logs": "{}", "logs": "{}",
"stop": "stop" "stop": "stop"
@ -44,6 +45,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/", "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -54,6 +56,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|string|between:3,15", "rules": "required|string|between:3,15",
"sort": null,
"field_type": "text" "field_type": "text"
} }
] ]

View File

@ -4,9 +4,10 @@
"version": "PTDL_v2", "version": "PTDL_v2",
"update_url": null "update_url": null
}, },
"exported_at": "2024-05-08T11:57:56+02:00", "exported_at": "2024-06-02T20:42:09+00:00",
"name": "Rust", "name": "Rust",
"author": "panel@example.com", "author": "panel@example.com",
"uuid": "bace2dfb-209c-452a-9459-7d6f340b07ae",
"description": "The only aim in Rust is to survive. To do this you will need to overcome struggles such as hunger, thirst and cold. Build a fire. Build a shelter. Kill animals for meat. Protect yourself from other players, and kill them for meat. Create alliances with other players and form a town. Do whatever it takes to survive.", "description": "The only aim in Rust is to survive. To do this you will need to overcome struggles such as hunger, thirst and cold. Build a fire. Build a shelter. Kill animals for meat. Protect yourself from other players, and kill them for meat. Create alliances with other players and form a town. Do whatever it takes to survive.",
"features": [ "features": [
"steam_disk_space" "steam_disk_space"
@ -38,6 +39,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|string|max:60", "rules": "required|string|max:60",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -48,6 +50,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|in:vanilla,oxide,carbon", "rules": "required|in:vanilla,oxide,carbon",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -58,6 +61,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|string|max:20", "rules": "required|string|max:20",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -68,6 +72,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|string", "rules": "required|string",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -78,6 +83,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "nullable|url", "rules": "nullable|url",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -88,6 +94,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|integer", "rules": "required|integer",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -98,6 +105,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "nullable|string", "rules": "nullable|string",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -108,6 +116,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|integer", "rules": "required|integer",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -118,6 +127,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "nullable|url", "rules": "nullable|url",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -128,6 +138,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": false, "user_editable": false,
"rules": "required|integer", "rules": "required|integer",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -138,6 +149,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": false, "user_editable": false,
"rules": "required|integer", "rules": "required|integer",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -148,6 +160,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|regex:\/^[\\w.-]*$\/|max:64", "rules": "required|regex:\/^[\\w.-]*$\/|max:64",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -158,6 +171,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|integer", "rules": "required|integer",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -168,6 +182,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "nullable|string", "rules": "nullable|string",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -178,6 +193,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": false, "user_editable": false,
"rules": "required|integer", "rules": "required|integer",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -188,6 +204,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "nullable|url", "rules": "nullable|url",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -198,6 +215,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "nullable|url", "rules": "nullable|url",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -208,6 +226,7 @@
"user_viewable": false, "user_viewable": false,
"user_editable": false, "user_editable": false,
"rules": "required|string|in:258550", "rules": "required|string|in:258550",
"sort": null,
"field_type": "text" "field_type": "text"
} }
] ]

View File

@ -4,9 +4,10 @@
"version": "PTDL_v2", "version": "PTDL_v2",
"update_url": null "update_url": null
}, },
"exported_at": "2024-05-08T12:03:19+02:00", "exported_at": "2024-06-02T20:42:04+00:00",
"name": "Counter-Strike: Global Offensive", "name": "Counter-Strike: Global Offensive",
"author": "panel@example.com", "author": "panel@example.com",
"uuid": "437c367d-06be-498f-a604-fdad135504d7",
"description": "Counter-Strike: Global Offensive is a multiplayer first-person shooter video game developed by Hidden Path Entertainment and Valve Corporation.", "description": "Counter-Strike: Global Offensive is a multiplayer first-person shooter video game developed by Hidden Path Entertainment and Valve Corporation.",
"features": [ "features": [
"gsl_token", "gsl_token",
@ -39,6 +40,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|string|alpha_dash", "rules": "required|string|alpha_dash",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -49,6 +51,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|string|alpha_num|size:32", "rules": "required|string|alpha_num|size:32",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -59,6 +62,7 @@
"user_viewable": false, "user_viewable": false,
"user_editable": false, "user_editable": false,
"rules": "required|string|max:20", "rules": "required|string|max:20",
"sort": null,
"field_type": "text" "field_type": "text"
} }
] ]

View File

@ -4,9 +4,10 @@
"version": "PTDL_v2", "version": "PTDL_v2",
"update_url": null "update_url": null
}, },
"exported_at": "2024-05-08T12:02:53+02:00", "exported_at": "2024-06-02T20:42:04+00:00",
"name": "Custom Source Engine Game", "name": "Custom Source Engine Game",
"author": "panel@example.com", "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.", "description": "This option allows modifying the startup arguments and other details to run a custom SRCDS based game on the panel.",
"features": [ "features": [
"steam_disk_space" "steam_disk_space"
@ -38,6 +39,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": false, "user_editable": false,
"rules": "required|numeric|digits_between:1,6", "rules": "required|numeric|digits_between:1,6",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -48,6 +50,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": false, "user_editable": false,
"rules": "required|alpha_dash|between:1,100", "rules": "required|alpha_dash|between:1,100",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -58,6 +61,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|string|alpha_dash", "rules": "required|string|alpha_dash",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -68,6 +72,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "nullable|string", "rules": "nullable|string",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -78,6 +83,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "nullable|string", "rules": "nullable|string",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -88,6 +94,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "nullable|string", "rules": "nullable|string",
"sort": null,
"field_type": "text" "field_type": "text"
} }
] ]

View File

@ -4,9 +4,10 @@
"version": "PTDL_v2", "version": "PTDL_v2",
"update_url": null "update_url": null
}, },
"exported_at": "2024-05-08T12:02:26+02:00", "exported_at": "2024-06-02T20:42:05+00:00",
"name": "Garrys Mod", "name": "Garrys Mod",
"author": "panel@example.com", "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.", "description": "Garrys Mod, is a sandbox physics game created by Garry Newman, and developed by his company, Facepunch Studios.",
"features": [ "features": [
"gsl_token", "gsl_token",
@ -39,6 +40,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|string|alpha_dash", "rules": "required|string|alpha_dash",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -49,6 +51,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "nullable|string|alpha_num|size:32", "rules": "nullable|string|alpha_num|size:32",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -59,6 +62,7 @@
"user_viewable": false, "user_viewable": false,
"user_editable": false, "user_editable": false,
"rules": "required|string|max:20", "rules": "required|string|max:20",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -69,6 +73,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "nullable|integer", "rules": "nullable|integer",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -79,6 +84,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|string", "rules": "required|string",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -89,6 +95,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|integer|max:128", "rules": "required|integer|max:128",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -99,6 +106,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|integer|max:100", "rules": "required|integer|max:100",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -109,6 +117,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|boolean", "rules": "required|boolean",
"sort": null,
"field_type": "text" "field_type": "text"
} }
] ]

View File

@ -4,9 +4,10 @@
"version": "PTDL_v2", "version": "PTDL_v2",
"update_url": null "update_url": null
}, },
"exported_at": "2024-05-08T12:01:57+02:00", "exported_at": "2024-06-02T20:42:06+00:00",
"name": "Insurgency", "name": "Insurgency",
"author": "panel@example.com", "author": "panel@example.com",
"uuid": "a5702286-655b-4069-bf1e-925c7300b61a",
"description": "Take to the streets for intense close quarters combat, where a team's survival depends upon securing crucial strongholds and destroying enemy supply in this multiplayer and cooperative Source Engine based experience.", "description": "Take to the streets for intense close quarters combat, where a team's survival depends upon securing crucial strongholds and destroying enemy supply in this multiplayer and cooperative Source Engine based experience.",
"features": [ "features": [
"steam_disk_space" "steam_disk_space"
@ -38,6 +39,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": false, "user_editable": false,
"rules": "required|regex:\/^(237410)$\/", "rules": "required|regex:\/^(237410)$\/",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -48,6 +50,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|regex:\/^(\\w{1,20})$\/", "rules": "required|regex:\/^(\\w{1,20})$\/",
"sort": null,
"field_type": "text" "field_type": "text"
} }
] ]

View File

@ -4,9 +4,10 @@
"version": "PTDL_v2", "version": "PTDL_v2",
"update_url": null "update_url": null
}, },
"exported_at": "2024-05-08T11:59:56+02:00", "exported_at": "2024-06-02T20:42:07+00:00",
"name": "Team Fortress 2", "name": "Team Fortress 2",
"author": "panel@example.com", "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.", "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.",
"features": [ "features": [
"gsl_token", "gsl_token",
@ -39,6 +40,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": false, "user_editable": false,
"rules": "required|in:232250", "rules": "required|in:232250",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -49,6 +51,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|regex:\/^(\\w{1,20})$\/", "rules": "required|regex:\/^(\\w{1,20})$\/",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -59,6 +62,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|string|alpha_num|size:32", "rules": "required|string|alpha_num|size:32",
"sort": null,
"field_type": "text" "field_type": "text"
} }
] ]

View File

@ -4,9 +4,10 @@
"version": "PTDL_v2", "version": "PTDL_v2",
"update_url": null "update_url": null
}, },
"exported_at": "2024-05-21T21:40:24-04:00", "exported_at": "2024-06-04T22:53:03+00:00",
"name": "Mumble Server", "name": "Mumble Server",
"author": "panel@example.com", "author": "panel@example.com",
"uuid": "727ee758-7fb2-4979-972b-d3eba4e1e9f0",
"description": "Mumble is an open source, low-latency, high quality voice chat software primarily intended for use while gaming.", "description": "Mumble is an open source, low-latency, high quality voice chat software primarily intended for use while gaming.",
"features": null, "features": null,
"docker_images": { "docker_images": {
@ -15,7 +16,7 @@
"file_denylist": [], "file_denylist": [],
"startup": "mumble-server -fg -ini murmur.ini", "startup": "mumble-server -fg -ini murmur.ini",
"config": { "config": {
"files": "{\r\n \"murmur.ini\": {\r\n \"parser\": \"ini\",\r\n \"find\": {\r\n \"database\": \"\/home\/container\/murmur.sqlite\",\r\n \"logfile\": \"\/home\/container\/murmur.log\",\r\n \"port\": \"{{server.build.default.port}}\",\r\n \"host\": \"\",\r\n \"users\": \"{{server.build.env.MAX_USERS}}\"\r\n }\r\n }\r\n}", "files": "{\r\n \"murmur.ini\": {\r\n \"parser\": \"ini\",\r\n \"find\": {\r\n \"database\": \"\/home\/container\/murmur.sqlite\",\r\n \"logfile\": \"\/home\/container\/murmur.log\",\r\n \"port\": \"{{server.allocations.default.port}}\",\r\n \"host\": \"\",\r\n \"users\": \"{{server.environment.MAX_USERS}}\"\r\n }\r\n }\r\n}",
"startup": "{\r\n \"done\": \"Server listening on\"\r\n}", "startup": "{\r\n \"done\": \"Server listening on\"\r\n}",
"logs": "{}", "logs": "{}",
"stop": "^C" "stop": "^C"
@ -36,7 +37,8 @@
"user_viewable": true, "user_viewable": true,
"user_editable": false, "user_editable": false,
"rules": "required|numeric|digits_between:1,5", "rules": "required|numeric|digits_between:1,5",
"sort": null,
"field_type": "text" "field_type": "text"
} }
] ]
} }

View File

@ -4,9 +4,10 @@
"version": "PTDL_v2", "version": "PTDL_v2",
"update_url": null "update_url": null
}, },
"exported_at": "2024-05-08T11:53:32+02:00", "exported_at": "2024-06-02T20:42:08+00:00",
"name": "Teamspeak3 Server", "name": "Teamspeak3 Server",
"author": "panel@example.com", "author": "panel@example.com",
"uuid": "983b1fac-d322-4d5f-a636-436127326b37",
"description": "VoIP software designed with security in mind, featuring crystal clear voice quality, endless customization options, and scalabilty up to thousands of simultaneous users.", "description": "VoIP software designed with security in mind, featuring crystal clear voice quality, endless customization options, and scalabilty up to thousands of simultaneous users.",
"features": null, "features": null,
"docker_images": { "docker_images": {
@ -36,6 +37,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|string|max:6", "rules": "required|string|max:6",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -46,6 +48,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": false, "user_editable": false,
"rules": "required|integer|between:1025,65535", "rules": "required|integer|between:1025,65535",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -56,6 +59,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": false, "user_editable": false,
"rules": "required|integer|between:1025,65535", "rules": "required|integer|between:1025,65535",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -66,6 +70,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|string|max:12", "rules": "required|string|max:12",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -76,6 +81,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": false, "user_editable": false,
"rules": "required|integer|between:1025,65535", "rules": "required|integer|between:1025,65535",
"sort": null,
"field_type": "text" "field_type": "text"
}, },
{ {
@ -86,6 +92,7 @@
"user_viewable": true, "user_viewable": true,
"user_editable": false, "user_editable": false,
"rules": "required|integer|between:1025,65535", "rules": "required|integer|between:1025,65535",
"sort": null,
"field_type": "text" "field_type": "text"
} }
] ]

View File

@ -0,0 +1,43 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
$nameToUuidMapping = [
'Bungeecord' => '9e6b409e-4028-4947-aea8-50a2c404c271',
'Forge Minecraft' => 'ed072427-f209-4603-875c-f540c6dd5a65',
'Paper' => '5da37ef6-58da-4169-90a6-e683e1721247',
'Sponge (SpongeVanilla)' => 'f0d2f88f-1ff3-42a0-b03f-ac44c5571e6d',
'Vanilla Minecraft' => '9ac39f3d-0c34-4d93-8174-c52ab9e6c57b',
'Counter-Strike: Global Offensive' => '437c367d-06be-498f-a604-fdad135504d7',
'Custom Source Engine Game' => '2a42d0c2-c0ba-4067-9a0a-9b95d77a3490',
'Garrys Mod' => '60ef81d4-30a2-4d98-ab64-f59c69e2f915',
'Insurgency' => 'a5702286-655b-4069-bf1e-925c7300b61a',
'Team Fortress 2' => '7f8eb681-b2c8-4bf8-b9f4-d79ff70b6e5d',
'Mumble Server' => '727ee758-7fb2-4979-972b-d3eba4e1e9f0',
'Teamspeak3 Server' => '983b1fac-d322-4d5f-a636-436127326b37',
'Rust' => 'bace2dfb-209c-452a-9459-7d6f340b07ae',
];
foreach ($nameToUuidMapping as $name => $uuid) {
DB::table('eggs')
->where('name', $name)
->update(['uuid' => $uuid]);
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
//
}
};

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('nodes', function (Blueprint $table) {
$table->string('daemon_sftp_alias')->nullable()->after('daemon_sftp');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('nodes', function (Blueprint $table) {
$table->dropColumn('daemon_sftp_alias');
});
}
};

View File

@ -0,0 +1,92 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
$eggs = DB::table('eggs')->get();
foreach ($eggs as $egg) {
$updatedPort = str_replace(
'server.build.default.port',
'server.allocations.default.port',
$egg->config_files
);
if ($updatedPort !== $egg->config_files) {
$egg->config_files = $updatedPort;
echo "Processed Port update with ID: {$egg->name}\n";
}
$updatedIp = str_replace(
'server.build.default.ip',
'server.allocations.default.ip',
$egg->config_files
);
if ($updatedIp !== $egg->config_files) {
$egg->config_files = $updatedIp;
echo "Processed IP update with ID: {$egg->name}\n";
}
$updatedEnv = str_replace(
'server.build.env.',
'server.environment.',
$egg->config_files
);
if ($updatedEnv !== $egg->config_files) {
$egg->config_files = $updatedEnv;
echo "Processed ENV update with ID: {$egg->name}\n";
}
DB::table('eggs')
->where('id', $egg->id)
->update(['config_files' => $egg->config_files]);
}
}
public function down(): void
{
$eggs = DB::table('eggs')->get();
foreach ($eggs as $egg) {
$revertedEnv = str_replace(
'server.environment.',
'server.build.env.',
$egg->config_files
);
if ($revertedEnv !== $egg->config_files) {
$egg->config_files = $revertedEnv;
}
$revertedIp = str_replace(
'server.allocations.default.ip',
'server.build.default.ip',
$egg->config_files
);
if ($revertedIp !== $egg->config_files) {
$egg->config_files = $revertedIp;
}
$revertedPort = str_replace(
'server.allocations.default.port',
'server.build.default.port',
$egg->config_files
);
if ($revertedPort !== $egg->config_files) {
$egg->config_files = $revertedPort;
}
DB::table('eggs')
->where('id', $egg->id)
->update(['config_files' => $egg->config_files]);
}
}
};

View File

@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
$eggs = DB::table('eggs')->get();
foreach ($eggs as $egg) {
$updatedEnv = str_replace(
'server.build.environment.',
'server.environment.',
$egg->config_files
);
if ($updatedEnv !== $egg->config_files) {
$egg->config_files = $updatedEnv;
echo "Processed ENV update with ID: {$egg->name}\n";
}
DB::table('eggs')
->where('id', $egg->id)
->update(['config_files' => $egg->config_files]);
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
// We shouldn't revert this...
}
};

View File

@ -37,6 +37,7 @@ return [
'upload_size' => "'Enter the maximum filesize upload", 'upload_size' => "'Enter the maximum filesize upload",
'daemonListen' => 'Enter the daemon listening port', 'daemonListen' => 'Enter the daemon listening port',
'daemonSFTP' => 'Enter the daemon SFTP listening port', 'daemonSFTP' => 'Enter the daemon SFTP listening port',
'daemonSFTPAlias' => 'Enter the daemon SFTP alias (can be empty)',
'daemonBase' => 'Enter the base folder', 'daemonBase' => 'Enter the base folder',
'succes1' => 'Successfully created a new node with the name: ', 'succes1' => 'Successfully created a new node with the name: ',
'succes2' => 'and has an id of: ', 'succes2' => 'and has an id of: ',

View File

@ -19,6 +19,10 @@ return [
'button_issues' => 'Create Issue', 'button_issues' => 'Create Issue',
'button_features' => 'Discuss Features', 'button_features' => 'Discuss Features',
], ],
'intro-update' => [
'heading' => 'Update available',
'content' => ':latestVersion is available! Read our documentation to update your Panel.',
],
'intro-first-node' => [ 'intro-first-node' => [
'heading' => 'No Nodes Detected', 'heading' => 'No Nodes Detected',
'content' => "It looks like you don't have any Nodes set up yet, but don't worry because you click the action button to create your first one!", 'content' => "It looks like you don't have any Nodes set up yet, but don't worry because you click the action button to create your first one!",

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 +1 @@
function r({state:i}){return{state:i,rows:[],shouldUpdateRows:!0,init:function(){this.updateRows(),this.rows.length<=0?this.rows.push({key:"",value:""}):this.updateState(),this.$watch("state",(t,e)=>{let s=o=>o===null?0:Array.isArray(o)?o.length:typeof o!="object"?0:Object.keys(o).length;s(t)===0&&s(e)===0||this.updateRows()})},addRow:function(){this.rows.push({key:"",value:""}),this.updateState()},deleteRow:function(t){this.rows.splice(t,1),this.rows.length<=0&&this.addRow(),this.updateState()},reorderRows:function(t){let e=Alpine.raw(this.rows),s=e.splice(t.oldIndex,1)[0];e.splice(t.newIndex,0,s),this.rows=e,this.updateState()},updateRows:function(){if(!this.shouldUpdateRows){this.shouldUpdateRows=!0;return}let t=[];for(let[e,s]of Object.entries(this.state??{}))t.push({key:e,value:s});this.rows=t},updateState:function(){let t={};this.rows.forEach(e=>{e.key===""||e.key===null||(t[e.key]=e.value)}),this.shouldUpdateRows=!1,this.state=t}}}export{r as default}; function r({state:o}){return{state:o,rows:[],shouldUpdateRows:!0,init:function(){this.updateRows(),this.rows.length<=0?this.rows.push({key:"",value:""}):this.updateState(),this.$watch("state",(t,e)=>{let s=i=>i===null?0:Array.isArray(i)?i.length:typeof i!="object"?0:Object.keys(i).length;s(t)===0&&s(e)===0||this.updateRows()})},addRow:function(){this.rows.push({key:"",value:""}),this.updateState()},deleteRow:function(t){this.rows.splice(t,1),this.rows.length<=0&&this.addRow(),this.updateState()},reorderRows:function(t){let e=Alpine.raw(this.rows);this.rows=[];let s=e.splice(t.oldIndex,1)[0];e.splice(t.newIndex,0,s),this.$nextTick(()=>{this.rows=e,this.updateState()})},updateRows:function(){if(!this.shouldUpdateRows){this.shouldUpdateRows=!0;return}let t=[];for(let[e,s]of Object.entries(this.state??{}))t.push({key:e,value:s});this.rows=t},updateState:function(){let t={};this.rows.forEach(e=>{e.key===""||e.key===null||(t[e.key]=e.value)}),this.shouldUpdateRows=!1,this.state=t}}}export{r as default};

File diff suppressed because one or more lines are too long

View File

@ -26,46 +26,20 @@ This gives you the power to run game servers without bloating machines with a ho
Some of our popular eggs include but are not limited to: Some of our popular eggs include but are not limited to:
* [Minecraft](https://github.com/pelican-eggs/minecraft) | Category | Eggs | | | |
* Paper |----------------------------------------------------------------------|-----------------|---------------|--------------------|----------------|
* Sponge | [Minecraft](https://github.com/pelican-eggs/minecraft) | Paper | Sponge | Bungeecord | Waterfall |
* Bungeecord | [SteamCMD](https://github.com/pelican-eggs/steamcmd) | 7 Days to Die | ARK: Survival | Arma 3 | Counter Strike |
* Waterfall | | DayZ | Enshrouded | Left 4 Dead | Palworld |
* [SteamCMD](https://github.com/pelican-eggs/steamcmd) | | Project Zomboid | Satisfactory | Sons of the Forest | Starbound |
* 7 Days to Die | [Standalone Games](https://github.com/pelican-eggs/games-standalone) | Among Us | Factorio | FTL | GTA |
* ARK: Survival | | Kerbal Space | Mindustry | Rimworld | Terraria |
* ARMA | [Discord Bots](https://github.com/pelican-eggs/chatbots) | Redbot | JMusicBot | JMusicBot | Dynamica |
* Counter Strike | [Voice Servers](https://github.com/pelican-eggs/voice) | Mumble | Teamspeak | Lavalink | |
* DayZ | [Software](https://github.com/pelican-eggs/software) | Elasticsearch | Gitea | Grafana | RabbitMQ |
* Enshrouded | [Programming](https://github.com/pelican-eggs/generic) | Node.js | Python | Java | C# |
* Left 4 Dead | [Databases](https://github.com/pelican-eggs/database) | Redis | MariaDB | PostgreSQL | MongoDB |
* Palworld | [Storage](https://github.com/pelican-eggs/storage) | S3 | SFTP Share | | |
* Project Zomboid | [Monitoring](https://github.com/pelican-eggs/monitoring) | Prometheus | Loki | | |
* Sons of the Forest
* [Other Games](https://github.com/pelican-eggs/games)
* Among Us
* Factorio
* GTA
* Rimworld
* Terraria
* [Discord Bots](https://github.com/pelican-eggs/chatbots)
* Redbot
* JMusicBot
* SinusBot
* Dynamica
* [Software](https://github.com/pelican-eggs/software)
* [Programming Languages](https://github.com/pelican-eggs/generic)
* C#
* Java
* Lua
* Node.js
* Python
* [Database](https://github.com/pelican-eggs/database)
* Redis
* MariaDB
* PostgreSQL
* [Voice Servers](https://github.com/pelican-eggs/voice)
* [Storage](https://github.com/pelican-eggs/storage)
* [Monitoring](https://github.com/pelican-eggs/monitoring)
Copyright Pelican® 2024 *Copyright Pelican® 2024*

View File

@ -21,6 +21,7 @@ export interface Server {
status: ServerStatus; status: ServerStatus;
sftpDetails: { sftpDetails: {
ip: string; ip: string;
alias: string;
port: number; port: number;
}; };
invocation: string; invocation: string;
@ -57,6 +58,7 @@ export const rawDataToServerObject = ({ attributes: data }: FractalResponseData)
dockerImage: data.docker_image, dockerImage: data.docker_image,
sftpDetails: { sftpDetails: {
ip: data.sftp_details.ip, ip: data.sftp_details.ip,
alias: data.sftp_details.alias,
port: data.sftp_details.port, port: data.sftp_details.port,
}, },
description: data.description ? (data.description.length > 0 ? data.description : null) : null, description: data.description ? (data.description.length > 0 ? data.description : null) : null,

View File

@ -40,7 +40,7 @@ const ModalContent = ({ ...props }: RequiredModalProps) => {
label={'Ignored Files & Directories'} label={'Ignored Files & Directories'}
description={` description={`
Enter the files or folders to ignore while generating this backup. Leave blank to use Enter the files or folders to ignore while generating this backup. Leave blank to use
the contents of the .panelignore file in the root of the server directory if present. the contents of the .pelicanignore file in the root of the server directory if present.
Wildcard matching of files and folders is supported in addition to negating a rule by Wildcard matching of files and folders is supported in addition to negating a rule by
prefixing the path with an exclamation point. prefixing the path with an exclamation point.
`} `}

View File

@ -91,13 +91,14 @@ export default () => {
<FileManagerBreadcrumbs withinFileEditor isNewFile={action !== 'edit'} /> <FileManagerBreadcrumbs withinFileEditor isNewFile={action !== 'edit'} />
</div> </div>
</ErrorBoundary> </ErrorBoundary>
{hash.replace(/^#/, '').endsWith('.panelignore') && ( {hash.replace(/^#/, '').endsWith('.pelicanignore') && (
<div css={tw`mb-4 p-4 border-l-4 bg-neutral-900 rounded border-cyan-400`}> <div css={tw`mb-4 p-4 border-l-4 bg-neutral-900 rounded border-cyan-400`}>
<p css={tw`text-neutral-300 text-sm`}> <p css={tw`text-neutral-300 text-sm`}>
You&apos;re editing a <code css={tw`font-mono bg-black rounded py-px px-1`}>.panelignore</code>{' '} You&apos;re editing a{' '}
file. Any files or directories listed in here will be excluded from backups. Wildcards are <code css={tw`font-mono bg-black rounded py-px px-1`}>.pelicanignore</code> directories listed
supported by using an asterisk (<code css={tw`font-mono bg-black rounded py-px px-1`}>*</code>). in here will be excluded from backups. Wildcards are supported by using an supported by using an
You can negate a prior rule by prepending an exclamation point ( asterisk (<code css={tw`font-mono bg-black rounded py-px px-1`}>*</code>). You can negate a
prior rule by prepending an exclamation point (
<code css={tw`font-mono bg-black rounded py-px px-1`}>!</code>). <code css={tw`font-mono bg-black rounded py-px px-1`}>!</code>).
</p> </p>
</div> </div>

View File

@ -168,7 +168,7 @@ const TaskDetailsModal = ({ schedule, task }: Props) => {
<FormikFieldWrapper <FormikFieldWrapper
name={'payload'} name={'payload'}
description={ description={
'Optional. Include the files and folders to be excluded in this backup. By default, the contents of your .panelignore file will be used. If you have reached your backup limit, the oldest backup will be rotated.' 'Optional. Include the files and folders to be excluded in this backup. By default, the contents of your .pelicanignore file will be used. If you have reached your backup limit, the oldest backup will be rotated.'
} }
> >
<FormikField as={Textarea} name={'payload'} rows={6} /> <FormikField as={Textarea} name={'payload'} rows={6} />

View File

@ -31,8 +31,12 @@ export default () => {
<TitledGreyBox title={'SFTP Details'} css={tw`mb-6 md:mb-10`}> <TitledGreyBox title={'SFTP Details'} css={tw`mb-6 md:mb-10`}>
<div> <div>
<Label>Server Address</Label> <Label>Server Address</Label>
<CopyOnClick text={`sftp://${ip(sftp.ip)}:${sftp.port}`}> <CopyOnClick text={`sftp://${sftp.alias ? sftp.alias : ip(sftp.ip)}:${sftp.port}`}>
<Input type={'text'} value={`sftp://${ip(sftp.ip)}:${sftp.port}`} readOnly /> <Input
type={'text'}
value={`sftp://${sftp.alias ? sftp.alias : ip(sftp.ip)}:${sftp.port}`}
readOnly
/>
</CopyOnClick> </CopyOnClick>
</div> </div>
<div css={tw`mt-6`}> <div css={tw`mt-6`}>
@ -50,7 +54,10 @@ export default () => {
</div> </div>
</div> </div>
<div css={tw`ml-4`}> <div css={tw`ml-4`}>
<a href={`sftp://${username}.${id}@${ip(sftp.ip)}:${sftp.port}`}> <a
href={`sftp://${username}.${id}@${sftp.alias ? sftp.alias : ip(sftp.ip)}:${sftp.port
}`}
>
<Button.Text variant={Button.Variants.Secondary}>Launch SFTP</Button.Text> <Button.Text variant={Button.Variants.Secondary}>Launch SFTP</Button.Text>
</a> </a>
</div> </div>

View File

@ -4,7 +4,7 @@
:actions="$this->getCachedHeaderActions()" :actions="$this->getCachedHeaderActions()"
:breadcrumbs="filament()->hasBreadcrumbs() ? $this->getBreadcrumbs() : []" :breadcrumbs="filament()->hasBreadcrumbs() ? $this->getBreadcrumbs() : []"
:heading=" trans('dashboard/index.heading')" :heading=" trans('dashboard/index.heading')"
:subheading="trans('strings.version', ['version' => config('app.version')])" :subheading="trans('strings.version', ['version' => $version])"
></x-filament-panels::header> ></x-filament-panels::header>
<p>{{ trans('dashboard/index.expand_sections') }}</p> <p>{{ trans('dashboard/index.expand_sections') }}</p>
@ -30,6 +30,22 @@
</x-filament::section> </x-filament::section>
@endif @endif
@if (!$isLatest)
<x-filament::section
icon="tabler-info-circle"
icon-color="warning"
id="intro-update"
collapsible
persist-collapsed
:header-actions="$updateActions"
>
<x-slot name="heading">{{ trans('dashboard/index.sections.intro-update.heading') }}</x-slot>
<p>{{ trans('dashboard/index.sections.intro-update.content', ['latestVersion' => $latestVersion]) }}</p>
</x-filament::section>
@endif
{{-- No Nodes Created --}} {{-- No Nodes Created --}}
@if ($nodesCount <= 0) @if ($nodesCount <= 0)
<x-filament::section <x-filament::section