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_PATH=/
SESSION_DOMAIN=null
# Set this to true, and set start & end ports to auto create allocations.
PANEL_CLIENT_ALLOCATIONS_ENABLED=false
PANEL_CLIENT_ALLOCATIONS_RANGE_START=
PANEL_CLIENT_ALLOCATIONS_RANGE_END=

View File

@ -59,7 +59,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
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
coverage: none
@ -119,7 +119,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
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
coverage: none

1
.gitignore vendored
View File

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

View File

@ -118,7 +118,9 @@ class AppSettingsCommand extends Command
}
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());
@ -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
{

View File

@ -98,7 +98,7 @@ class DatabaseSettingsCommand extends Command
} elseif ($this->variables['DB_CONNECTION'] === 'sqlite') {
$this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask(
'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
{
$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';
if (file_exists($path) && !$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.');
$fileExists = file_exists($path);
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;
}
$user = $this->option('user') ?? $this->ask('User', 'www-data');
$group = $this->option('group') ?? $this->ask('Group', 'www-data');
$user = $this->option('user') ?? $this->ask('Webserver User', 'www-data');
$group = $this->option('group') ?? $this->ask('Webserver Group', 'www-data');
$afterRedis = $this->option('use-redis') ? '\nAfter=redis-server.service' : '';
@ -45,7 +46,7 @@ Description=Pelican Queue Service$afterRedis
User=$user
Group=$group
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
StartLimitBurst=30
RestartSec=5s
@ -60,13 +61,24 @@ WantedBy=multi-user.target
return;
}
$result = Process::run("systemctl enable --now $serviceName.service");
if ($result->failed()) {
$this->error('Error enabling service: ' . $result->errorOutput());
if ($fileExists) {
$result = Process::run("systemctl restart $serviceName.service");
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->table([], [
['Panel Version', config('app.version')],
['Panel Version', $this->versionService->versionData()['version']],
['Latest Version', $this->versionService->getPanel()],
['Up-to-Date', $this->versionService->isLatestPanel() ? 'Yes' : $this->formatText('No', 'bg=red')],
], 'compact');

View File

@ -25,6 +25,7 @@ class MakeNodeCommand extends Command
{--uploadSize= : Enter the maximum upload filesize.}
{--daemonListeningPort= : Enter the daemon listening port.}
{--daemonSFTPPort= : Enter the daemon SFTP listening port.}
{--daemonSFTPAlias= : Enter the daemon SFTP alias.}
{--daemonBase= : Enter the base folder.}';
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['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_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');
$node = $this->creationService->handle($data);

View File

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

View File

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

View File

@ -6,9 +6,9 @@ use App\Exceptions\DisplayException;
class TwoFactorAuthenticationTokenInvalid extends DisplayException
{
/**
* TwoFactorAuthenticationTokenInvalid constructor.
*/
public string $title = 'Invalid 2FA Code';
public string $icon = 'tabler-2fa';
public function __construct()
{
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\Server;
use App\Models\User;
use App\Services\Helpers\SoftwareVersionService;
use Filament\Actions\CreateAction;
use Filament\Pages\Page;
@ -29,8 +30,14 @@ class Dashboard extends Page
public function getViewData(): array
{
/** @var SoftwareVersionService $softwareVersionService */
$softwareVersionService = app(SoftwareVersionService::class);
return [
'inDevelopment' => config('app.version') === 'canary',
'version' => $softwareVersionService->versionData()['version'],
'latestVersion' => $softwareVersionService->getPanel(),
'isLatest' => $softwareVersionService->isLatestPanel(),
'eggsCount' => Egg::query()->count(),
'nodesList' => ListNodes::getUrl(),
'nodesCount' => Node::query()->count(),
@ -43,6 +50,13 @@ class Dashboard extends Page
->icon('tabler-brand-github')
->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' => [
CreateAction::make()
->label(trans('dashboard/index.sections.intro-first-node.button_label'))
@ -53,7 +67,7 @@ class Dashboard extends Page
CreateAction::make()
->label(trans('dashboard/index.sections.intro-support.button_donate'))
->icon('tabler-cash')
->url('https://pelican.dev/donate', true)
->url($softwareVersionService->getDonations(), true)
->color('success'),
],
'helpActions' => [

View File

@ -3,6 +3,7 @@
namespace App\Filament\Resources\DatabaseHostResource\Pages;
use App\Filament\Resources\DatabaseHostResource;
use App\Models\DatabaseHost;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
use Filament\Forms;
@ -71,7 +72,9 @@ class EditDatabaseHost extends EditRecord
protected function getHeaderActions(): array
{
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'),
];
}

View File

@ -8,6 +8,7 @@ use App\Services\Eggs\Sharing\EggImporterService;
use Exception;
use Filament\Actions;
use Filament\Forms;
use Filament\Forms\Components\Tabs;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Table;
@ -62,21 +63,58 @@ class ListEggs extends ListRecords
Actions\Action::make('import')
->label('Import')
->form([
Forms\Components\FileUpload::make('egg')
->acceptedFileTypes(['application/json'])
->storeFiles(false)
->multiple(),
Tabs::make('Tabs')
->tabs([
Tabs\Tab::make('From File')
->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 {
/** @var TemporaryUploadedFile $eggFile */
$eggFile = $data['egg'];
/** @var EggImporterService $eggImportService */
$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 {
$eggImportService->handle($file);
$eggImportService->fromUrl($data['url']);
} catch (Exception $exception) {
Notification::make()
->title('Import Failed')

View File

@ -4,8 +4,10 @@ namespace App\Filament\Resources\NodeResource\Pages;
use App\Filament\Resources\NodeResource;
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 Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
class CreateNode extends CreateRecord
@ -18,19 +20,19 @@ class CreateNode extends CreateRecord
public function form(Forms\Form $form): Forms\Form
{
return $form->schema([
Tabs::make('Tabs')
->columns([
'default' => 2,
'sm' => 3,
'md' => 3,
'lg' => 4,
])
->persistTabInQueryString()
->columnSpanFull()
->tabs([
Tabs\Tab::make('Basic Settings')
return $form
->schema([
Wizard::make([
Wizard\Step::make('basic')
->label('Basic Settings')
->icon('tabler-server')
->columnSpanFull()
->columns([
'default' => 2,
'sm' => 3,
'md' => 3,
'lg' => 4,
])
->schema([
Forms\Components\TextInput::make('fqdn')
->columnSpan(2)
@ -55,7 +57,7 @@ class CreateNode extends CreateRecord
return "
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!
";
";
})
->hintColor('danger')
->hint(function ($state) {
@ -184,30 +186,17 @@ class CreateNode extends CreateRecord
])
->default(fn () => request()->isSecure() ? 'https' : 'http'),
]),
Tabs\Tab::make('Advanced Settings')
Wizard\Step::make('advanced')
->label('Advanced Settings')
->icon('tabler-server-cog')
->columnSpanFull()
->columns([
'default' => 2,
'sm' => 3,
'md' => 3,
'lg' => 4,
])
->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')
->label('Maintenance Mode')->inline()
->columnSpan(1)
@ -222,13 +211,46 @@ class CreateNode extends CreateRecord
true => 'danger',
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')
->label('Tags')
->disabled()
->placeholder('Not Implemented')
->hintIcon('tabler-question-mark')
->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()
->columns(6)
->columnSpanFull()
@ -353,8 +375,17 @@ class CreateNode extends CreateRecord
->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
@ -363,4 +394,9 @@ class CreateNode extends CreateRecord
'tab' => '-configuration-tab',
];
}
protected function getFormActions(): array
{
return [];
}
}

View File

@ -215,6 +215,18 @@ class EditNode extends EditRecord
->minValue(1)
->maxValue(1024)
->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')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
->label('Automatic Allocation')->inline()

View File

@ -113,7 +113,7 @@ class AllocationsRelationManager extends RelationManager
$start = max((int) $start, 0);
$end = min((int) $end, 2 ** 16 - 1);
for ($i = $start; $i <= $end; $i++) {
foreach (range($start, $end) as $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\Validation\Rules\Password;
/**
* @method User getUser()
*/
class EditProfile extends \Filament\Pages\Auth\EditProfile
{
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'),
];
}
/** @var TwoFactorSetupService */
$setupService = app(TwoFactorSetupService::class);
['image_url_data' => $url, 'secret' => $secret] = cache()->remember(
@ -126,6 +130,7 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
$options = new QROptions([
'svgLogo' => public_path('pelican.svg'),
'svgLogoScale' => 0.05,
'addLogoSpace' => true,
'logoSpaceWidth' => 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
// 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
// @phpstan-ignore property.protected
$options->version = Version::AUTO;
// $options->outputInterface = QRSvgWithLogo::class;
// @phpstan-ignore property.protected
$options->outputBase64 = false;
// @phpstan-ignore property.protected
$options->eccLevel = EccLevel::H; // ECC level H is necessary when using logos
// @phpstan-ignore property.protected
$options->addQuietzone = true;
// $options->drawLightModules = true;
// @phpstan-ignore property.protected
$options->connectPaths = true;
// @phpstan-ignore property.protected
$options->drawCircularModules = true;
// $options->circleRadius = 0.45;
// @phpstan-ignore property.protected
$options->svgDefs = '<linearGradient id="gradient" x1="100%" y2="100%">
<stop stop-color="#7dd4fc" offset="0"/>
<stop stop-color="#38bdf8" offset="0.5"/>
@ -196,8 +203,8 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
])->headerActions([
Action::make('Create')
->successRedirectUrl(route('filament.admin.auth.profile', ['tab' => '-api-keys-tab']))
->action(function (Get $get, Action $action) {
$token = auth()->user()->createToken(
->action(function (Get $get, Action $action, $user) {
$token = $user->createToken(
$get('description'),
$get('allowed_ips'),
);

View File

@ -3,6 +3,7 @@
namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource;
use App\Services\Exceptions\FilamentExceptionHandler;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
use App\Models\User;
@ -66,7 +67,9 @@ class EditUser extends EditRecord
protected function getHeaderActions(): array
{
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'),
];
}
@ -75,4 +78,9 @@ class EditUser extends EditRecord
{
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
{
$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();
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
{
$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();
return redirect()->route('admin.eggs.view', ['egg' => $egg]);

View File

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

View File

@ -3,7 +3,6 @@
namespace App\Http\Controllers\Admin\Nodes;
use Illuminate\View\View;
use Illuminate\Http\Request;
use App\Models\Node;
use Illuminate\Support\Collection;
use App\Models\Allocation;
@ -29,16 +28,10 @@ class NodeViewController extends Controller
/**
* 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');
$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', [
'node' => $node,
'version' => $this->versionService,
@ -48,7 +41,7 @@ class NodeViewController extends Controller
/**
* 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', [
'node' => $node,
@ -58,7 +51,7 @@ class NodeViewController extends Controller
/**
* 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'));
}
@ -66,7 +59,7 @@ class NodeViewController extends Controller
/**
* Return the node allocation management page.
*/
public function allocations(Request $request, Node $node): View
public function allocations(Node $node): View
{
$node->setRelation(
'allocations',
@ -92,7 +85,7 @@ class NodeViewController extends Controller
/**
* 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([
'node' => Collection::wrap($node->makeVisible(['daemon_token_id', 'daemon_token']))

View File

@ -3,7 +3,6 @@
namespace App\Http\Controllers\Admin\Servers;
use Illuminate\View\View;
use Illuminate\Http\Request;
use App\Models\Server;
use Spatie\QueryBuilder\QueryBuilder;
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
* 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'))
->allowedFilters([

View File

@ -3,13 +3,13 @@
namespace App\Http\Controllers\Admin;
use App\Enums\ServerState;
use Filament\Notifications\Notification;
use Illuminate\Http\Request;
use App\Models\User;
use Illuminate\Http\Response;
use App\Models\Mount;
use App\Models\Server;
use App\Models\Database;
use App\Models\MountServer;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use App\Exceptions\DisplayException;
@ -70,7 +70,7 @@ class ServersController extends Controller
* @throws \App\Exceptions\DisplayException
* @throws \App\Exceptions\Model\DataValidationException
*/
public function toggleInstall(Server $server): RedirectResponse
public function toggleInstall(Server $server)
{
if ($server->status === ServerState::InstallFailed) {
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->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\Model\DataValidationException
*/
public function reinstallServer(Server $server): RedirectResponse
public function reinstallServer(Server $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
{
$mountServer = (new MountServer())->forceFill([
'mount_id' => $request->input('mount_id'),
'server_id' => $server->id,
]);
$mountServer->saveOrFail();
$server->mounts()->attach($request->input('mount_id'));
$this->alert->success('Mount was added successfully.')->flash();
@ -245,7 +247,7 @@ class ServersController extends Controller
*/
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();

View File

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

View File

@ -33,10 +33,9 @@ class StoreNodeRequest extends ApplicationApiRequest
'upload_size',
'daemon_listen',
'daemon_sftp',
'daemon_sftp_alias',
'daemon_base',
])->mapWithKeys(function ($value, $key) {
$key = ($key === 'daemon_sftp') ? 'daemon_sftp' : $key;
return [snake_case($key) => $value];
})->toArray();
}
@ -60,12 +59,8 @@ class StoreNodeRequest extends ApplicationApiRequest
public function validated($key = null, $default = null): array
{
$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');
unset($response['daemon_base'], $response['daemon_listen'], $response['daemon_sftp']);
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',
'disk_overallocate', 'cpu', 'cpu_overallocate',
'upload_size', 'daemon_base',
'daemon_sftp', 'daemon_listen',
'daemon_sftp', 'daemon_sftp_alias', 'daemon_listen',
'description', 'maintenance_mode',
];
@ -63,6 +63,7 @@ class Node extends Model
'cpu_overallocate' => 'required|numeric|min:-1',
'daemon_base' => 'sometimes|required|regex:/^([\/][\d\w.\-\/]+)$/',
'daemon_sftp' => 'required|numeric|between:1,65535',
'daemon_sftp_alias' => 'nullable|string',
'daemon_listen' => 'required|numeric|between:1,65535',
'maintenance_mode' => 'boolean',
'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
{
return 'id';

View File

@ -5,6 +5,7 @@ namespace App\Models;
use App\Enums\ServerState;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Query\JoinClause;
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\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use App\Exceptions\Http\Server\ServerStateConflictException;
class Server extends Model
@ -224,12 +224,9 @@ class Server extends Model
return $this->hasMany(Backup::class);
}
/**
* Returns all mounts that have this server has mounted.
*/
public function mounts(): HasManyThrough
public function mounts(): BelongsToMany
{
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\ApiKey;
use App\Models\Node;
use App\Services\Helpers\SoftwareVersionService;
use Dedoc\Scramble\Scramble;
use Dedoc\Scramble\Support\Generator\OpenApi;
use Dedoc\Scramble\Support\Generator\SecurityScheme;
@ -30,8 +31,9 @@ class AppServiceProvider extends ServiceProvider
{
Schema::defaultStringLength(191);
View::share('appVersion', $this->versionData()['version'] ?? 'undefined');
View::share('appIsGit', $this->versionData()['is_git'] ?? false);
$versionData = app(SoftwareVersionService::class)->versionData();
View::share('appVersion', $versionData['version'] ?? 'undefined');
View::share('appIsGit', $versionData['is_git'] ?? false);
Paginator::useBootstrap();
@ -96,34 +98,6 @@ class AppServiceProvider extends ServiceProvider
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
{
Sanctum::usePersonalAccessTokenModel(ApiKey::class);

View File

@ -124,38 +124,6 @@ class EggConfigurationService
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
{
preg_match_all('/{{(?<key>[\w.-]*)}}/', $value, $matches);
@ -175,8 +143,6 @@ class EggConfigurationService
continue;
}
$value = $this->replaceLegacyModifiers($key, $value);
// 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
// interface to proxy through, but the Panel would be unaware of that.
@ -198,7 +164,7 @@ class EggConfigurationService
// variable from the server configuration.
$plucked = Arr::get(
$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
{
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.
*
@ -26,11 +37,20 @@ class EggParserService
$version = $parsed['meta']['version'] ?? '';
return match ($version) {
$parsed = match ($version) {
'PTDL_v1' => $this->convertToV2($parsed),
'PTDL_v2' => $parsed,
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 Illuminate\Database\ConnectionInterface;
use App\Services\Eggs\EggParserService;
use Spatie\TemporaryDirectory\TemporaryDirectory;
class EggImporterService
{
@ -21,7 +22,7 @@ class EggImporterService
*
* @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable
*/
public function handle(UploadedFile $file): Egg
public function fromFile(UploadedFile $file): Egg
{
$parsed = $this->parser->handle($file);
@ -45,4 +46,20 @@ class EggImporterService
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 Illuminate\Database\ConnectionInterface;
use App\Services\Eggs\EggParserService;
use Spatie\TemporaryDirectory\TemporaryDirectory;
class EggUpdateImporterService
{
@ -23,7 +24,7 @@ class EggUpdateImporterService
*
* @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);
@ -47,4 +48,20 @@ class EggUpdateImporterService
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';
}
/**
* 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.
*/
@ -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
{
$data['id'] = $node->id;
if ($resetToken) {
$data['daemon_token'] = Str::random(Node::DAEMON_TOKEN_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) {
/** @var \App\Models\Node $updated */
$updated = $node->replicate();
$updated->exists = true;
$updated->forceFill($data)->save();
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;
$this->configurationRepository->setNode($node)->update($updated);

View File

@ -40,7 +40,7 @@ class ServerConfigurationStructureService
*/
protected function returnFormat(Server $server): array
{
return [
$response = [
'uuid' => $server->uuid,
'meta' => [
'name' => $server->name,
@ -50,7 +50,6 @@ class ServerConfigurationStructureService
'environment' => $this->environment->handle($server),
'invocation' => $server->startup,
'skip_egg_scripts' => $server->skip_scripts,
'labels' => $server->docker_labels,
'build' => [
'memory_limit' => $server->memory,
'swap' => $server->swap,
@ -72,17 +71,27 @@ class ServerConfigurationStructureService
],
'mappings' => $server->getAllocationMappings(),
],
'mounts' => $server->mounts->map(function (Mount $mount) {
return [
'source' => $mount->source,
'target' => $mount->target,
'read_only' => $mount->read_only,
];
}),
'egg' => [
'id' => $server->egg->uuid,
'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;
use App\Enums\ServerState;
use Filament\Notifications\Notification;
use Webmozart\Assert\Assert;
use App\Models\Server;
use App\Repositories\Daemon\DaemonServerRepository;
@ -26,7 +27,7 @@ class SuspensionService
*
* @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]);
@ -35,11 +36,12 @@ class SuspensionService
// suspended in the database. Additionally, nothing needs to happen if the server
// is not suspended, and we try to un-suspend the instance.
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.
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.');
}

View File

@ -53,7 +53,7 @@ class TransferServerService
{
$node_id = $data['node_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.
$node = Node::query()

View File

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

View File

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

View File

@ -33,6 +33,7 @@
"s1lentium/iptools": "~1.2.0",
"spatie/laravel-fractal": "^6.2",
"spatie/laravel-query-builder": "^5.8.1",
"spatie/temporary-directory": "^2.2",
"symfony/http-client": "^7.1",
"symfony/mailgun-mailer": "^7.1",
"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();
if ($egg instanceof Egg) {
$this->updateImporterService->handle($egg, $file);
$this->updateImporterService->fromFile($egg, $file);
$this->command->info('Updated ' . $decoded['name']);
} else {
$this->importerService->handle($file);
$this->importerService->fromFile($file);
$this->command->comment('Created ' . $decoded['name']);
}
}

View File

@ -4,9 +4,10 @@
"version": "PTDL_v2",
"update_url": null
},
"exported_at": "2024-05-08T11:47:32+02:00",
"exported_at": "2024-06-04T22:51:49+00:00",
"name": "Bungeecord",
"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.",
"features": [
"eula",
@ -23,7 +24,7 @@
"file_denylist": [],
"startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}",
"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}",
"logs": "{}",
"stop": "end"
@ -44,6 +45,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "required|alpha_num|between:1,6",
"sort": null,
"field_type": "text"
},
{
@ -54,6 +56,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/",
"sort": null,
"field_type": "text"
}
]

View File

@ -4,9 +4,10 @@
"version": "PTDL_v2",
"update_url": null
},
"exported_at": "2024-05-08T11:47:58+02:00",
"exported_at": "2024-06-04T22:51:58+00:00",
"name": "Forge Minecraft",
"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.",
"features": [
"eula",
@ -23,7 +24,7 @@
"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\" )",
"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}",
"logs": "{}",
"stop": "stop"
@ -44,6 +45,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/",
"sort": null,
"field_type": "text"
},
{
@ -54,6 +56,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "required|string|max:9",
"sort": null,
"field_type": "text"
},
{
@ -64,6 +67,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "required|string|in:recommended,latest",
"sort": null,
"field_type": "text"
},
{
@ -74,6 +78,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "nullable|regex:\/^[0-9\\.\\-]+$\/",
"sort": null,
"field_type": "text"
}
]

View File

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

View File

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

View File

@ -4,9 +4,10 @@
"version": "PTDL_v2",
"update_url": null
},
"exported_at": "2024-05-08T11:49:32+02:00",
"exported_at": "2024-06-04T22:51:16+00:00",
"name": "Vanilla Minecraft",
"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.",
"features": [
"eula",
@ -23,7 +24,7 @@
"file_denylist": [],
"startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}",
"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}",
"logs": "{}",
"stop": "stop"
@ -44,6 +45,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/",
"sort": null,
"field_type": "text"
},
{
@ -54,6 +56,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "required|string|between:3,15",
"sort": null,
"field_type": "text"
}
]

View File

@ -4,9 +4,10 @@
"version": "PTDL_v2",
"update_url": null
},
"exported_at": "2024-05-08T11:57:56+02:00",
"exported_at": "2024-06-02T20:42:09+00:00",
"name": "Rust",
"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.",
"features": [
"steam_disk_space"
@ -38,6 +39,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "required|string|max:60",
"sort": null,
"field_type": "text"
},
{
@ -48,6 +50,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "required|in:vanilla,oxide,carbon",
"sort": null,
"field_type": "text"
},
{
@ -58,6 +61,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "required|string|max:20",
"sort": null,
"field_type": "text"
},
{
@ -68,6 +72,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "required|string",
"sort": null,
"field_type": "text"
},
{
@ -78,6 +83,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "nullable|url",
"sort": null,
"field_type": "text"
},
{
@ -88,6 +94,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "required|integer",
"sort": null,
"field_type": "text"
},
{
@ -98,6 +105,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "nullable|string",
"sort": null,
"field_type": "text"
},
{
@ -108,6 +116,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "required|integer",
"sort": null,
"field_type": "text"
},
{
@ -118,6 +127,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "nullable|url",
"sort": null,
"field_type": "text"
},
{
@ -128,6 +138,7 @@
"user_viewable": true,
"user_editable": false,
"rules": "required|integer",
"sort": null,
"field_type": "text"
},
{
@ -138,6 +149,7 @@
"user_viewable": true,
"user_editable": false,
"rules": "required|integer",
"sort": null,
"field_type": "text"
},
{
@ -148,6 +160,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "required|regex:\/^[\\w.-]*$\/|max:64",
"sort": null,
"field_type": "text"
},
{
@ -158,6 +171,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "required|integer",
"sort": null,
"field_type": "text"
},
{
@ -168,6 +182,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "nullable|string",
"sort": null,
"field_type": "text"
},
{
@ -178,6 +193,7 @@
"user_viewable": true,
"user_editable": false,
"rules": "required|integer",
"sort": null,
"field_type": "text"
},
{
@ -188,6 +204,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "nullable|url",
"sort": null,
"field_type": "text"
},
{
@ -198,6 +215,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "nullable|url",
"sort": null,
"field_type": "text"
},
{
@ -208,6 +226,7 @@
"user_viewable": false,
"user_editable": false,
"rules": "required|string|in:258550",
"sort": null,
"field_type": "text"
}
]

View File

@ -4,9 +4,10 @@
"version": "PTDL_v2",
"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",
"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.",
"features": [
"gsl_token",
@ -39,6 +40,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "required|string|alpha_dash",
"sort": null,
"field_type": "text"
},
{
@ -49,6 +51,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "required|string|alpha_num|size:32",
"sort": null,
"field_type": "text"
},
{
@ -59,6 +62,7 @@
"user_viewable": false,
"user_editable": false,
"rules": "required|string|max:20",
"sort": null,
"field_type": "text"
}
]

View File

@ -4,9 +4,10 @@
"version": "PTDL_v2",
"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",
"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.",
"features": [
"steam_disk_space"
@ -38,6 +39,7 @@
"user_viewable": true,
"user_editable": false,
"rules": "required|numeric|digits_between:1,6",
"sort": null,
"field_type": "text"
},
{
@ -48,6 +50,7 @@
"user_viewable": true,
"user_editable": false,
"rules": "required|alpha_dash|between:1,100",
"sort": null,
"field_type": "text"
},
{
@ -58,6 +61,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "required|string|alpha_dash",
"sort": null,
"field_type": "text"
},
{
@ -68,6 +72,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "nullable|string",
"sort": null,
"field_type": "text"
},
{
@ -78,6 +83,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "nullable|string",
"sort": null,
"field_type": "text"
},
{
@ -88,6 +94,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "nullable|string",
"sort": null,
"field_type": "text"
}
]

View File

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

View File

@ -4,9 +4,10 @@
"version": "PTDL_v2",
"update_url": null
},
"exported_at": "2024-05-08T12:01:57+02:00",
"exported_at": "2024-06-02T20:42:06+00:00",
"name": "Insurgency",
"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.",
"features": [
"steam_disk_space"
@ -38,6 +39,7 @@
"user_viewable": true,
"user_editable": false,
"rules": "required|regex:\/^(237410)$\/",
"sort": null,
"field_type": "text"
},
{
@ -48,6 +50,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "required|regex:\/^(\\w{1,20})$\/",
"sort": null,
"field_type": "text"
}
]

View File

@ -4,9 +4,10 @@
"version": "PTDL_v2",
"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",
"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.",
"features": [
"gsl_token",
@ -39,6 +40,7 @@
"user_viewable": true,
"user_editable": false,
"rules": "required|in:232250",
"sort": null,
"field_type": "text"
},
{
@ -49,6 +51,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "required|regex:\/^(\\w{1,20})$\/",
"sort": null,
"field_type": "text"
},
{
@ -59,6 +62,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "required|string|alpha_num|size:32",
"sort": null,
"field_type": "text"
}
]

View File

@ -4,9 +4,10 @@
"version": "PTDL_v2",
"update_url": null
},
"exported_at": "2024-05-21T21:40:24-04:00",
"exported_at": "2024-06-04T22:53:03+00:00",
"name": "Mumble Server",
"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.",
"features": null,
"docker_images": {
@ -15,7 +16,7 @@
"file_denylist": [],
"startup": "mumble-server -fg -ini murmur.ini",
"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}",
"logs": "{}",
"stop": "^C"
@ -36,7 +37,8 @@
"user_viewable": true,
"user_editable": false,
"rules": "required|numeric|digits_between:1,5",
"sort": null,
"field_type": "text"
}
]
}
}

View File

@ -4,9 +4,10 @@
"version": "PTDL_v2",
"update_url": null
},
"exported_at": "2024-05-08T11:53:32+02:00",
"exported_at": "2024-06-02T20:42:08+00:00",
"name": "Teamspeak3 Server",
"author": "panel@example.com",
"uuid": "983b1fac-d322-4d5f-a636-436127326b37",
"description": "VoIP software designed with security in mind, featuring crystal clear voice quality, endless customization options, and scalabilty up to thousands of simultaneous users.",
"features": null,
"docker_images": {
@ -36,6 +37,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "required|string|max:6",
"sort": null,
"field_type": "text"
},
{
@ -46,6 +48,7 @@
"user_viewable": true,
"user_editable": false,
"rules": "required|integer|between:1025,65535",
"sort": null,
"field_type": "text"
},
{
@ -56,6 +59,7 @@
"user_viewable": true,
"user_editable": false,
"rules": "required|integer|between:1025,65535",
"sort": null,
"field_type": "text"
},
{
@ -66,6 +70,7 @@
"user_viewable": true,
"user_editable": true,
"rules": "required|string|max:12",
"sort": null,
"field_type": "text"
},
{
@ -76,6 +81,7 @@
"user_viewable": true,
"user_editable": false,
"rules": "required|integer|between:1025,65535",
"sort": null,
"field_type": "text"
},
{
@ -86,6 +92,7 @@
"user_viewable": true,
"user_editable": false,
"rules": "required|integer|between:1025,65535",
"sort": null,
"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",
'daemonListen' => 'Enter the daemon listening port',
'daemonSFTP' => 'Enter the daemon SFTP listening port',
'daemonSFTPAlias' => 'Enter the daemon SFTP alias (can be empty)',
'daemonBase' => 'Enter the base folder',
'succes1' => 'Successfully created a new node with the name: ',
'succes2' => 'and has an id of: ',

View File

@ -19,6 +19,10 @@ return [
'button_issues' => 'Create Issue',
'button_features' => 'Discuss Features',
],
'intro-update' => [
'heading' => 'Update available',
'content' => ':latestVersion is available! Read our documentation to update your Panel.',
],
'intro-first-node' => [
'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!",

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:
* [Minecraft](https://github.com/pelican-eggs/minecraft)
* Paper
* Sponge
* Bungeecord
* Waterfall
* [SteamCMD](https://github.com/pelican-eggs/steamcmd)
* 7 Days to Die
* ARK: Survival
* ARMA
* Counter Strike
* DayZ
* Enshrouded
* Left 4 Dead
* Palworld
* Project Zomboid
* 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)
| Category | Eggs | | | |
|----------------------------------------------------------------------|-----------------|---------------|--------------------|----------------|
| [Minecraft](https://github.com/pelican-eggs/minecraft) | Paper | Sponge | Bungeecord | Waterfall |
| [SteamCMD](https://github.com/pelican-eggs/steamcmd) | 7 Days to Die | ARK: Survival | Arma 3 | Counter Strike |
| | DayZ | Enshrouded | Left 4 Dead | Palworld |
| | Project Zomboid | Satisfactory | Sons of the Forest | Starbound |
| [Standalone Games](https://github.com/pelican-eggs/games-standalone) | Among Us | Factorio | FTL | GTA |
| | Kerbal Space | Mindustry | Rimworld | Terraria |
| [Discord Bots](https://github.com/pelican-eggs/chatbots) | Redbot | JMusicBot | JMusicBot | Dynamica |
| [Voice Servers](https://github.com/pelican-eggs/voice) | Mumble | Teamspeak | Lavalink | |
| [Software](https://github.com/pelican-eggs/software) | Elasticsearch | Gitea | Grafana | RabbitMQ |
| [Programming](https://github.com/pelican-eggs/generic) | Node.js | Python | Java | C# |
| [Databases](https://github.com/pelican-eggs/database) | Redis | MariaDB | PostgreSQL | MongoDB |
| [Storage](https://github.com/pelican-eggs/storage) | S3 | SFTP Share | | |
| [Monitoring](https://github.com/pelican-eggs/monitoring) | Prometheus | Loki | | |
Copyright Pelican® 2024
*Copyright Pelican® 2024*

View File

@ -21,6 +21,7 @@ export interface Server {
status: ServerStatus;
sftpDetails: {
ip: string;
alias: string;
port: number;
};
invocation: string;
@ -57,6 +58,7 @@ export const rawDataToServerObject = ({ attributes: data }: FractalResponseData)
dockerImage: data.docker_image,
sftpDetails: {
ip: data.sftp_details.ip,
alias: data.sftp_details.alias,
port: data.sftp_details.port,
},
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'}
description={`
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
prefixing the path with an exclamation point.
`}

View File

@ -91,13 +91,14 @@ export default () => {
<FileManagerBreadcrumbs withinFileEditor isNewFile={action !== 'edit'} />
</div>
</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`}>
<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>{' '}
file. Any files or directories listed in here will be excluded from backups. Wildcards are
supported by using an 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 (
You&apos;re editing a{' '}
<code css={tw`font-mono bg-black rounded py-px px-1`}>.pelicanignore</code> directories listed
in here will be excluded from backups. Wildcards are supported by using an supported by using an
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>).
</p>
</div>

View File

@ -168,7 +168,7 @@ const TaskDetailsModal = ({ schedule, task }: Props) => {
<FormikFieldWrapper
name={'payload'}
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} />

View File

@ -31,8 +31,12 @@ export default () => {
<TitledGreyBox title={'SFTP Details'} css={tw`mb-6 md:mb-10`}>
<div>
<Label>Server Address</Label>
<CopyOnClick text={`sftp://${ip(sftp.ip)}:${sftp.port}`}>
<Input type={'text'} value={`sftp://${ip(sftp.ip)}:${sftp.port}`} readOnly />
<CopyOnClick text={`sftp://${sftp.alias ? sftp.alias : ip(sftp.ip)}:${sftp.port}`}>
<Input
type={'text'}
value={`sftp://${sftp.alias ? sftp.alias : ip(sftp.ip)}:${sftp.port}`}
readOnly
/>
</CopyOnClick>
</div>
<div css={tw`mt-6`}>
@ -50,7 +54,10 @@ export default () => {
</div>
</div>
<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>
</a>
</div>

View File

@ -4,7 +4,7 @@
:actions="$this->getCachedHeaderActions()"
:breadcrumbs="filament()->hasBreadcrumbs() ? $this->getBreadcrumbs() : []"
:heading=" trans('dashboard/index.heading')"
:subheading="trans('strings.version', ['version' => config('app.version')])"
:subheading="trans('strings.version', ['version' => $version])"
></x-filament-panels::header>
<p>{{ trans('dashboard/index.expand_sections') }}</p>
@ -30,6 +30,22 @@
</x-filament::section>
@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 --}}
@if ($nodesCount <= 0)
<x-filament::section