Merge branch 'issue/fix-3' of https://github.com/pelican-dev/panel into issue/fix-3

This commit is contained in:
notCharles 2024-04-21 14:09:35 -04:00
commit 693c65995d
182 changed files with 7002 additions and 1762 deletions

1
.github/FUNDING.yml vendored
View File

@ -1 +1,2 @@
github: pelican-dev
custom: [https://buy.stripe.com/14kdU99SI4UT7ni9AB, https://buy.stripe.com/14kaHXc0Q9b9372eUU]

View File

@ -4,6 +4,30 @@
# If using CentOS this file should be placed in:
# /etc/nginx/conf.d/
#
# The MIT License (MIT)
#
# Pterodactyl®
# Copyright © Dane Everitt <dane@daneeveritt.com> and contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
server {
listen 80;
server_name _;

View File

@ -1,9 +1,6 @@
name: Build
on:
push:
branches:
- '**'
pull_request:
branches:
- '**'

View File

@ -1,7 +1,7 @@
name: Tests
on:
push:
pull_request:
branches:
- '**'

30
.github/workflows/cla.yaml vendored Normal file
View File

@ -0,0 +1,30 @@
name: "CLA Assistant"
on:
issue_comment:
types: [created]
pull_request_target:
types: [opened,closed,synchronize]
permissions:
actions: write
contents: write
pull-requests: write
statuses: write
jobs:
CLAAssistant:
runs-on: ubuntu-latest
steps:
- name: "CLA Assistant"
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
uses: contributor-assistant/github-action@v2.3.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
with:
path-to-signatures: 'version1/cla.json'
path-to-document: 'https://github.com/pelican-dev/panel/blob/3.x/contributor_license_agreement.md'
branch: 'main'
allowlist: dependabot[bot]
remote-organization-name: pelican-dev
remote-repository-name: cla-signatures

View File

@ -1,9 +1,6 @@
name: Lint
on:
push:
branches:
- '**'
pull_request:
branches:
- '**'

View File

@ -44,7 +44,6 @@ class InfoCommand extends Command
['Session Driver', config('session.driver')],
['Filesystem Driver', config('filesystems.default')],
['Default Theme', config('themes.active')],
['Proxies', config('trustedproxies.proxies')],
], 'compact');
$this->output->title('Database Configuration');

View File

@ -0,0 +1,43 @@
<?php
namespace App\Enums;
enum ContainerStatus: string
{
case Created = 'created';
case Running = 'running';
case Restarting = 'restarting';
case Exited = 'exited';
case Paused = 'paused';
case Dead = 'dead';
case Removing = 'removing';
case Missing = 'missing';
public function icon(): string
{
return match ($this) {
self::Created => 'tabler-heart-plus',
self::Running => 'tabler-heartbeat',
self::Restarting => 'tabler-heart-bolt',
self::Exited => 'tabler-heart-exclamation',
self::Paused => 'tabler-heart-pause',
self::Dead => 'tabler-heart-x',
self::Removing => 'tabler-heart-down',
self::Missing => 'tabler-heart-question',
};
}
public function color(): string
{
return match ($this) {
self::Created => 'primary',
self::Running => 'success',
self::Restarting => 'info',
self::Exited => 'danger',
self::Paused => 'warning',
self::Dead => 'danger',
self::Removing => 'warning',
self::Missing => 'gray',
};
}
}

37
app/Enums/ServerState.php Normal file
View File

@ -0,0 +1,37 @@
<?php
namespace App\Enums;
enum ServerState: string
{
case None = 'none';
case Installing = 'installing';
case InstallFailed = 'install_failed';
case ReinstallFailed = 'reinstall_failed';
case Suspended = 'suspended';
case RestoringBackup = 'restoring_backup';
public function icon(): string
{
return match ($this) {
self::None => 'tabler-heart',
self::Installing => 'tabler-heart-bolt',
self::InstallFailed => 'tabler-heart-x',
self::ReinstallFailed => 'tabler-heart-x',
self::Suspended => 'tabler-heart-cancel',
self::RestoringBackup => 'tabler-heart-up',
};
}
public function color(): string
{
return match ($this) {
self::None => 'primary',
self::Installing => 'info',
self::InstallFailed => 'danger',
self::ReinstallFailed => 'danger',
self::Suspended => 'danger',
self::RestoringBackup => 'info',
};
}
}

View File

@ -2,6 +2,7 @@
namespace App\Exceptions\Http\Server;
use App\Enums\ServerState;
use App\Models\Server;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
@ -20,7 +21,7 @@ class ServerStateConflictException extends ConflictHttpException
$message = 'The node of this server is currently under maintenance and the functionality requested is unavailable.';
} elseif (!$server->isInstalled()) {
$message = 'This server has not yet completed its installation process, please try again later.';
} elseif ($server->status === Server::STATUS_RESTORING_BACKUP) {
} elseif ($server->status === ServerState::RestoringBackup) {
$message = 'This server is currently restoring from a backup, please try again later.';
} elseif (!is_null($server->transfer)) {
$message = 'This server is currently being transferred to a new machine, please try again later.';

View File

@ -1,5 +1,28 @@
<?php
/* The MIT License (MIT)
Pterodactyl®
Copyright © Dane Everitt <dane@daneeveritt.com> and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. */
namespace App\Extensions\Filesystem;
use Aws\S3\S3ClientInterface;

View File

@ -0,0 +1,82 @@
<?php
namespace App\Filament\Pages;
use App\Filament\Resources\NodeResource\Pages\ListNodes;
use App\Models\Egg;
use App\Models\Node;
use App\Models\Server;
use App\Models\User;
use Filament\Actions\CreateAction;
use Filament\Pages\Page;
class Dashboard extends Page
{
protected static ?string $navigationIcon = 'tabler-layout-dashboard';
protected static string $view = 'filament.pages.dashboard';
protected ?string $heading = '';
protected static ?string $title = 'Dashboard';
protected static ?string $slug = '/';
public string $activeTab = 'nodes';
public function getViewData(): array
{
return [
'inDevelopment' => config('app.version') === 'canary',
'eggsCount' => Egg::query()->count(),
'nodesList' => ListNodes::getUrl(),
'nodesCount' => Node::query()->count(),
'serversCount' => Server::query()->count(),
'usersCount' => User::query()->count(),
'devActions' => [
CreateAction::make()
->label('Create Issue')
->icon('tabler-brand-github')
->url('https://github.com/pelican-dev/panel/issues/new/choose', true)
->color('warning'),
CreateAction::make()
->label('Discuss Features')
->icon('tabler-brand-github')
->url('https://github.com/pelican-dev/panel/discussions', true)
->color('primary'),
],
'nodeActions' => [
CreateAction::make()
->label('Create first Node in Pelican')
->icon('tabler-server-2')
->url(route('filament.admin.resources.nodes.create'))
->color('primary'),
],
'supportActions' => [
CreateAction::make()
->label('Help Translate')
->icon('tabler-language')
->url('https://crowdin.com/project/pelican-dev', true)
->color('info'),
CreateAction::make()
->label('Donate Directly')
->icon('tabler-cash')
->url('https://pelican.dev/donate', true)
->color('success'),
],
'helpActions' => [
CreateAction::make()
->label('Read Documentation')
->icon('tabler-speedboat')
->url('https://pelican.dev/docs', true)
->color('info'),
CreateAction::make()
->label('Get Help in Discord')
->icon('tabler-brand-discord')
->url('https://discord.gg/pelican-panel', true)
->color('primary'),
],
];
}
}

View File

@ -0,0 +1,194 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\ApiKeyResource\Pages;
use App\Models\ApiKey;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Components\Tab;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
class ApiKeyResource extends Resource
{
protected static ?string $model = ApiKey::class;
protected static ?string $label = 'API Key';
protected static ?string $navigationIcon = 'tabler-key';
public static function canEdit($record): bool
{
return false;
}
public function getTabs(): array
{
return [
'all' => Tab::make('All Keys'),
'application' => Tab::make('Application Keys')
->modifyQueryUsing(fn (Builder $query) => $query->where('key_type', ApiKey::TYPE_APPLICATION)),
];
}
public function getDefaultActiveTab(): string|int|null
{
return 'application';
}
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Hidden::make('identifier')->default(ApiKey::generateTokenIdentifier(ApiKey::TYPE_APPLICATION)),
Forms\Components\Hidden::make('token')->default(encrypt(str_random(ApiKey::KEY_LENGTH))),
Forms\Components\Select::make('user_id')
->hidden()
->searchable()
->preload()
->relationship('user', 'username')
->default(auth()->user()->id)
->required(),
Forms\Components\Select::make('key_type')
->options(function (ApiKey $apiKey) {
$originalOptions = [
ApiKey::TYPE_NONE => 'None',
ApiKey::TYPE_ACCOUNT => 'Account',
ApiKey::TYPE_APPLICATION => 'Application',
ApiKey::TYPE_DAEMON_USER => 'Daemon User',
ApiKey::TYPE_DAEMON_APPLICATION => 'Daemon Application',
];
return collect($originalOptions)
->filter(fn ($value, $key) => $key <= ApiKey::TYPE_APPLICATION || $apiKey->key_type === $key)
->all();
})
->hidden()
->selectablePlaceholder(false)
->required()
->default(ApiKey::TYPE_APPLICATION),
Forms\Components\Fieldset::make('Permissions')
->columns([
'default' => 1,
'sm' => 1,
'md' => 2,
])
->schema(
collect(ApiKey::RESOURCES)->map(fn ($resource) => Forms\Components\ToggleButtons::make("r_$resource")
->label(str($resource)->replace('_', ' ')->title())
->options([
0 => 'None',
1 => 'Read',
// 2 => 'Write',
3 => 'Read & Write',
])
->icons([
0 => 'tabler-book-off',
1 => 'tabler-book',
2 => 'tabler-writing',
3 => 'tabler-writing',
])
->colors([
0 => 'success',
1 => 'warning',
2 => 'danger',
3 => 'danger',
])
->inline()
->required()
->disabledOn('edit')
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
])
->default(0),
)->all(),
),
Forms\Components\TagsInput::make('allowed_ips')
->placeholder('Example: 127.0.0.1 or 192.168.1.1')
->label('Whitelisted IPv4 Addresses')
->helperText('Press enter to add a new IP address or leave blank to allow any IP address')
->columnSpanFull()
->hidden()
->default(null),
Forms\Components\Textarea::make('memo')
->required()
->label('Description')
->helperText('
Once you have assigned permissions and created this set of credentials you will be unable to come back and edit it.
If you need to make changes down the road you will need to create a new set of credentials.
')
->columnSpanFull(),
]);
}
public static function table(Table $table): Table
{
return $table
->searchable(false)
->columns([
Tables\Columns\TextColumn::make('user.username')
->searchable()
->hidden()
->sortable(),
Tables\Columns\TextColumn::make('key')
->copyable()
->icon('tabler-clipboard-text')
->state(fn (ApiKey $key) => $key->identifier . decrypt($key->token)),
Tables\Columns\TextColumn::make('memo')
->label('Description')
->wrap()
->limit(50),
Tables\Columns\TextColumn::make('identifier')
->hidden()
->searchable(),
Tables\Columns\TextColumn::make('last_used_at')
->label('Last Used')
->dateTime()
->sortable(),
Tables\Columns\TextColumn::make('created_at')
->label('Created')
->dateTime()
->sortable(),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListApiKeys::route('/'),
'create' => Pages\CreateApiKey::route('/create'),
];
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Filament\Resources\ApiKeyResource\Pages;
use App\Filament\Resources\ApiKeyResource;
use Filament\Resources\Pages\CreateRecord;
class CreateApiKey extends CreateRecord
{
protected static string $resource = ApiKeyResource::class;
protected ?string $heading = 'Create Application API Key';
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Filament\Resources\ApiKeyResource\Pages;
use App\Filament\Resources\ApiKeyResource;
use App\Models\ApiKey;
use Filament\Actions;
use Filament\Resources\Components\Tab;
use Filament\Resources\Pages\ListRecords;
use Illuminate\Database\Eloquent\Builder;
class ListApiKeys extends ListRecords
{
protected static string $resource = ApiKeyResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
public function getTabs(): array
{
return [
'all' => Tab::make('All Keys'),
'application' => Tab::make('Application Keys')
->modifyQueryUsing(fn (Builder $query) => $query->where('key_type', ApiKey::TYPE_APPLICATION)
),
'account' => Tab::make('Account Keys')
->modifyQueryUsing(fn (Builder $query) => $query->where('key_type', ApiKey::TYPE_ACCOUNT)
),
];
}
public function getDefaultActiveTab(): string|int|null
{
return 'application';
}
}

View File

@ -0,0 +1,116 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\DatabaseHostResource\Pages;
use App\Models\DatabaseHost;
use Filament\Forms;
use Filament\Forms\Components\Section;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
class DatabaseHostResource extends Resource
{
protected static ?string $model = DatabaseHost::class;
protected static ?string $label = 'Databases';
protected static ?string $navigationIcon = 'tabler-database';
public static function form(Form $form): Form
{
return $form
->schema([
Section::make()->schema([
Forms\Components\TextInput::make('host')
->helperText('The IP address or Domain name that should be used when attempting to connect to this MySQL host from this Panel to create new databases.')
->required()
->live()
->debounce(500)
->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state))
->maxLength(191),
Forms\Components\TextInput::make('port')
->helperText('The port that MySQL is running on for this host.')
->required()
->numeric()
->default(3306)
->minValue(0)
->maxValue(65535),
Forms\Components\TextInput::make('username')
->helperText('The username of an account that has enough permissions to create new users and databases on the system.')
->required()
->maxLength(191),
Forms\Components\TextInput::make('password')
->helperText('The password for the database user.')
->password()
->revealable()
->maxLength(191)
->required(),
Forms\Components\TextInput::make('name')
->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.')
->required()
->maxLength(60),
Forms\Components\Select::make('node_id')
->searchable()
->preload()
->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.')
->label('Linked Node')
->relationship('node', 'name'),
])->columns([
'default' => 1,
'lg' => 2,
]),
]);
}
public static function table(Table $table): Table
{
return $table
->searchable(false)
->columns([
Tables\Columns\TextColumn::make('name')
->searchable(),
Tables\Columns\TextColumn::make('host')
->searchable(),
Tables\Columns\TextColumn::make('port')
->sortable(),
Tables\Columns\TextColumn::make('username')
->searchable(),
Tables\Columns\TextColumn::make('max_databases')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('node.name')
->numeric()
->sortable(),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListDatabaseHosts::route('/'),
'create' => Pages\CreateDatabaseHost::route('/create'),
'edit' => Pages\EditDatabaseHost::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace App\Filament\Resources\DatabaseHostResource\Pages;
use App\Filament\Resources\DatabaseHostResource;
use Filament\Resources\Pages\CreateRecord;
class CreateDatabaseHost extends CreateRecord
{
protected static string $resource = DatabaseHostResource::class;
protected ?string $heading = 'Database Hosts';
protected ?string $subheading = '(database servers that can have individual databases)';
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\DatabaseHostResource\Pages;
use App\Filament\Resources\DatabaseHostResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditDatabaseHost extends EditRecord
{
protected static string $resource = DatabaseHostResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\DatabaseHostResource\Pages;
use App\Filament\Resources\DatabaseHostResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListDatabaseHosts extends ListRecords
{
protected static string $resource = DatabaseHostResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@ -0,0 +1,110 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\DatabaseResource\Pages;
use App\Models\Database;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
class DatabaseResource extends Resource
{
protected static ?string $model = Database::class;
protected static ?string $navigationIcon = 'tabler-database';
protected static bool $shouldRegisterNavigation = false;
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Select::make('server_id')
->relationship('server', 'name')
->searchable()
->preload()
->required(),
Forms\Components\TextInput::make('database_host_id')
->required()
->numeric(),
Forms\Components\TextInput::make('database')
->required()
->maxLength(191),
Forms\Components\TextInput::make('remote')
->required()
->maxLength(191)
->default('%'),
Forms\Components\TextInput::make('username')
->required()
->maxLength(191),
Forms\Components\TextInput::make('password')
->password()
->revealable()
->required(),
Forms\Components\TextInput::make('max_connections')
->numeric()
->minValue(0)
->default(0),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('server.name')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('database_host_id')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('database')
->searchable(),
Tables\Columns\TextColumn::make('username')
->searchable(),
Tables\Columns\TextColumn::make('remote')
->searchable(),
Tables\Columns\TextColumn::make('max_connections')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListDatabases::route('/'),
'create' => Pages\CreateDatabase::route('/create'),
'edit' => Pages\EditDatabase::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\DatabaseResource\Pages;
use App\Filament\Resources\DatabaseResource;
use Filament\Resources\Pages\CreateRecord;
class CreateDatabase extends CreateRecord
{
protected static string $resource = DatabaseResource::class;
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\DatabaseResource\Pages;
use App\Filament\Resources\DatabaseResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditDatabase extends EditRecord
{
protected static string $resource = DatabaseResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\DatabaseResource\Pages;
use App\Filament\Resources\DatabaseResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListDatabases extends ListRecords
{
protected static string $resource = DatabaseResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@ -0,0 +1,195 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\EggResource\Pages;
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
use App\Models\Egg;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
class EggResource extends Resource
{
protected static ?string $model = Egg::class;
protected static ?string $navigationIcon = 'tabler-eggs';
protected static ?string $recordTitleAttribute = 'name';
protected static ?string $recordRouteKeyName = 'id';
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Tabs::make()->tabs([
Forms\Components\Tabs\Tab::make('Configuration')
->columns(2)
->schema([
Forms\Components\TextInput::make('name')->required()->maxLength(191)
->helperText('A simple, human-readable name to use as an identifier for this Egg.'),
Forms\Components\Textarea::make('description')->rows(5)
->helperText('A description of this Egg that will be displayed throughout the Panel as needed.'),
Forms\Components\TextInput::make('uuid')->disabled()
->helperText('This is the globally unique identifier for this Egg which the Daemon uses as an identifier.'),
Forms\Components\TextInput::make('author')->required()->maxLength(191)->disabled()
->helperText('The author of this version of the Egg. Uploading a new Egg configuration from a different author will change this.'),
Forms\Components\Toggle::make('force_outgoing_ip')->required()
->helperText("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's primary allocation IP.
Required for certain games to work properly when the Node has multiple public IP addresses.
Enabling this option will disable internal networking for any servers using this egg, causing them to be unable to internally access other servers on the same node."),
Forms\Components\Textarea::make('startup')->rows(5)
->helperText('The default startup command that should be used for new servers using this Egg.'),
Forms\Components\KeyValue::make('docker_images')
->columnSpanFull()
->addActionLabel('Add Image')
->keyLabel('Name')
->valueLabel('Image URI')
->helperText('The docker images available to servers using this egg.'),
]),
Forms\Components\Tabs\Tab::make('Process Management')
->columns(2)
->schema([
Forms\Components\Select::make('config_from')
->label('Copy Settings From')
->placeholder('None')
->relationship('configFrom', 'name', ignoreRecord: true)
->helperText('If you would like to default to settings from another Egg select it from the menu above.'),
Forms\Components\TextInput::make('config_stop')
->maxLength(191)
->label('Stop Command')
->helperText('The command that should be sent to server processes to stop them gracefully. If you need to send a SIGINT you should enter ^C here.'),
Forms\Components\Textarea::make('config_startup')->rows(10)->json()
->label('Start Configuration')
->helperText('List of values the daemon should be looking for when booting a server to determine completion.'),
Forms\Components\Textarea::make('config_files')->rows(10)->json()
->label('Configuration Files')
->helperText('This should be a JSON representation of configuration files to modify and what parts should be changed.'),
Forms\Components\Textarea::make('config_logs')->rows(10)->json()
->label('Log Configuration')
->helperText('This should be a JSON representation of where log files are stored, and whether or not the daemon should be creating custom logs.'),
]),
Forms\Components\Tabs\Tab::make('Variables')
->columnSpanFull()
// ->columns(2)
->schema([
Forms\Components\Repeater::make('Blah')
->grid(3)
->relationship('variables')
->name('name')
->columns(1)
->columnSpan(1)
->itemLabel(fn (array $state) => $state['name'])
->schema([
Forms\Components\TextInput::make('name')->live()->maxLength(191)->columnSpanFull(),
Forms\Components\Textarea::make('description')->columnSpanFull(),
Forms\Components\TextInput::make('env_variable')->maxLength(191),
Forms\Components\TextInput::make('default_value')->maxLength(191),
Forms\Components\Textarea::make('rules')->rows(3)->columnSpanFull()->required(),
]),
]),
Forms\Components\Tabs\Tab::make('Install Script')
->columns(3)
->schema([
Forms\Components\Select::make('copy_script_from')
->placeholder('None')
->relationship('scriptFrom', 'name', ignoreRecord: true),
Forms\Components\TextInput::make('script_container')
->required()
->maxLength(191)
->default('alpine:3.4'),
Forms\Components\TextInput::make('script_entry')
->required()
->maxLength(191)
->default('ash'),
MonacoEditor::make('script_install')
->columnSpanFull()
->fontSize('16px')
->language('shell')
->view('filament.plugins.monaco-editor'),
]),
])->columnSpanFull()->persistTabInQueryString(),
// Forms\Components\TagsInput::make('features'),
// Forms\Components\TagsInput::make('file_denylist')->placeholder('new-file.txt'),
// Forms\Components\TextInput::make('update_url'),
// Forms\Components\Toggle::make('script_is_privileged')->required(),
]);
}
public static function table(Table $table): Table
{
return $table
->defaultPaginationPageOption(25)
->columns([
Tables\Columns\TextColumn::make('id')
->label('Id')
->hidden()
->searchable(),
Tables\Columns\TextColumn::make('name')
->icon('tabler-egg')
->description(fn ($record): string => $record->description)
->wrap()
->searchable(),
Tables\Columns\TextColumn::make('author')
->hidden()
->searchable(),
Tables\Columns\TextColumn::make('servers_count')
->counts('servers')
->icon('tabler-server')
->label('Servers'),
Tables\Columns\TextColumn::make('script_container')
->searchable()
->hidden(),
Tables\Columns\TextColumn::make('copyFrom.name')
->hidden()
->sortable(),
Tables\Columns\TextColumn::make('script_entry')
->hidden()
->searchable(),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->headerActions([
//
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListEggs::route('/'),
'create' => Pages\CreateEgg::route('/create'),
'edit' => Pages\EditEgg::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\EggResource\Pages;
use App\Filament\Resources\EggResource;
use Filament\Resources\Pages\CreateRecord;
class CreateEgg extends CreateRecord
{
protected static string $resource = EggResource::class;
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\EggResource\Pages;
use App\Filament\Resources\EggResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditEgg extends EditRecord
{
protected static string $resource = EggResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace App\Filament\Resources\EggResource\Pages;
use App\Filament\Resources\EggResource;
use App\Services\Eggs\Sharing\EggImporterService;
use Exception;
use Filament\Actions;
use Filament\Forms;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\ListRecords;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
class ListEggs extends ListRecords
{
protected static string $resource = EggResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
Actions\Action::make('import')
->label('Import Egg')
->form([
Forms\Components\FileUpload::make('egg')
->acceptedFileTypes(['application/json'])
->storeFiles(false),
])
->action(function (array $data): void {
/** @var TemporaryUploadedFile $eggFile */
$eggFile = $data['egg'];
/** @var EggImporterService $eggImportService */
$eggImportService = resolve(EggImporterService::class);
try {
$newEgg = $eggImportService->handle($eggFile);
} catch (Exception $exception) {
Notification::make()
->title('Egg Import Failed')
->danger()
->send();
report($exception);
return;
}
Notification::make()
->title("Egg Import Success: $newEgg->name")
->success()
->send();
redirect()->route('filament.admin.resources.eggs.edit', [$newEgg]);
}),
];
}
}

View File

@ -0,0 +1,154 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\MountResource\Pages;
use App\Models\Mount;
use Filament\Forms;
use Filament\Forms\Components\Group;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
class MountResource extends Resource
{
protected static ?string $model = Mount::class;
protected static ?string $navigationIcon = 'tabler-layers-linked';
public static function form(Form $form): Form
{
return $form
->schema([
Section::make()->schema([
Forms\Components\TextInput::make('name')
->required()
->helperText('Unique name used to separate this mount from another.')
->maxLength(64),
Forms\Components\ToggleButtons::make('read_only')
->label('Read only?')
->helperText('Is the mount read only inside the container?')
->options([
false => 'Writeable',
true => 'Read only',
])
->icons([
false => 'tabler-writing',
true => 'tabler-writing-off',
])
->colors([
false => 'warning',
true => 'success',
])
->inline()
->default(false)
->required(),
Forms\Components\TextInput::make('source')
->required()
->helperText('File path on the host system to mount to a container.')
->maxLength(191),
Forms\Components\TextInput::make('target')
->required()
->helperText('Where the mount will be accessible inside a container.')
->maxLength(191),
Forms\Components\ToggleButtons::make('user_mountable')
->hidden()
->label('User mountable?')
->options([
false => 'No',
true => 'Yes',
])
->icons([
false => 'tabler-user-cancel',
true => 'tabler-user-bolt',
])
->colors([
false => 'success',
true => 'warning',
])
->default(false)
->inline()
->required(),
Forms\Components\Textarea::make('description')
->helperText('A longer description for this mount.')
->columnSpanFull(),
])->columnSpan(1)->columns([
'default' => 1,
'lg' => 2,
]),
Group::make()->schema([
Section::make()->schema([
Select::make('eggs')->multiple()
->relationship('eggs', 'name')
->preload(),
Select::make('nodes')->multiple()
->relationship('nodes', 'name')
->searchable(['name', 'fqdn'])
->preload(),
]),
])->columns([
'default' => 1,
'lg' => 2,
]),
])->columns([
'default' => 1,
'lg' => 2,
]);
}
public static function table(Table $table): Table
{
return $table
->searchable(false)
->columns([
Tables\Columns\TextColumn::make('id')
->label('')
->searchable(),
Tables\Columns\TextColumn::make('name')
->searchable(),
Tables\Columns\TextColumn::make('source')
->searchable(),
Tables\Columns\TextColumn::make('target')
->searchable(),
Tables\Columns\IconColumn::make('read_only')
->icon(fn (bool $state) => $state ? 'tabler-circle-check-filled' : 'tabler-circle-x-filled')
->color(fn (bool $state) => $state ? 'success' : 'danger')
->sortable(),
Tables\Columns\IconColumn::make('user_mountable')
->hidden()
->icon(fn (bool $state) => $state ? 'tabler-circle-check-filled' : 'tabler-circle-x-filled')
->color(fn (bool $state) => $state ? 'success' : 'danger')
->sortable(),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListMounts::route('/'),
'create' => Pages\CreateMount::route('/create'),
'edit' => Pages\EditMount::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\MountResource\Pages;
use App\Filament\Resources\MountResource;
use Filament\Resources\Pages\CreateRecord;
class CreateMount extends CreateRecord
{
protected static string $resource = MountResource::class;
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\MountResource\Pages;
use App\Filament\Resources\MountResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditMount extends EditRecord
{
protected static string $resource = MountResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\MountResource\Pages;
use App\Filament\Resources\MountResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListMounts extends ListRecords
{
protected static string $resource = MountResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@ -0,0 +1,163 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\NodeResource\Pages;
use App\Models\Node;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
class NodeResource extends Resource
{
protected static ?string $model = Node::class;
protected static ?string $navigationIcon = 'tabler-server-2';
protected static ?string $recordTitleAttribute = 'name';
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Toggle::make('behind_proxy')
->helperText('If you are running the daemon behind a proxy such as Cloudflare, select this to have the daemon skip looking for certificates on boot.')
->required(),
Forms\Components\TextInput::make('memory')
->required()
->numeric(),
Forms\Components\TextInput::make('memory_overallocate')
->required()
->numeric()
->default(0),
Forms\Components\TextInput::make('disk')
->required()
->numeric(),
Forms\Components\TextInput::make('disk_overallocate')
->required()
->numeric()
->default(0),
Forms\Components\TextInput::make('upload_size')
->required()
->integer()
->default(100),
Forms\Components\TextInput::make('daemonListen')
->required()
->integer()
->label('Daemon Port')
->default(8080),
Forms\Components\TextInput::make('daemonSFTP')
->required()
->integer()
->label('Daemon SFTP Port')
->default(2022),
Forms\Components\TextInput::make('daemonBase')
->required()
->maxLength(191)
->default('/home/daemon-files'),
Forms\Components\ToggleButtons::make('public')
->label('Node Visibility')
->inline()
->default(true)
->helperText('By setting a node to private you will be denying the ability to auto-deploy to this node.')
->options([
true => 'Public',
false => 'Private',
])
->colors([
true => 'warning',
false => 'danger',
])
->icons([
true => 'tabler-eye-check',
false => 'tabler-eye-cancel',
]),
]);
}
public static function table(Table $table): Table
{
return $table
->searchable(false)
->columns([
Tables\Columns\TextColumn::make('uuid')
->label('UUID')
->searchable()
->hidden(),
Tables\Columns\IconColumn::make('health')
->alignCenter()
->state(fn (Node $node) => $node)
->view('livewire.columns.version-column'),
Tables\Columns\TextColumn::make('name')
->icon('tabler-server-2')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('fqdn')
->visibleFrom('md')
->label('Address')
->icon('tabler-network')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('memory')
->visibleFrom('sm')
->icon('tabler-device-desktop-analytics')
->numeric()
->suffix(' GB')
->formatStateUsing(fn ($state) => number_format($state / 1000, 2))
->sortable(),
Tables\Columns\TextColumn::make('disk')
->visibleFrom('sm')
->icon('tabler-file')
->numeric()
->suffix(' GB')
->formatStateUsing(fn ($state) => number_format($state / 1000, 2))
->sortable(),
Tables\Columns\IconColumn::make('scheme')
->visibleFrom('xl')
->label('SSL')
->trueIcon('tabler-lock')
->falseIcon('tabler-lock-open-off')
->state(fn (Node $node) => $node->scheme === 'https'),
Tables\Columns\IconColumn::make('public')
->visibleFrom('lg')
->trueIcon('tabler-eye-check')
->falseIcon('tabler-eye-cancel'),
Tables\Columns\TextColumn::make('servers_count')
->visibleFrom('sm')
->counts('servers')
->label('Servers')
->sortable()
->icon('tabler-brand-docker'),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListNodes::route('/'),
'create' => Pages\CreateNode::route('/create'),
'edit' => Pages\EditNode::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,200 @@
<?php
namespace App\Filament\Resources\NodeResource\Pages;
use App\Filament\Resources\NodeResource;
use Filament\Forms;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Support\HtmlString;
class CreateNode extends CreateRecord
{
protected static string $resource = NodeResource::class;
protected static bool $canCreateAnother = false;
protected ?string $subheading = 'which is a machine that runs your Servers';
public function form(Forms\Form $form): Forms\Form
{
return $form
->columns([
'default' => 2,
'sm' => 3,
'md' => 3,
'lg' => 4,
])
->schema([
Forms\Components\TextInput::make('fqdn')
->columnSpan(2)
->required()
->autofocus()
->live(debounce: 1500)
->rule('prohibited', fn ($state) => is_ip($state) && request()->isSecure())
->label(fn ($state) => is_ip($state) ? 'IP Address' : 'Domain Name')
->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
->helperText(function ($state) {
if (is_ip($state)) {
if (request()->isSecure()) {
return '
Your panel is currently secured via an SSL certificate and that means your nodes require one too.
You must use a domain name, because you cannot get SSL certificates for IP Addresses
';
}
return '';
}
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) {
if (is_ip($state) && request()->isSecure()) {
return 'You cannot connect to an IP Address over SSL';
}
return '';
})
->afterStateUpdated(function (Forms\Set $set, ?string $state) {
$set('dns', null);
$set('ip', null);
[$subdomain] = str($state)->explode('.', 2);
if (!is_numeric($subdomain)) {
$set('name', $subdomain);
}
if (!$state || is_ip($state)) {
$set('dns', null);
return;
}
$validRecords = gethostbynamel($state);
if ($validRecords) {
$set('dns', true);
$set('ip', collect($validRecords)->first());
return;
}
$set('dns', false);
})
->maxLength(191),
Forms\Components\TextInput::make('ip')
->disabled()
->hidden(),
Forms\Components\ToggleButtons::make('dns')
->label('DNS Record Check')
->helperText('This lets you know if your DNS record correctly points to an IP Address.')
->disabled()
->inline()
->default(null)
->hint(fn (Forms\Get $get) => $get('ip'))
->hintColor('success')
->options([
true => 'Valid',
false => 'Invalid',
])
->colors([
true => 'success',
false => 'danger',
])
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 1,
]),
Forms\Components\TextInput::make('daemonListen')
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 1,
])
->label('Port')
->helperText('If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.')
->minValue(0)
->maxValue(65536)
->default(8080)
->required()
->integer(),
Forms\Components\TextInput::make('name')
->label('Display Name')
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 2,
])
->required()
->regex('/[a-zA-Z0-9_\.\- ]+/')
->helperText('This name is for display only and can be changed later.')
->maxLength(100),
Forms\Components\ToggleButtons::make('scheme')
->label('Communicate over SSL')
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 1,
])
->required()
->inline()
->helperText(function (Forms\Get $get) {
if (request()->isSecure()) {
return new HtmlString('Your Panel is using a secure SSL connection,<br>so your Daemon must too.');
}
if (is_ip($get('fqdn'))) {
return 'An IP address cannot use SSL.';
}
return '';
})
->disableOptionWhen(fn (string $value): bool => $value === 'http' && request()->isSecure())
->options([
'http' => 'HTTP',
'https' => 'HTTPS (SSL)',
])
->colors([
'http' => 'warning',
'https' => 'success',
])
->icons([
'http' => 'tabler-lock-open-off',
'https' => 'tabler-lock',
])
->default(fn () => request()->isSecure() ? 'https' : 'http'),
Forms\Components\Textarea::make('description')
->hidden()
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 2,
'lg' => 4,
])
->rows(5),
Forms\Components\Hidden::make('skipValidation')->default(true),
]);
}
protected function getRedirectUrlParameters(): array
{
return [
'tab' => '-configuration-tab',
];
}
}

View File

@ -0,0 +1,130 @@
<?php
namespace App\Filament\Resources\NodeResource\Pages;
use App\Filament\Resources\NodeResource;
use App\Models\Allocation;
use App\Models\Node;
use Filament\Actions;
use Filament\Forms;
use Filament\Forms\Components\Tabs;
use Filament\Resources\Pages\EditRecord;
use Illuminate\Support\HtmlString;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
class EditNode extends EditRecord
{
protected static string $resource = NodeResource::class;
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')
->icon('tabler-server')
->schema((new CreateNode())->form($form)->getComponents()),
Tabs\Tab::make('Advanced Settings')
->icon('tabler-server-cog'),
Tabs\Tab::make('Configuration')
->icon('tabler-code')
->schema([
Forms\Components\Placeholder::make('instructions')
->columnSpanFull()
->content(new HtmlString('
Save this file to your <span title="usually /etc/pelican/">daemon\'s root directory</span>, named <code>config.yml</code>
')),
Forms\Components\Textarea::make('config')
->label('/etc/pelican/config.yml')
->disabled()
->rows(19)
->hintAction(CopyAction::make())
->columnSpanFull(),
]),
Tabs\Tab::make('Allocations')
->icon('tabler-plug-connected')
->columns(4)
->schema([
Forms\Components\Repeater::make('allocations')
->orderColumn('server_id')
->columnSpan(1)
->columns(4)
->relationship()
->addActionLabel('Create New Allocation')
->addAction(fn ($action) => $action->color('info'))
->schema([
Forms\Components\TextInput::make('ip')
->label('IP Address'),
Forms\Components\TextInput::make('ip_alias')
->label('Alias'),
Forms\Components\TextInput::make('port')
->minValue(0)
->maxValue(65535)
->numeric(),
Forms\Components\TextInput::make('server')
->formatStateUsing(fn (Allocation $allocation) => $allocation->server?->name)
->readOnly()
->placeholder('No Server'),
]),
Forms\Components\Section::make('Assign New Allocations')
->columnSpan(2)
->inlineLabel()
->headerActions([
Forms\Components\Actions\Action::make('submit')
->color('success')
->action(function () {
// ...
}),
])
->schema([
Forms\Components\TextInput::make('ip')
->label('IP Address')
->placeholder('0.0.0.0')
->helperText('IP address to assign ports to')
->columnSpanFull(),
Forms\Components\TextInput::make('ip_alias')
->label('Alias')
->placeholder('minecraft')
->helperText('Display name to help you remember')
->columnSpanFull(),
Forms\Components\TextInput::make('ports')
->label('Ports')
->placeholder('25565')
->helperText('Individual ports or port ranges here separated by commas or spaces')
->columnSpanFull(),
]),
]),
]),
]);
}
protected function mutateFormDataBeforeFill(array $data): array
{
$node = Node::findOrFail($data['id']);
$data['config'] = $node->getYamlConfiguration();
return $data;
}
protected function getSteps(): array
{
return [
];
}
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\NodeResource\Pages;
use App\Filament\Resources\NodeResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListNodes extends ListRecords
{
protected static string $resource = NodeResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@ -0,0 +1,661 @@
<?php
namespace App\Filament\Resources;
use App\Enums\ContainerStatus;
use App\Enums\ServerState;
use App\Filament\Resources\ServerResource\Pages;
use App\Models\Allocation;
use App\Models\Egg;
use App\Models\Node;
use App\Models\Server;
use App\Repositories\Daemon\DaemonServerRepository;
use App\Services\Allocations\AssignmentService;
use Closure;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Validator;
class ServerResource extends Resource
{
protected static ?string $model = Server::class;
protected static ?string $navigationIcon = 'tabler-brand-docker';
protected static ?string $recordTitleAttribute = 'name';
public static function form(Form $form): Form
{
return $form
->columns([
'default' => 2,
'sm' => 2,
'md' => 4,
'lg' => 6,
])
->schema([
Forms\Components\ToggleButtons::make('docker')
->label('Container Status')
->hiddenOn('create')
->disableOptionWhen(fn ($state, $value) => $state !== $value)
->formatStateUsing(function ($state, Server $server) {
if ($server->node_id === null) {
return 'unknown';
}
/** @var DaemonServerRepository $service */
$service = resolve(DaemonServerRepository::class);
$details = $service->setServer($server)->getDetails();
return $details['state'] ?? 'unknown';
})
->options(collect(ContainerStatus::cases())->mapWithKeys(
fn (ContainerStatus $status) => [$status->value => str($status->value)->ucwords()]
))
->colors(collect(ContainerStatus::cases())->mapWithKeys(
fn (ContainerStatus $status) => [$status->value => $status->color()]
))
->icons(collect(ContainerStatus::cases())->mapWithKeys(
fn (ContainerStatus $status) => [$status->value => $status->icon()]
))
->grouped()
->columnSpanFull()
->inline(),
Forms\Components\ToggleButtons::make('status')
->label('Server State')
->helperText('')
->hiddenOn('create')
->disableOptionWhen(fn ($state, $value) => $state !== $value)
->formatStateUsing(fn ($state) => $state ?? 'none')
->options(collect(ServerState::cases())->mapWithKeys(
fn (ServerState $state) => [$state->value => str($state->value)->replace('_', ' ')->ucwords()]
))
->colors(collect(ServerState::cases())->mapWithKeys(
fn (ServerState $state) => [$state->value => $state->color()]
))
->icons(collect(ServerState::cases())->mapWithKeys(
fn (ServerState $state) => [$state->value => $state->icon()]
))
->grouped()
->columnSpanFull()
->inline(),
Forms\Components\TextInput::make('external_id')
->maxLength(191)
->hidden(),
Forms\Components\TextInput::make('name')
->prefixIcon('tabler-server')
->label('Display Name')
->suffixAction(Forms\Components\Actions\Action::make('random')
->icon('tabler-dice-' . random_int(1, 6))
->color('primary')
->action(function (Forms\Set $set, Forms\Get $get) {
$egg = Egg::find($get('egg_id'));
$prefix = $egg ? str($egg->name)->lower()->kebab() . '-' : '';
$set('name', $prefix . fake()->domainWord);
}))
->columnSpan([
'default' => 2,
'sm' => 4,
'md' => 2,
'lg' => 3,
])
->required()
->maxLength(191),
Forms\Components\Select::make('owner_id')
->prefixIcon('tabler-user')
->default(auth()->user()->id)
->label('Owner')
->columnSpan([
'default' => 2,
'sm' => 4,
'md' => 2,
'lg' => 3,
])
->relationship('user', 'username')
->searchable()
->preload()
->required(),
Forms\Components\Select::make('node_id')
->disabledOn('edit')
->prefixIcon('tabler-server-2')
->default(fn () => Node::query()->latest()->first()?->id)
->columnSpan(2)
->live()
->relationship('node', 'name')
->searchable()
->preload()
->afterStateUpdated(fn (Forms\Set $set) => $set('allocation_id', null))
->required(),
Forms\Components\Select::make('allocation_id')
->preload()
->live()
->prefixIcon('tabler-network')
->label('Primary Allocation')
->columnSpan(2)
->disabled(fn (Forms\Get $get) => $get('node_id') === null)
->searchable(['ip', 'port', 'ip_alias'])
->afterStateUpdated(function (Forms\Set $set) {
$set('allocation_additional', null);
$set('allocation_additional.needstobeastringhere.extra_allocations', null);
})
->getOptionLabelFromRecordUsing(
fn (Allocation $allocation) => "$allocation->ip:$allocation->port" .
($allocation->ip_alias ? " ($allocation->ip_alias)" : '')
)
->placeholder(function (Forms\Get $get) {
$node = Node::find($get('node_id'));
if ($node?->allocations) {
return 'Select an Allocation';
}
return 'Create a New Allocation';
})
->relationship(
'allocation',
'ip',
fn (Builder $query, Forms\Get $get) => $query
->where('node_id', $get('node_id'))
->whereNull('server_id'),
)
->createOptionForm(fn (Forms\Get $get) => [
Forms\Components\TextInput::make('allocation_ip')
->ipv4()
->datalist(function () use ($get) {
$node = Node::find($get('node_id'));
if (is_ip($node->fqdn)) {
return [$node->fqdn];
}
$validRecords = gethostbynamel($node->fqdn);
if (!$validRecords) {
return [];
}
return $validRecords ?: [];
})
->label('IP Address')
->helperText("Usually your machine's public IP unless you are port forwarding.")
->required(),
Forms\Components\TextInput::make('allocation_alias')
->label('Alias')
->default(null)
->datalist([
$get('name'),
Egg::find($get('egg_id'))?->name,
])
->helperText('This is just a display only name to help you recognize what this Allocation is used for.')
->required(false),
Forms\Components\TagsInput::make('allocation_ports')
->placeholder('Examples: 27015, 27017-27019')
->helperText('
These are the ports that users can connect to this Server through.
They usually consist of the port forwarded ones.
')
->label('Ports')
->required(),
])
->createOptionUsing(function (array $data, Forms\Get $get): int {
return collect(
resolve(AssignmentService::class)->handle(Node::find($get('node_id')), $data)
)->first();
})
->required(),
Forms\Components\Repeater::make('allocation_additional')
->label('Additional Allocations')
->columnSpan(2)
->addActionLabel('Add Allocation')
->disabled(fn (Forms\Get $get) => $get('allocation_id') === null)
// ->addable() TODO disable when all allocations are taken
// ->addable() TODO disable until first additional allocation is selected
->simple(
Forms\Components\Select::make('extra_allocations')
->live()
->preload()
->disableOptionsWhenSelectedInSiblingRepeaterItems()
->prefixIcon('tabler-network')
->label('Additional Allocations')
->columnSpan(2)
->disabled(fn (Forms\Get $get) => $get('../../node_id') === null)
->searchable(['ip', 'port', 'ip_alias'])
->getOptionLabelFromRecordUsing(
fn (Allocation $allocation) => "$allocation->ip:$allocation->port" .
($allocation->ip_alias ? " ($allocation->ip_alias)" : '')
)
->placeholder('Select additional Allocations')
->relationship(
'allocations',
'ip',
fn (Builder $query, Forms\Get $get, Forms\Components\Select $component, $state) => $query
->where('node_id', $get('../../node_id'))
->whereNotIn(
'id',
collect(($repeater = $component->getParentRepeater())->getState())
->pluck(
(string) str($component->getStatePath())
->after("{$repeater->getStatePath()}.")
->after('.'),
)
->flatten()
->diff(Arr::wrap($state))
->filter(fn (mixed $siblingItemState): bool => filled($siblingItemState))
->add($get('../../allocation_id'))
)
->whereNull('server_id'),
),
),
Forms\Components\Textarea::make('description')
->hidden()
->default('')
->required()
->columnSpanFull(),
Forms\Components\Select::make('egg_id')
->disabledOn('edit')
->prefixIcon('tabler-egg')
->columnSpan([
'default' => 2,
'sm' => 2,
'md' => 2,
'lg' => 6,
])
->relationship('egg', 'name')
->searchable()
->preload()
->live()
->afterStateUpdated(function ($state, Forms\Set $set) {
$egg = Egg::find($state);
$set('startup', $egg->startup);
$variables = $egg->variables ?? [];
$serverVariables = collect();
foreach ($variables as $variable) {
$serverVariables->add($variable->toArray());
}
$variables = [];
$set($path = 'server_variables', $serverVariables->all());
for ($i = 0; $i < $serverVariables->count(); $i++) {
$set("$path.$i.variable_value", $serverVariables[$i]['default_value']);
$set("$path.$i.variable_id", $serverVariables[$i]['id']);
$variables[$serverVariables[$i]['env_variable']] = $serverVariables[$i]['default_value'];
}
$set('environment', $variables);
})
->required(),
Forms\Components\ToggleButtons::make('skip_scripts')
->label('Run Egg Install Script?')
->default(false)
->options([
false => 'Yes',
true => 'Skip',
])
->colors([
false => 'primary',
true => 'danger',
])
->icons([
false => 'tabler-code',
true => 'tabler-code-off',
])
->inline()
->required(),
Forms\Components\ToggleButtons::make('custom_image')
->live()
->label('Custom Image?')
->default(false)
->formatStateUsing(function ($state, Forms\Get $get) {
if ($state !== null) {
return $state;
}
$images = Egg::find($get('egg_id'))->docker_images ?? [];
return !in_array($get('image'), $images);
})
->options([
false => 'No',
true => 'Yes',
])
->colors([
false => 'primary',
true => 'danger',
])
->icons([
false => 'tabler-settings-cancel',
true => 'tabler-settings-check',
])
->inline(),
Forms\Components\TextInput::make('image')
->hidden(fn (Forms\Get $get) => !$get('custom_image'))
->disabled(fn (Forms\Get $get) => !$get('custom_image'))
->label('Docker Image')
->placeholder('Enter a custom Image')
->columnSpan([
'default' => 2,
'sm' => 2,
'md' => 2,
'lg' => 4,
])
->required(),
Forms\Components\Select::make('image')
->hidden(fn (Forms\Get $get) => $get('custom_image'))
->disabled(fn (Forms\Get $get) => $get('custom_image'))
->label('Docker Image')
->prefixIcon('tabler-brand-docker')
->options(function (Forms\Get $get, Forms\Set $set) {
$images = Egg::find($get('egg_id'))->docker_images ?? [];
$set('image', collect($images)->first());
return $images;
})
->disabled(fn (Forms\Components\Select $component) => empty($component->getOptions()))
->selectablePlaceholder(false)
->columnSpan([
'default' => 2,
'sm' => 2,
'md' => 2,
'lg' => 4,
])
->required(),
Forms\Components\Fieldset::make('Application Feature Limits')
->inlineLabel()
->hiddenOn('create')
->columns(3)
->schema([
Forms\Components\TextInput::make('allocation_limit')
->suffixIcon('tabler-network')
->required()
->numeric()
->default(0),
Forms\Components\TextInput::make('database_limit')
->suffixIcon('tabler-database')
->required()
->numeric()
->default(0),
Forms\Components\TextInput::make('backup_limit')
->suffixIcon('tabler-copy-check')
->required()
->numeric()
->default(0),
]),
Forms\Components\Textarea::make('startup')
->hintIcon('tabler-code')
->label('Startup Command')
->required()
->live()
->columnSpan([
'default' => 2,
'sm' => 4,
'md' => 4,
'lg' => 6,
])
->rows(function ($state) {
return str($state)->explode("\n")->reduce(
fn (int $carry, $line) => $carry + floor(strlen($line) / 125),
0
);
}),
Forms\Components\Hidden::make('environment')->default([]),
Forms\Components\Hidden::make('start_on_completion')->default(true),
Forms\Components\Section::make('Egg Variables')
->icon('tabler-eggs')
->iconColor('primary')
->collapsible()
->collapsed()
->columnSpan(([
'default' => 2,
'sm' => 4,
'md' => 4,
'lg' => 6,
]))
->schema([
Forms\Components\Placeholder::make('Select an egg first to show its variables!')
->hidden(fn (Forms\Get $get) => !empty($get('server_variables'))),
Forms\Components\Repeater::make('server_variables')
->relationship('serverVariables')
->grid(2)
->reorderable(false)
->addable(false)
->deletable(false)
->default([])
->hidden(fn ($state) => empty($state))
->schema([
Forms\Components\TextInput::make('variable_value')
->rules([
fn (Forms\Get $get): Closure => function (string $attribute, $value, Closure $fail) use ($get) {
$validator = Validator::make(['validatorkey' => $value], [
'validatorkey' => $get('rules'),
]);
if ($validator->fails()) {
$message = str($validator->errors()->first())->replace('validatorkey', $get('name'));
$fail($message);
}
},
])
->label(fn (Forms\Get $get) => $get('name'))
//->hint('Rule')
->hintIcon('tabler-code')
->hintIconTooltip(fn (Forms\Get $get) => $get('rules'))
->prefix(fn (Forms\Get $get) => '{{' . $get('env_variable') . '}}')
->helperText(fn (Forms\Get $get) => empty($get('description')) ? '—' : $get('description'))
->maxLength(191),
Forms\Components\Hidden::make('variable_id')->default(0),
])
->columnSpan(2),
]),
Forms\Components\Section::make('Resource Management')
// ->hiddenOn('create')
->collapsed()
->icon('tabler-server-cog')
->iconColor('primary')
->columns(2)
->columnSpan(([
'default' => 2,
'sm' => 4,
'md' => 4,
'lg' => 6,
]))
->schema([
Forms\Components\TextInput::make('memory')
->default(0)
->label('Allocated Memory')
->suffix('MB')
->required()
->numeric(),
Forms\Components\TextInput::make('swap')
->default(0)
->label('Swap Memory')
->suffix('MB')
->helperText('0 disables swap and -1 allows unlimited swap')
->minValue(-1)
->required()
->numeric(),
Forms\Components\TextInput::make('disk')
->default(0)
->label('Disk Space Limit')
->suffix('MB')
->required()
->numeric(),
Forms\Components\TextInput::make('cpu')
->default(0)
->label('CPU Limit')
->suffix('%')
->required()
->numeric(),
Forms\Components\TextInput::make('threads')
->hint('Advanced')
->hintColor('danger')
->helperText('Examples: 0, 0-1,3, or 0,1,3,4')
->label('CPU Pinning')
->suffixIcon('tabler-cpu')
->maxLength(191),
Forms\Components\TextInput::make('io')
->helperText('The IO performance relative to other running containers')
->label('Block IO Proportion')
->required()
->minValue(0)
->maxValue(1000)
->step(10)
->default(0)
->numeric(),
Forms\Components\ToggleButtons::make('oom_disabled')
->label('OOM Killer')
->inline()
->default(false)
->options([
false => 'Disabled',
true => 'Enabled',
])
->colors([
false => 'success',
true => 'danger',
])
->icons([
false => 'tabler-sword-off',
true => 'tabler-sword',
])
->required(),
]),
]);
}
public static function table(Table $table): Table
{
return $table
->searchable(false)
->columns([
Tables\Columns\TextColumn::make('status')
->default('unknown')
->badge()
->default(function (Server $server) {
if ($server->status !== null) {
return $server->status;
}
$statuses = collect($server->retrieveStatus())
->mapWithKeys(function ($status) {
return [$status['configuration']['uuid'] => $status['state']];
})->all();
return $statuses[$server->uuid] ?? 'node_fail';
})
->icon(fn ($state) => match ($state) {
'node_fail' => 'tabler-server-off',
'running' => 'tabler-heartbeat',
'removing' => 'tabler-heart-x',
'offline' => 'tabler-heart-off',
'paused' => 'tabler-heart-pause',
'installing' => 'tabler-heart-bolt',
'suspended' => 'tabler-heart-cancel',
default => 'tabler-heart-question',
})
->color(fn (string $state): string => match ($state) {
'running' => 'success',
'installing', 'restarting' => 'primary',
'paused', 'removing' => 'warning',
'node_fail', 'install_failed', 'suspended' => 'danger',
default => 'gray',
}),
Tables\Columns\TextColumn::make('uuid')
->hidden()
->label('UUID')
->searchable(),
Tables\Columns\TextColumn::make('name')
->icon('tabler-brand-docker')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('node.name')
->icon('tabler-server-2')
->url(fn (Server $server): string => route('filament.admin.resources.nodes.edit', ['record' => $server->node]))
->sortable(),
Tables\Columns\TextColumn::make('egg.name')
->icon('tabler-egg')
->url(fn (Server $server): string => route('filament.admin.resources.eggs.edit', ['record' => $server->egg]))
->sortable(),
Tables\Columns\TextColumn::make('user.username')
->icon('tabler-user')
->label('Owner')
->url(fn (Server $server): string => route('filament.admin.resources.users.edit', ['record' => $server->user]))
->sortable(),
Tables\Columns\SelectColumn::make('allocation_id')
->label('Primary Allocation')
->options(fn ($state, Server $server) => $server->allocations->mapWithKeys(
fn ($allocation) => [$allocation->id => $allocation->address])
)
->selectablePlaceholder(false)
->sortable(),
Tables\Columns\TextColumn::make('image')->hidden(),
Tables\Columns\TextColumn::make('backups_count')
->counts('backups')
->label('Backups')
->icon('tabler-file-download')
->numeric()
->sortable(),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
// Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListServers::route('/'),
'create' => Pages\CreateServer::route('/create'),
'edit' => Pages\EditServer::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Filament\Resources\ServerResource\Pages;
use App\Filament\Resources\ServerResource;
use App\Services\Servers\ServerCreationService;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Database\Eloquent\Model;
class CreateServer extends CreateRecord
{
protected static string $resource = ServerResource::class;
protected static bool $canCreateAnother = false;
protected function handleRecordCreation(array $data): Model
{
$data['allocation_additional'] = collect($data['allocation_additional'])->filter()->all();
/** @var ServerCreationService $service */
$service = resolve(ServerCreationService::class);
$server = $service->handle($data);
return $server;
}
// protected function getRedirectUrl(): string
// {
// return $this->getResource()::getUrl('edit');
// }
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Filament\Resources\ServerResource\Pages;
use App\Filament\Resources\ServerResource;
use App\Models\Server;
use App\Services\Servers\ServerDeletionService;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditServer extends EditRecord
{
protected static string $resource = ServerResource::class;
protected function getHeaderActions(): array
{
return [
Actions\Action::make('Delete')
->successRedirectUrl($this->getResource()::getUrl('index'))
->color('danger')
->action(fn (Server $server) => resolve(ServerDeletionService::class)->handle($server))
->requiresConfirmation(),
];
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\ServerResource\Pages;
use App\Filament\Resources\ServerResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListServers extends ListRecords
{
protected static string $resource = ServerResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@ -0,0 +1,153 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource\RelationManagers;
use App\Models\User;
use Filament\Forms;
use Filament\Forms\Components\Section;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Support\Facades\Hash;
class UserResource extends Resource
{
protected static ?string $model = User::class;
protected static ?string $navigationIcon = 'tabler-users';
protected static ?string $recordTitleAttribute = 'username';
public static function form(Form $form): Form
{
return $form
->schema([
Section::make()->schema([
Forms\Components\TextInput::make('username')->required()->maxLength(191),
Forms\Components\TextInput::make('email')->email()->required()->maxLength(191),
Forms\Components\TextInput::make('name_first')
->maxLength(191)
->hidden(fn (string $operation): bool => $operation === 'create')
->label('First Name'),
Forms\Components\TextInput::make('name_last')
->maxLength(191)
->hidden(fn (string $operation): bool => $operation === 'create')
->label('Last Name'),
Forms\Components\TextInput::make('password')
->dehydrateStateUsing(fn (string $state): string => Hash::make($state))
->dehydrated(fn (?string $state): bool => filled($state))
->required(fn (string $operation): bool => $operation === 'create')
->password(),
Forms\Components\ToggleButtons::make('root_admin')
->label('Administrator (Root)')
->options([
false => 'No',
true => 'Admin',
])
->colors([
false => 'primary',
true => 'danger',
])
->disableOptionWhen(function (string $operation, $value, User $user) {
if ($operation !== 'edit' || $value) {
return false;
}
return $user->isLastRootAdmin();
})
->hint(fn (User $user) => $user->isLastRootAdmin() ? 'This is the last root administrator!' : '')
->helperText(fn (User $user) => $user->isLastRootAdmin() ? 'You must have at least one root administrator in your system.' : '')
->hintColor('warning')
->inline()
->required()
->default(false),
Forms\Components\Hidden::make('skipValidation')->default(true),
Forms\Components\Select::make('language')
->required()
->hidden()
->default('en')
->options(fn (User $user) => $user->getAvailableLanguages()),
])->columns(2),
]);
}
public static function table(Table $table): Table
{
return $table
->searchable(false)
->columns([
Tables\Columns\ImageColumn::make('picture')
->visibleFrom('lg')
->label('')
->extraImgAttributes(['class' => 'rounded-full'])
->defaultImageUrl(fn (User $user) => 'https://gravatar.com/avatar/' . md5(strtolower($user->email))),
Tables\Columns\TextColumn::make('external_id')
->searchable()
->hidden(),
Tables\Columns\TextColumn::make('uuid')
->label('UUID')
->hidden()
->searchable(),
Tables\Columns\TextColumn::make('username')
->searchable(),
Tables\Columns\TextColumn::make('email')
->searchable()
->icon('tabler-mail'),
Tables\Columns\IconColumn::make('root_admin')
->visibleFrom('md')
->label('Admin')
->boolean()
->trueIcon('tabler-star')
->falseIcon('tabler-star-off')
->sortable(),
Tables\Columns\IconColumn::make('use_totp')->label('2FA')
->visibleFrom('lg')
->icon(fn (User $user) => $user->use_totp ? 'tabler-lock' : 'tabler-lock-open-off')
->boolean()->sortable(),
Tables\Columns\TextColumn::make('servers_count')
->counts('servers')
->icon('tabler-server')
->label('Servers'),
Tables\Columns\TextColumn::make('subusers_count')
->visibleFrom('sm')
->counts('subusers')
->icon('tabler-users')
// ->formatStateUsing(fn (string $state, $record): string => (string) ($record->servers_count + $record->subusers_count))
->label('Subuser Accounts'),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
RelationManagers\ServersRelationManager::class,
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListUsers::route('/'),
'create' => Pages\CreateUser::route('/create'),
'edit' => Pages\EditUser::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource;
use Filament\Resources\Pages\CreateRecord;
class CreateUser extends CreateRecord
{
protected static string $resource = UserResource::class;
}

View File

@ -0,0 +1,183 @@
<?php
namespace App\Filament\Resources\UserResource\Pages;
use App\Models\ActivityLog;
use App\Models\User;
use App\Services\Users\TwoFactorSetupService;
use chillerlan\QRCode\Common\EccLevel;
use chillerlan\QRCode\Common\Version;
use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\QROptions;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Get;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\HtmlString;
use Illuminate\Validation\Rules\Password;
class EditProfile extends \Filament\Pages\Auth\EditProfile
{
protected function getForms(): array
{
return [
'form' => $this->form(
$this->makeForm()
->schema([
Tabs::make()->schema([
Tab::make('Account')
->icon('tabler-user')
->schema([
TextInput::make('username')
->disabled()
->readOnly()
->maxLength(191)
->unique(ignoreRecord: true)
->autofocus(),
TextInput::make('email')
->prefixIcon('tabler-mail')
->email()
->required()
->maxLength(191)
->unique(ignoreRecord: true),
TextInput::make('password')
->password()
->prefixIcon('tabler-password')
->revealable(filament()->arePasswordsRevealable())
->rule(Password::default())
->autocomplete('new-password')
->dehydrated(fn ($state): bool => filled($state))
->dehydrateStateUsing(fn ($state): string => Hash::make($state))
->live(debounce: 500)
->same('passwordConfirmation'),
TextInput::make('passwordConfirmation')
->password()
->prefixIcon('tabler-password-fingerprint')
->revealable(filament()->arePasswordsRevealable())
->required()
->visible(fn (Get $get): bool => filled($get('password')))
->dehydrated(false),
Select::make('language')
->required()
->prefixIcon('tabler-flag')
->live()
->default('en')
->helperText(fn (User $user, $state) => new HtmlString($user->isLanguageTranslated($state) ? '' : "
Your language ($state) has not been translated yet!
But never fear, you can help fix that by
<a style='color: rgb(56, 189, 248)' href='https://crowdin.com/project/pelican-dev'>contributing directly here</a>.
")
)
->options(fn (User $user) => $user->getAvailableLanguages()),
]),
Tab::make('2FA')
->icon('tabler-shield-lock')
->schema(function () {
if ($this->getUser()->use_totp) {
return [
Placeholder::make('2FA already enabled!'),
];
}
$setupService = app(TwoFactorSetupService::class);
['image_url_data' => $url] = $setupService->handle($this->getUser());
$options = new QROptions([
'svgLogo' => public_path('pelican.svg'),
'addLogoSpace' => true,
'logoSpaceWidth' => 13,
'logoSpaceHeight' => 13,
]);
// 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
$options->version = Version::AUTO;
// $options->outputInterface = QRSvgWithLogo::class;
$options->outputBase64 = false;
$options->eccLevel = EccLevel::H; // ECC level H is necessary when using logos
$options->addQuietzone = true;
// $options->drawLightModules = true;
$options->connectPaths = true;
$options->drawCircularModules = true;
// $options->circleRadius = 0.45;
$options->svgDefs = '<linearGradient id="gradient" x1="100%" y2="100%">
<stop stop-color="#7dd4fc" offset="0"/>
<stop stop-color="#38bdf8" offset="0.5"/>
<stop stop-color="#0369a1" offset="1"/>
</linearGradient>
<style><![CDATA[
.dark{fill: url(#gradient);}
.light{fill: #000;}
]]></style>';
$image = (new QRCode($options))->render($url);
return [
Placeholder::make('qr')
->label('Scan QR Code')
->content(fn () => new HtmlString("
<div style='width: 300px'>$image</div>
"))
->default('asdfasdf'),
];
}),
Tab::make('API Keys')
->icon('tabler-key')
->schema([
Placeholder::make('Coming soon!'),
TagsInput::make('allowed_ips')
->placeholder('Example: 127.0.0.1 or 192.168.1.1')
->label('Whitelisted IPv4 Addresses')
->helperText('Press enter to add a new IP address or leave blank to allow any IP address')
->columnSpanFull()
->hidden()
->default(null),
]),
Tab::make('SSH Keys')
->icon('tabler-lock-code')
->schema([
Placeholder::make('Coming soon!'),
]),
Tab::make('Activity')
->icon('tabler-history')
->schema([
Repeater::make('activity')
->deletable(false)
->addable(false)
->relationship()
->schema([
Placeholder::make('activity!')->label('')->content(fn (ActivityLog $log) => new HtmlString($log->htmlable())),
]),
]),
]),
])
->operation('edit')
->model($this->getUser())
->statePath('data')
->inlineLabel(!static::isSimple()),
),
];
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditUser extends EditRecord
{
protected static string $resource = UserResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListUsers extends ListRecords
{
protected static string $resource = UserResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace App\Filament\Resources\UserResource\RelationManagers;
use App\Models\Server;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Resources\RelationManagers\RelationManager;
class ServersRelationManager extends RelationManager
{
protected static string $relationship = 'servers';
public function table(Table $table): Table
{
return $table
->searchable(false)
->columns([
Tables\Columns\TextColumn::make('uuid')
->hidden()
->label('UUID')
->searchable(),
Tables\Columns\TextColumn::make('name')
->icon('tabler-brand-docker')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('node.name')
->icon('tabler-server-2')
->url(fn (Server $server): string => route('filament.admin.resources.nodes.edit', ['record' => $server->node]))
->sortable(),
Tables\Columns\TextColumn::make('egg.name')
->icon('tabler-egg')
->url(fn (Server $server): string => route('filament.admin.resources.eggs.edit', ['record' => $server->egg]))
->sortable(),
Tables\Columns\SelectColumn::make('allocation.id')
->label('Primary Allocation')
->options(fn ($state, Server $server) => [$server->allocation->id => $server->allocation->address])
->selectablePlaceholder(false)
->sortable(),
Tables\Columns\TextColumn::make('image')->hidden(),
Tables\Columns\TextColumn::make('databases_count')
->counts('databases')
->label('Databases')
->icon('tabler-database')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('backups_count')
->counts('backups')
->label('Backups')
->icon('tabler-file-download')
->numeric()
->sortable(),
]);
}
}

View File

@ -1,152 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Models\Egg;
use Ramsey\Uuid\Uuid;
use Illuminate\View\View;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Models\Mount;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\View\Factory as ViewFactory;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\MountFormRequest;
class MountController extends Controller
{
/**
* MountController constructor.
*/
public function __construct(
protected AlertsMessageBag $alert,
protected ViewFactory $view
) {
}
/**
* Return the mount overview page.
*/
public function index(): View
{
return view('admin.mounts.index', [
'mounts' => Mount::query()->withCount(['eggs', 'nodes'])->get(),
]);
}
/**
* Return the mount view page.
*/
public function view(string $id): View
{
return view('admin.mounts.view', [
'mount' => Mount::with(['eggs', 'nodes'])->findOrFail($id),
'eggs' => Egg::all(),
]);
}
/**
* Handle request to create new mount.
*
* @throws \Throwable
*/
public function create(MountFormRequest $request): RedirectResponse
{
$model = (new Mount())->fill($request->validated());
$model->forceFill(['uuid' => Uuid::uuid4()->toString()]);
$model->saveOrFail();
$mount = $model->fresh();
$this->alert->success('Mount was created successfully.')->flash();
return redirect()->route('admin.mounts.view', $mount->id);
}
/**
* Handle request to update or delete location.
*
* @throws \Throwable
*/
public function update(MountFormRequest $request, Mount $mount): RedirectResponse
{
if ($request->input('action') === 'delete') {
return $this->delete($mount);
}
$mount->forceFill($request->validated())->save();
$this->alert->success('Mount was updated successfully.')->flash();
return redirect()->route('admin.mounts.view', $mount->id);
}
/**
* Delete a location from the system.
*
* @throws \Exception
*/
public function delete(Mount $mount): RedirectResponse
{
$mount->delete();
return redirect()->route('admin.mounts');
}
/**
* Adds eggs to the mount's many-to-many relation.
*/
public function addEggs(Request $request, Mount $mount): RedirectResponse
{
$validatedData = $request->validate([
'eggs' => 'required|exists:eggs,id',
]);
$eggs = $validatedData['eggs'] ?? [];
if (count($eggs) > 0) {
$mount->eggs()->attach($eggs);
}
$this->alert->success('Mount was updated successfully.')->flash();
return redirect()->route('admin.mounts.view', $mount->id);
}
/**
* Adds nodes to the mount's many-to-many relation.
*/
public function addNodes(Request $request, Mount $mount): RedirectResponse
{
$data = $request->validate(['nodes' => 'required|exists:nodes,id']);
$nodes = $data['nodes'] ?? [];
if (count($nodes) > 0) {
$mount->nodes()->attach($nodes);
}
$this->alert->success('Mount was updated successfully.')->flash();
return redirect()->route('admin.mounts.view', $mount->id);
}
/**
* Deletes an egg from the mount's many-to-many relation.
*/
public function deleteEgg(Mount $mount, int $egg_id): Response
{
$mount->eggs()->detach($egg_id);
return response('', 204);
}
/**
* Deletes a node from the mount's many-to-many relation.
*/
public function deleteNode(Mount $mount, int $node_id): Response
{
$mount->nodes()->detach($node_id);
return response('', 204);
}
}

View File

@ -39,29 +39,8 @@ class NodeViewController extends Controller
->where('node_id', '=', $node->id)
->first();
$usageStats = Collection::make(['disk' => $stats->sum_disk, 'memory' => $stats->sum_memory])
->mapWithKeys(function ($value, $key) use ($node) {
$maxUsage = $node->{$key};
if ($node->{$key . '_overallocate'} > 0) {
$maxUsage = $node->{$key} * (1 + ($node->{$key . '_overallocate'} / 100));
}
$percent = ($value / $maxUsage) * 100;
return [
$key => [
'value' => number_format($value),
'max' => number_format($maxUsage),
'percent' => $percent,
'css' => ($percent <= self::THRESHOLD_PERCENTAGE_LOW) ? 'green' : (($percent > self::THRESHOLD_PERCENTAGE_MEDIUM) ? 'red' : 'yellow'),
],
];
})
->toArray();
return view('admin.nodes.view.index', [
'node' => $node,
'stats' => $usageStats,
'version' => $this->versionService,
]);
}

View File

@ -2,12 +2,12 @@
namespace App\Http\Controllers\Admin\Servers;
use App\Enums\ServerState;
use App\Models\DatabaseHost;
use App\Models\Egg;
use App\Models\Mount;
use App\Models\Node;
use Illuminate\View\View;
use Illuminate\Http\Request;
use App\Models\Server;
use App\Exceptions\DisplayException;
use App\Http\Controllers\Controller;
@ -22,14 +22,14 @@ class ServerViewController extends Controller
* ServerViewController constructor.
*/
public function __construct(
private EnvironmentService $environmentService,
private readonly EnvironmentService $environmentService,
) {
}
/**
* Returns the index view for a server.
*/
public function index(Request $request, Server $server): View
public function index(Server $server): View
{
return view('admin.servers.view.index', compact('server'));
}
@ -37,7 +37,7 @@ class ServerViewController extends Controller
/**
* Returns the server details page.
*/
public function details(Request $request, Server $server): View
public function details(Server $server): View
{
return view('admin.servers.view.details', compact('server'));
}
@ -45,7 +45,7 @@ class ServerViewController extends Controller
/**
* Returns a view of server build settings.
*/
public function build(Request $request, Server $server): View
public function build(Server $server): View
{
$allocations = $server->node->allocations->toBase();
@ -59,7 +59,7 @@ class ServerViewController extends Controller
/**
* Returns the server startup management page.
*/
public function startup(Request $request, Server $server): View
public function startup(Server $server): View
{
$variables = $this->environmentService->handle($server);
$eggs = Egg::all()->keyBy('id');
@ -76,7 +76,7 @@ class ServerViewController extends Controller
/**
* Returns all the databases that exist for the server.
*/
public function database(Request $request, Server $server): View
public function database(Server $server): View
{
return view('admin.servers.view.database', [
'hosts' => DatabaseHost::all(),
@ -87,7 +87,7 @@ class ServerViewController extends Controller
/**
* Returns all the mounts that exist for the server.
*/
public function mounts(Request $request, Server $server): View
public function mounts(Server $server): View
{
$server->load('mounts');
@ -108,9 +108,9 @@ class ServerViewController extends Controller
*
* @throws \App\Exceptions\DisplayException
*/
public function manage(Request $request, Server $server): View
public function manage(Server $server): View
{
if ($server->status === Server::STATUS_INSTALL_FAILED) {
if ($server->status === ServerState::InstallFailed) {
throw new DisplayException('This server is in a failed install state and cannot be recovered. Please delete and re-create the server.');
}
@ -135,7 +135,7 @@ class ServerViewController extends Controller
/**
* Returns the server deletion page.
*/
public function delete(Request $request, Server $server): View
public function delete(Server $server): View
{
return view('admin.servers.view.delete', compact('server'));
}

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Admin;
use App\Enums\ServerState;
use Illuminate\Http\Request;
use App\Models\User;
use Illuminate\Http\Response;
@ -71,11 +72,11 @@ class ServersController extends Controller
*/
public function toggleInstall(Server $server): RedirectResponse
{
if ($server->status === Server::STATUS_INSTALL_FAILED) {
if ($server->status === ServerState::InstallFailed) {
throw new DisplayException(trans('admin/server.exceptions.marked_as_failed'));
}
$server->status = $server->isInstalled() ? Server::STATUS_INSTALLING : null;
$server->status = $server->isInstalled() ? ServerState::Installing : null;
$server->save();
$this->alert->success(trans('admin/server.alerts.install_toggled'))->flash();

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Api\Client\Servers;
use App\Enums\ServerState;
use Illuminate\Http\Request;
use App\Models\Backup;
use App\Models\Server;
@ -212,7 +213,7 @@ class BackupController extends ClientApiController
// Update the status right away for the server so that we know not to allow certain
// actions against it via the Panel API.
$server->update(['status' => Server::STATUS_RESTORING_BACKUP]);
$server->update(['status' => ServerState::RestoringBackup]);
$this->daemonRepository->setServer($server)->restore($backup, $url ?? null, $request->input('truncate'));
});

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Api\Remote\Servers;
use App\Enums\ServerState;
use App\Models\Backup;
use Illuminate\Http\Request;
use App\Models\Server;
@ -81,7 +82,7 @@ class ServerDetailsController extends Controller
->latest('timestamp'),
])
->where('node_id', $node->id)
->where('status', Server::STATUS_RESTORING_BACKUP)
->where('status', ServerState::RestoringBackup)
->get();
$this->connection->transaction(function () use ($node, $servers) {
@ -108,7 +109,7 @@ class ServerDetailsController extends Controller
// Update any server marked as installing or restoring as being in a normal state
// at this point in the process.
Server::query()->where('node_id', $node->id)
->whereIn('status', [Server::STATUS_INSTALLING, Server::STATUS_RESTORING_BACKUP])
->whereIn('status', [ServerState::Installing, ServerState::RestoringBackup])
->update(['status' => null]);
});

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Api\Remote\Servers;
use App\Enums\ServerState;
use Illuminate\Http\Response;
use App\Models\Server;
use Illuminate\Http\JsonResponse;
@ -36,16 +37,16 @@ class ServerInstallController extends Controller
// Make sure the type of failure is accurate
if (!$request->boolean('successful')) {
$status = Server::STATUS_INSTALL_FAILED;
$status = ServerState::InstallFailed;
if ($request->boolean('reinstall')) {
$status = Server::STATUS_REINSTALL_FAILED;
$status = ServerState::ReinstallFailed;
}
}
// Keep the server suspended if it's already suspended
if ($server->status === Server::STATUS_SUSPENDED) {
$status = Server::STATUS_SUSPENDED;
if ($server->status === ServerState::Suspended) {
$status = ServerState::Suspended;
}
$previouslyInstalledAt = $server->installed_at;

View File

@ -1,23 +0,0 @@
<?php
namespace App\Http\Requests\Admin;
use App\Models\Mount;
class MountFormRequest extends AdminFormRequest
{
/**
* Set up the validation rules to use for these requests.
*/
public function rules(): array
{
if ($this->method() === 'PATCH') {
/** @var Mount $mount */
$mount = $this->route()->parameter('mount');
return Mount::getRulesForUpdate($mount->id);
}
return Mount::getRules();
}
}

View File

@ -2,7 +2,6 @@
namespace App\Http\Requests\Admin\Node;
use App\Rules\Fqdn;
use App\Models\Node;
use App\Http\Requests\Admin\AdminFormRequest;
@ -17,9 +16,6 @@ class NodeFormRequest extends AdminFormRequest
return Node::getRulesForUpdate($this->route()->parameter('node'));
}
$data = Node::getRules();
$data['fqdn'][] = Fqdn::make('scheme');
return $data;
return Node::getRules();
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Livewire;
use App\Models\Node;
use Livewire\Component;
class NodeSystemInformation extends Component
{
public Node $node;
public string $sizeClasses;
public function render()
{
return view('livewire.node-system-information');
}
public function placeholder()
{
return <<<'HTML'
<div>
<x-filament::icon
:icon="'tabler-heart-question'"
@class(['fi-ta-icon-item', $sizeClasses, 'fi-color-custom text-custom-500 dark:text-custom-400', 'fi-color-warning'])
@style([\Filament\Support\get_color_css_variables('warning', shades: [400, 500], alias: 'tables::columns.icon-column.item')])
/>
</div>
HTML;
}
}

View File

@ -144,4 +144,29 @@ class ActivityLog extends Model
Event::dispatch(new ActivityLogged($model));
});
}
public function htmlable()
{
$user = $this->actor;
if (!$user instanceof User) {
$user = new User([
'email' => 'system@pelican.dev',
'username' => 'system',
]);
}
$event = __('activity.'.str($this->event)->replace(':', '.'));
return "
<div style='display: flex; align-items: center;'>
<img width='50px' height='50px' src='{$user->getFilamentAvatarUrl()}' style='margin-right: 15px' />
<div>
<p>$user->username $this->event</p>
<p>$event</p>
<p>$this->ip <span title='{$this->timestamp->format('M j, Y g:ia')}'>{$this->timestamp->diffForHumans()}</span></p>
</div>
</div>
";
}
}

View File

@ -3,6 +3,7 @@
namespace App\Models;
use App\Exceptions\Service\Allocation\ServerUsingAllocationException;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
@ -111,9 +112,16 @@ class Allocation extends Model
return !is_null($this->ip_alias);
}
public function address(): Attribute
{
return Attribute::make(
get: fn () => "$this->ip:$this->port",
);
}
public function toString(): string
{
return sprintf('%s:%s', $this->ip, $this->port);
return $this->address;
}
/**

View File

@ -83,6 +83,8 @@ class ApiKey extends Model
*/
public const KEY_LENGTH = 32;
public const RESOURCES = ['servers', 'nodes', 'allocations', 'users', 'eggs', 'database_hosts', 'server_databases'];
/**
* The table associated with the model.
*/
@ -92,12 +94,21 @@ class ApiKey extends Model
* Fields that are mass assignable.
*/
protected $fillable = [
'user_id',
'key_type',
'identifier',
'token',
'allowed_ips',
'memo',
'last_used_at',
'expires_at',
'r_' . AdminAcl::RESOURCE_USERS,
'r_' . AdminAcl::RESOURCE_ALLOCATIONS,
'r_' . AdminAcl::RESOURCE_DATABASE_HOSTS,
'r_' . AdminAcl::RESOURCE_SERVER_DATABASES,
'r_' . AdminAcl::RESOURCE_EGGS,
'r_' . AdminAcl::RESOURCE_NODES,
'r_' . AdminAcl::RESOURCE_SERVERS,
];
/**
@ -187,7 +198,7 @@ class ApiKey extends Model
{
Assert::oneOf($type, [self::TYPE_ACCOUNT, self::TYPE_APPLICATION]);
return $type === self::TYPE_ACCOUNT ? 'ptlc_' : 'ptla_';
return $type === self::TYPE_ACCOUNT ? 'plcn_' : 'peli_';
}
/**

View File

@ -65,6 +65,11 @@ class DatabaseHost extends Model
];
}
public function getRouteKeyName(): string
{
return 'id';
}
/**
* Gets the node associated with a database host.
*/

View File

@ -152,6 +152,11 @@ class Egg extends Model
});
}
public function getRouteKeyName(): string
{
return 'id';
}
/**
* Returns the install script for the egg; if egg is copying from another
* it will return the copied script.

View File

@ -71,8 +71,8 @@ class Mount extends Model
* Blacklisted source paths.
*/
public static $invalidSourcePaths = [
'/etc/panel',
'/var/lib/panel/volumes',
'/etc/pelican',
'/var/lib/pelican/volumes',
'/srv/daemon-data',
];
@ -115,4 +115,9 @@ class Mount extends Model
{
return $this->belongsToMany(Server::class);
}
public function getRouteKeyName(): string
{
return 'id';
}
}

View File

@ -2,6 +2,10 @@
namespace App\Models;
use App\Exceptions\Service\HasActiveServersException;
use App\Repositories\Daemon\DaemonConfigurationRepository;
use Exception;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
use Illuminate\Notifications\Notifiable;
@ -79,9 +83,9 @@ class Node extends Model
'fqdn' => 'required|string',
'scheme' => 'required',
'behind_proxy' => 'boolean',
'memory' => 'required|numeric|min:1',
'memory' => 'required|numeric|min:0',
'memory_overallocate' => 'required|numeric|min:-1',
'disk' => 'required|numeric|min:1',
'disk' => 'required|numeric|min:0',
'disk_overallocate' => 'required|numeric|min:-1',
'daemon_base' => 'sometimes|required|regex:/^([\/][\d\w.\-\/]+)$/',
'daemon_sftp' => 'required|numeric|between:1,65535',
@ -96,7 +100,9 @@ class Node extends Model
protected $attributes = [
'public' => true,
'behind_proxy' => false,
'memory' => 0,
'memory_overallocate' => 0,
'disk' => 0,
'disk_overallocate' => 0,
'daemon_base' => '/var/lib/pelican/volumes',
'daemon_sftp' => 2022,
@ -117,6 +123,26 @@ class Node extends Model
];
}
public function getRouteKeyName(): string
{
return 'id';
}
protected static function booted(): void
{
static::creating(function (self $node) {
$node->uuid = Str::uuid();
$node->daemon_token = encrypt(Str::random(self::DAEMON_TOKEN_LENGTH));
$node->daemon_token_id = Str::random(self::DAEMON_TOKEN_ID_LENGTH);
return true;
});
static::deleting(function (self $node) {
throw_if($node->servers()->count(), new HasActiveServersException(trans('exceptions.egg.delete_has_servers')));
});
}
/**
* Get the connection address to use when making calls to this node.
*/
@ -240,4 +266,40 @@ class Node extends Model
];
})->values();
}
public function systemInformation(): array
{
return once(function () {
try {
return resolve(DaemonConfigurationRepository::class)
->setNode($this)
->getSystemInformation(connectTimeout: 3);
} catch (Exception $exception) {
$message = str($exception->getMessage());
if ($message->startsWith('cURL error 6: Could not resolve host')) {
$message = str('Could not resolve host');
}
if ($message->startsWith('cURL error 28: Failed to connect to ')) {
$message = $message->after('cURL error 28: ')->before(' after ');
}
return ['exception' => $message->toString()];
}
});
}
public function serverStatuses(): array
{
try {
/** @var \Illuminate\Http\Client\Response $response */
$response = Http::daemon($this)->connectTimeout(1)->timeout(1)->get('/api/servers');
$statuses = $response->json();
} catch (Exception) {
$statuses = [];
}
return cache()->remember("nodes.$this->id.servers", now()->addSeconds(2), fn () => $statuses);
}
}

View File

@ -2,6 +2,7 @@
namespace App\Models;
use App\Enums\ServerState;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Notifications\Notifiable;
@ -112,12 +113,6 @@ class Server extends Model
*/
public const RESOURCE_NAME = 'server';
public const STATUS_INSTALLING = 'installing';
public const STATUS_INSTALL_FAILED = 'install_failed';
public const STATUS_REINSTALL_FAILED = 'reinstall_failed';
public const STATUS_SUSPENDED = 'suspended';
public const STATUS_RESTORING_BACKUP = 'restoring_backup';
/**
* The table associated with the model.
*/
@ -128,7 +123,7 @@ class Server extends Model
* on server instances unless the user specifies otherwise in the request.
*/
protected $attributes = [
'status' => self::STATUS_INSTALLING,
'status' => ServerState::Installing,
'oom_disabled' => true,
'installed_at' => null,
];
@ -152,7 +147,7 @@ class Server extends Model
'status' => 'nullable|string',
'memory' => 'required|numeric|min:0',
'swap' => 'required|numeric|min:-1',
'io' => 'required|numeric|between:10,1000',
'io' => 'required|numeric|between:0,1000',
'cpu' => 'required|numeric|min:0',
'threads' => 'nullable|regex:/^[0-9-,]+$/',
'oom_disabled' => 'sometimes|boolean',
@ -171,6 +166,7 @@ class Server extends Model
{
return [
'node_id' => 'integer',
'status' => ServerState::class,
'skip_scripts' => 'boolean',
'owner_id' => 'integer',
'memory' => 'integer',
@ -203,12 +199,12 @@ class Server extends Model
public function isInstalled(): bool
{
return $this->status !== self::STATUS_INSTALLING && $this->status !== self::STATUS_INSTALL_FAILED;
return $this->status !== ServerState::Installing && $this->status !== ServerState::InstallFailed;
}
public function isSuspended(): bool
{
return $this->status === self::STATUS_SUSPENDED;
return $this->status === ServerState::Suspended;
}
/**
@ -251,6 +247,11 @@ class Server extends Model
return $this->hasOne(Egg::class, 'id', 'egg_id');
}
public function eggVariables(): HasMany
{
return $this->hasMany(EggVariable::class, 'egg_id', 'egg_id');
}
/**
* Gets information for the egg variables associated with this server.
*/
@ -267,6 +268,11 @@ class Server extends Model
});
}
public function serverVariables(): HasMany
{
return $this->hasMany(ServerVariable::class);
}
/**
* Gets information for the node associated with this server.
*/
@ -349,7 +355,7 @@ class Server extends Model
$this->isSuspended() ||
$this->node->isUnderMaintenance() ||
!$this->isInstalled() ||
$this->status === self::STATUS_RESTORING_BACKUP ||
$this->status === ServerState::RestoringBackup ||
!is_null($this->transfer)
) {
throw new ServerStateConflictException($this);
@ -366,7 +372,7 @@ class Server extends Model
{
if (
!$this->isInstalled() ||
$this->status === self::STATUS_RESTORING_BACKUP ||
$this->status === ServerState::RestoringBackup ||
!is_null($this->transfer)
) {
throw new ServerStateConflictException($this);
@ -388,4 +394,9 @@ class Server extends Model
throw new DaemonConnectionException($exception);
}
}
public function retrieveStatus()
{
return $this->node->serverStatuses();
}
}

View File

@ -5,7 +5,11 @@ namespace App\Models;
use App\Exceptions\DisplayException;
use App\Rules\Username;
use App\Facades\Activity;
use Illuminate\Support\Collection;
use Filament\Models\Contracts\FilamentUser;
use Filament\Models\Contracts\HasAvatar;
use Filament\Models\Contracts\HasName;
use Filament\Panel;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\In;
use Illuminate\Auth\Authenticatable;
use Illuminate\Notifications\Notifiable;
@ -79,7 +83,7 @@ use App\Notifications\SendPasswordReset as ResetPasswordNotification;
*
* @mixin \Eloquent
*/
class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract
class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract, FilamentUser, HasAvatar, HasName
{
use Authenticatable;
use Authorizable {can as protected canned; }
@ -139,18 +143,20 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
'language' => 'en',
'use_totp' => false,
'totp_secret' => null,
'name_first' => '',
'name_last' => '',
];
/**
* Rules verifying that the data being stored matches the expectations of the database.
*/
public static array $validationRules = [
'uuid' => 'required|string|size:36|unique:users,uuid',
'uuid' => 'nullable|string|size:36|unique:users,uuid',
'email' => 'required|email|between:1,191|unique:users,email',
'external_id' => 'sometimes|nullable|string|max:191|unique:users,external_id',
'username' => 'required|between:1,191|unique:users,username',
'name_first' => 'required|string|between:1,191',
'name_last' => 'required|string|between:1,191',
'name_first' => 'nullable|string|between:0,191',
'name_last' => 'nullable|string|between:0,191',
'password' => 'sometimes|nullable|string',
'root_admin' => 'boolean',
'language' => 'string',
@ -170,6 +176,12 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
protected static function booted(): void
{
static::creating(function (self $user) {
$user->uuid = Str::uuid();
return true;
});
static::deleting(function (self $user) {
throw_if($user->servers()->count() > 0, new DisplayException(__('admin/user.exceptions.user_has_servers')));
@ -177,6 +189,11 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
});
}
public function getRouteKeyName(): string
{
return 'id';
}
/**
* Implement language verification by overriding Eloquence's gather
* rules function.
@ -196,7 +213,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
*/
public function toVueObject(): array
{
return Collection::make($this->toArray())->except(['id', 'external_id'])->toArray();
return collect($this->toArray())->except(['id', 'external_id'])->toArray();
}
/**
@ -278,6 +295,11 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
->groupBy('servers.id');
}
public function subusers(): HasMany
{
return $this->hasMany(Subuser::class);
}
protected function checkPermission(Server $server, string $permission = ''): bool
{
if ($this->root_admin || $server->owner_id === $this->id) {
@ -313,4 +335,26 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
return $this->canned($abilities, $arguments);
}
public function isLastRootAdmin(): bool
{
$rootAdmins = User::query()->where('root_admin', true)->limit(2)->get();
return once(fn () => $rootAdmins->count() === 1 && $rootAdmins->first()->is($this));
}
public function canAccessPanel(Panel $panel): bool
{
return $this->root_admin;
}
public function getFilamentName(): string
{
return $this->name_first ?: $this->username;
}
public function getFilamentAvatarUrl(): ?string
{
return 'https://gravatar.com/avatar/' . md5(strtolower($this->email));
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Policies;
use App\Models\User;
class EggPolicy
{
public function create(User $user): bool
{
return false;
}
}

View File

@ -0,0 +1,75 @@
<?php
namespace App\Providers\Filament;
use App\Filament\Resources\UserResource\Pages\EditProfile;
use Filament\Http\Middleware\Authenticate;
use Filament\Http\Middleware\DisableBladeIconComponents;
use Filament\Http\Middleware\DispatchServingFilamentEvent;
use Filament\Panel;
use Filament\PanelProvider;
use Filament\Support\Colors\Color;
use Filament\Support\Facades\FilamentAsset;
use Filament\Widgets;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\AuthenticateSession;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;
class AdminPanelProvider extends PanelProvider
{
public function boot()
{
FilamentAsset::registerCssVariables([
'sidebar-width' => '12rem !important',
]);
}
public function panel(Panel $panel): Panel
{
return $panel
->default()
->id('admin')
->path('panel')
->login()
->brandName('Pelican')
->homeUrl('/')
->favicon('/pelican.ico')
->profile(EditProfile::class, false)
->colors([
'danger' => Color::Red,
'gray' => Color::Zinc,
'info' => Color::Blue,
'primary' => Color::Sky,
'success' => Color::Green,
'warning' => Color::Amber,
])
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
->pages([
// Pages\Dashboard::class,
])
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
->widgets([
Widgets\AccountWidget::class,
Widgets\FilamentInfoWidget::class,
])
->middleware([
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
AuthenticateSession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
])
->authMiddleware([
Authenticate::class,
]);
}
}

View File

@ -35,11 +35,9 @@ class DaemonBackupRepository extends DaemonRepository
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/backup', $this->server->uuid),
[
'json' => [
'adapter' => $this->adapter ?? config('backups.default'),
'uuid' => $backup->uuid,
'ignore' => implode("\n", $backup->ignored_files),
],
]
);
} catch (TransferException $exception) {
@ -60,11 +58,9 @@ class DaemonBackupRepository extends DaemonRepository
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/backup/%s/restore', $this->server->uuid, $backup->uuid),
[
'json' => [
'adapter' => $backup->disk,
'truncate_directory' => $truncate,
'download_url' => $url ?? '',
],
]
);
} catch (TransferException $exception) {

View File

@ -13,10 +13,13 @@ class DaemonConfigurationRepository extends DaemonRepository
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
*/
public function getSystemInformation(?int $version = null): array
public function getSystemInformation(?int $version = null, $connectTimeout = 5): array
{
try {
$response = $this->getHttpClient()->get('/api/system' . (!is_null($version) ? '?v=' . $version : ''));
$response = $this
->getHttpClient()
->connectTimeout($connectTimeout)
->get('/api/system' . (!is_null($version) ? '?v=' . $version : ''));
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
@ -36,7 +39,7 @@ class DaemonConfigurationRepository extends DaemonRepository
try {
return $this->getHttpClient()->post(
'/api/update',
['json' => $node->getConfiguration()]
$node->getConfiguration(),
);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);

View File

@ -103,10 +103,8 @@ class DaemonFileRepository extends DaemonRepository
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/files/create-directory', $this->server->uuid),
[
'json' => [
'name' => $name,
'path' => $path,
],
]
);
} catch (TransferException $exception) {
@ -151,9 +149,7 @@ class DaemonFileRepository extends DaemonRepository
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/files/copy', $this->server->uuid),
[
'json' => [
'location' => $location,
],
]
);
} catch (TransferException $exception) {
@ -174,10 +170,8 @@ class DaemonFileRepository extends DaemonRepository
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/files/delete', $this->server->uuid),
[
'json' => [
'root' => $root ?? '/',
'files' => $files,
],
]
);
} catch (TransferException $exception) {
@ -195,16 +189,15 @@ class DaemonFileRepository extends DaemonRepository
Assert::isInstanceOf($this->server, Server::class);
try {
$response = $this->getHttpClient()->post(
sprintf('/api/servers/%s/files/compress', $this->server->uuid),
[
'json' => [
'root' => $root ?? '/',
'files' => $files,
],
$response = $this->getHttpClient()
// Wait for up to 15 minutes for the archive to be completed when calling this endpoint
// since it will likely take quite awhile for large directories.
'timeout' => 60 * 15,
->timeout(60 * 15)
->post(
sprintf('/api/servers/%s/files/compress', $this->server->uuid),
[
'root' => $root ?? '/',
'files' => $files,
]
);
} catch (TransferException $exception) {
@ -224,16 +217,15 @@ class DaemonFileRepository extends DaemonRepository
Assert::isInstanceOf($this->server, Server::class);
try {
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/files/decompress', $this->server->uuid),
[
'json' => [
'root' => $root ?? '/',
'file' => $file,
],
return $this->getHttpClient()
// Wait for up to 15 minutes for the decompress to be completed when calling this endpoint
// since it will likely take quite awhile for large directories.
'timeout' => (int) CarbonInterval::minutes(15)->totalSeconds,
->timeout((int) CarbonInterval::minutes(15)->totalSeconds)
->post(
sprintf('/api/servers/%s/files/decompress', $this->server->uuid),
[
'root' => $root ?? '/',
'file' => $file,
]
);
} catch (TransferException $exception) {
@ -254,10 +246,8 @@ class DaemonFileRepository extends DaemonRepository
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/files/chmod', $this->server->uuid),
[
'json' => [
'root' => $root ?? '/',
'files' => $files,
],
]
);
} catch (TransferException $exception) {
@ -286,7 +276,7 @@ class DaemonFileRepository extends DaemonRepository
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/files/pull', $this->server->uuid),
[
'json' => array_filter($attributes, fn ($value) => !is_null($value)),
array_filter($attributes, fn ($value) => !is_null($value)),
]
);
} catch (TransferException $exception) {

View File

@ -21,7 +21,7 @@ class DaemonPowerRepository extends DaemonRepository
try {
return $this->getHttpClient()->post(
sprintf('/api/servers/%s/power', $this->server->uuid),
['json' => ['action' => $action]]
['action' => $action],
);
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);

View File

@ -40,11 +40,9 @@ class DaemonServerRepository extends DaemonRepository
Assert::isInstanceOf($this->server, Server::class);
try {
$this->getHttpClient()->post('/api/servers', [
'json' => [
$response = $this->getHttpClient()->post('/api/servers', [
'uuid' => $this->server->uuid,
'start_on_completion' => $startOnCompletion,
],
]);
} catch (GuzzleException $exception) {
throw new DaemonConnectionException($exception);

View File

@ -1,80 +0,0 @@
<?php
namespace App\Rules;
use Illuminate\Support\Arr;
use Illuminate\Contracts\Validation\Rule;
use Illuminate\Contracts\Validation\DataAwareRule;
class Fqdn implements DataAwareRule, Rule
{
protected array $data = [];
protected string $message = '';
protected ?string $schemeField = null;
/**
* @param array $data
*/
public function setData($data): self
{
$this->data = $data;
return $this;
}
/**
* Validates that the value provided resolves to an IP address. If a scheme is
* specified when this rule is created additional checks will be applied.
*
* @param string $attribute
* @param mixed $value
*/
public function passes($attribute, $value): bool
{
if (filter_var($value, FILTER_VALIDATE_IP)) {
// Check if the scheme is set to HTTPS.
//
// Unless someone owns their IP blocks and decides to pay who knows how much for a
// custom SSL cert, IPs will not be able to use HTTPS. This should prevent most
// home users from making this mistake and wondering why their node is not working.
if ($this->schemeField && Arr::get($this->data, $this->schemeField) === 'https') {
$this->message = 'The :attribute must not be an IP address when HTTPS is enabled.';
return false;
}
return true;
}
// Lookup A and AAAA DNS records for the FQDN. Note, this function will also resolve CNAMEs
// for us automatically, there is no need to manually resolve them here.
//
// The error suppression is intentional, see https://bugs.php.net/bug.php?id=73149
$records = @dns_get_record($value, DNS_A + DNS_AAAA);
// If no records were returned fall back to trying to resolve the value using the hosts DNS
// resolution. This will not work for IPv6 which is why we prefer to use `dns_get_record`
// first.
if (!empty($records) || filter_var(gethostbyname($value), FILTER_VALIDATE_IP)) {
return true;
}
$this->message = 'The :attribute could not be resolved to a valid IP address.';
return false;
}
public function message(): string
{
return $this->message;
}
/**
* Returns a new instance of the rule with a defined scheme set.
*/
public static function make(string $schemeField = null): self
{
return tap(new self(), function ($fqdn) use ($schemeField) {
$fqdn->schemeField = $schemeField;
});
}
}

View File

@ -37,7 +37,7 @@ class AssignmentService
* @throws \App\Exceptions\Service\Allocation\PortOutOfRangeException
* @throws \App\Exceptions\Service\Allocation\TooManyPortsInRangeException
*/
public function handle(Node $node, array $data): void
public function handle(Node $node, array $data): array
{
$explode = explode('/', $data['allocation_ip']);
if (count($explode) !== 1) {
@ -58,6 +58,8 @@ class AssignmentService
}
$this->connection->beginTransaction();
$ids = [];
foreach ($parsed as $ip) {
foreach ($data['allocation_ports'] as $port) {
if (!is_digit($port) && !preg_match(self::PORT_RANGE_REGEX, $port)) {
@ -99,10 +101,12 @@ class AssignmentService
];
}
Allocation::query()->insertOrIgnore($insertData);
$ids[] = Allocation::query()->insertOrIgnore($insertData);
}
}
$this->connection->commit();
return $ids;
}
}

View File

@ -18,17 +18,19 @@ class EggParserService
*/
public function handle(UploadedFile $file): array
{
if ($file->getError() !== UPLOAD_ERR_OK || !$file->isFile()) {
throw new InvalidFileUploadException('The selected file is not valid and cannot be imported.');
if ($file->getError() !== UPLOAD_ERR_OK) {
throw new InvalidFileUploadException('The selected file was not uploaded successfully');
}
/** @var array $parsed */
$parsed = json_decode($file->openFile()->fread($file->getSize()), true, 512, JSON_THROW_ON_ERROR);
if (!in_array(Arr::get($parsed, 'meta.version') ?? '', ['PTDL_v1', 'PTDL_v2'])) {
throw new InvalidFileUploadException('The JSON file provided is not in a format that can be recognized.');
}
$parsed = json_decode($file->getContent(), true, 512, JSON_THROW_ON_ERROR);
return $this->convertToV2($parsed);
$version = $parsed['meta']['version'] ?? '';
return 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.')
};
}
/**
@ -62,10 +64,6 @@ class EggParserService
*/
protected function convertToV2(array $parsed): array
{
if (Arr::get($parsed, 'meta.version') === Egg::EXPORT_VERSION) {
return $parsed;
}
// Maintain backwards compatability for eggs that are still using the old single image
// string format. New eggs can provide an array of Docker images that can be used.
if (!isset($parsed['images'])) {

View File

@ -2,6 +2,7 @@
namespace App\Services\Servers;
use App\Enums\ServerState;
use App\Models\Server;
use Illuminate\Database\ConnectionInterface;
use App\Repositories\Daemon\DaemonServerRepository;
@ -25,7 +26,7 @@ class ReinstallServerService
public function handle(Server $server): Server
{
return $this->connection->transaction(function () use ($server) {
$server->fill(['status' => Server::STATUS_INSTALLING])->save();
$server->fill(['status' => ServerState::Installing])->save();
$this->daemonServerRepository->setServer($server)->reinstall();

View File

@ -2,6 +2,7 @@
namespace App\Services\Servers;
use App\Enums\ServerState;
use App\Models\ServerVariable;
use Ramsey\Uuid\Uuid;
use Illuminate\Support\Arr;
@ -132,7 +133,7 @@ class ServerCreationService
'node_id' => Arr::get($data, 'node_id'),
'name' => Arr::get($data, 'name'),
'description' => Arr::get($data, 'description') ?? '',
'status' => Server::STATUS_INSTALLING,
'status' => ServerState::Installing,
'skip_scripts' => Arr::get($data, 'skip_scripts') ?? isset($data['skip_scripts']),
'owner_id' => Arr::get($data, 'owner_id'),
'memory' => Arr::get($data, 'memory'),

View File

@ -2,6 +2,7 @@
namespace App\Services\Servers;
use App\Enums\ServerState;
use Webmozart\Assert\Assert;
use App\Models\Server;
use App\Repositories\Daemon\DaemonServerRepository;
@ -44,7 +45,7 @@ class SuspensionService
// Update the server's suspension status.
$server->update([
'status' => $isSuspending ? Server::STATUS_SUSPENDED : null,
'status' => $isSuspending ? ServerState::Suspended : null,
]);
try {
@ -53,7 +54,7 @@ class SuspensionService
} catch (\Exception $exception) {
// Rollback the server's suspension status if daemon fails to sync the server.
$server->update([
'status' => $isSuspending ? null : Server::STATUS_SUSPENDED,
'status' => $isSuspending ? null : ServerState::Suspended,
]);
throw $exception;
}

View File

@ -9,6 +9,15 @@ trait AvailableLanguages
{
private ?Filesystem $filesystem = null;
public const TRANSLATED = [
'cz',
'da',
'de',
'en',
'es',
'tr',
];
/**
* Return all the available languages on the Panel based on those
* that are present in the language folder.
@ -18,12 +27,17 @@ trait AvailableLanguages
return collect($this->getFilesystemInstance()->directories(base_path('lang')))->mapWithKeys(function ($path) {
$code = basename($path);
$value = Locale::getDisplayName($code, app()->currentLocale());
$value = Locale::getDisplayName($code, $code);
return [$code => title_case($value)];
})->toArray();
}
public function isLanguageTranslated(string $countryCode = 'en'): bool
{
return in_array($countryCode, self::TRANSLATED, true);
}
/**
* Return an instance of the filesystem for getting a folder listing.
*/

View File

@ -11,6 +11,13 @@ if (!function_exists('is_digit')) {
}
}
if (!function_exists('is_ip')) {
function is_ip(?string $address): bool
{
return $address !== null && filter_var($address, FILTER_VALIDATE_IP) !== false;
}
}
if (!function_exists('object_get_strict')) {
/**
* Get an object using dot notation. An object key with a value of null is still considered valid

View File

@ -6,6 +6,7 @@ return [
App\Providers\BackupsServiceProvider::class,
App\Providers\BladeServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\Filament\AdminPanelProvider::class,
App\Providers\HashidsServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\ViewComposerServiceProvider::class,

View File

@ -9,8 +9,11 @@
"ext-pdo": "*",
"ext-pdo_mysql": "*",
"ext-zip": "*",
"abdelhamiderrahmouni/filament-monaco-editor": "^0.2.0",
"aws/aws-sdk-php": "~3.288.1",
"chillerlan/php-qrcode": "^5.0",
"doctrine/dbal": "~3.6.0",
"filament/filament": "^3.2",
"guzzlehttp/guzzle": "^7.5",
"hashids/hashids": "~5.0.0",
"laracasts/utilities": "~3.2.2",
@ -26,12 +29,14 @@
"pragmarx/google2fa": "~8.0.0",
"predis/predis": "~2.1.1",
"prologue/alerts": "^1.2",
"ryangjchandler/blade-tabler-icons": "^2.3",
"s1lentium/iptools": "~1.2.0",
"spatie/laravel-fractal": "^6.1",
"spatie/laravel-query-builder": "^5.8",
"symfony/mailgun-mailer": "^7.0",
"symfony/postmark-mailer": "^7.0",
"symfony/yaml": "^7.0",
"webbingbrasil/filament-copyactions": "^3.0",
"webmozart/assert": "~1.11.0"
},
"require-dev": {
@ -66,7 +71,8 @@
"cs:check": "php-cs-fixer fix --dry-run --diff --verbose",
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi || true"
"@php artisan package:discover --ansi || true",
"@php artisan filament:upgrade"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""

2352
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Egg Feature: EULA Popup
|--------------------------------------------------------------------------
|
| This popup is enabled for Minecraft eggs and allows a custom frontend
| hook to run that monitors the console output of the server and pops up
| a modal asking the user to accept it if necessary.
|
| There is no additional configuration necessary.
|
*/
];

View File

@ -0,0 +1,270 @@
<?php
return [
'general' => [
'enable-preview' => true,
'show-full-screen-toggle' => true,
'show-placeholder' => true,
'placeholder-text' => 'Your code here...',
'show-loader' => true,
'font-size' => '15px',
'line-numbers-min-chars' => true,
'automatic-layout' => true,
'default-theme' => 'iPlastic',
],
'themes' => [
'blackboard' => [
'base' => 'vs-dark',
'inherit' => true,
'rules' => [
[
'background' => '0C1021',
'token' => '',
],
[
'foreground' => 'aeaeae',
'token' => 'comment',
],
[
'foreground' => 'd8fa3c',
'token' => 'constant',
],
[
'foreground' => 'ff6400',
'token' => 'entity',
],
[
'foreground' => 'fbde2d',
'token' => 'keyword',
],
[
'foreground' => 'fbde2d',
'token' => 'storage',
],
[
'foreground' => '61ce3c',
'token' => 'string',
],
[
'foreground' => '61ce3c',
'token' => 'meta.verbatim',
],
[
'foreground' => '8da6ce',
'token' => 'support',
],
[
'foreground' => 'ab2a1d',
'fontStyle' => 'italic',
'token' => 'invalid.deprecated',
],
[
'foreground' => 'f8f8f8',
'background' => '9d1e15',
'token' => 'invalid.illegal',
],
[
'foreground' => 'ff6400',
'fontStyle' => 'italic',
'token' => 'entity.other.inherited-class',
],
[
'foreground' => 'ff6400',
'token' => 'string constant.other.placeholder',
],
[
'foreground' => 'becde6',
'token' => 'meta.function-call.py',
],
[
'foreground' => '7f90aa',
'token' => 'meta.tag',
],
[
'foreground' => '7f90aa',
'token' => 'meta.tag entity',
],
[
'foreground' => 'ffffff',
'token' => 'entity.name.section',
],
[
'foreground' => 'd5e0f3',
'token' => 'keyword.type.variant',
],
[
'foreground' => 'f8f8f8',
'token' => 'source.ocaml keyword.operator.symbol',
],
[
'foreground' => '8da6ce',
'token' => 'source.ocaml keyword.operator.symbol.infix',
],
[
'foreground' => '8da6ce',
'token' => 'source.ocaml keyword.operator.symbol.prefix',
],
[
'fontStyle' => 'underline',
'token' => 'source.ocaml keyword.operator.symbol.infix.floating-point',
],
[
'fontStyle' => 'underline',
'token' => 'source.ocaml keyword.operator.symbol.prefix.floating-point',
],
[
'fontStyle' => 'underline',
'token' => 'source.ocaml constant.numeric.floating-point',
],
[
'background' => 'ffffff08',
'token' => 'text.tex.latex meta.function.environment',
],
[
'background' => '7a96fa08',
'token' => 'text.tex.latex meta.function.environment meta.function.environment',
],
[
'foreground' => 'fbde2d',
'token' => 'text.tex.latex support.function',
],
[
'foreground' => 'ffffff',
'token' => 'source.plist string.unquoted',
],
[
'foreground' => 'ffffff',
'token' => 'source.plist keyword.operator',
],
],
'colors' => [
'editor.foreground' => '#F8F8F8',
'editor.background' => '#0C1021',
'editor.selectionBackground' => '#253B76',
'editor.lineHighlightBackground' => '#FFFFFF0F',
'editorCursor.foreground' => '#FFFFFFA6',
'editorWhitespace.foreground' => '#FFFFFF40',
],
],
'iPlastic' => [
'base' => 'vs',
'inherit' => true,
'rules' => [
[
'background' => 'EEEEEEEB',
'token' => '',
],
[
'foreground' => '009933',
'token' => 'string',
],
[
'foreground' => '0066ff',
'token' => 'constant.numeric',
],
[
'foreground' => 'ff0080',
'token' => 'string.regexp',
],
[
'foreground' => '0000ff',
'token' => 'keyword',
],
[
'foreground' => '9700cc',
'token' => 'constant.language',
],
[
'foreground' => '990000',
'token' => 'support.class.exception',
],
[
'foreground' => 'ff8000',
'token' => 'entity.name.function',
],
[
'fontStyle' => 'bold underline',
'token' => 'entity.name.type',
],
[
'fontStyle' => 'italic',
'token' => 'variable.parameter',
],
[
'foreground' => '0066ff',
'fontStyle' => 'italic',
'token' => 'comment',
],
[
'foreground' => 'ff0000',
'background' => 'e71a114d',
'token' => 'invalid',
],
[
'background' => 'e71a1100',
'token' => 'invalid.deprecated.trailing-whitespace',
],
[
'foreground' => '000000',
'background' => 'fafafafc',
'token' => 'text source',
],
[
'foreground' => '0033cc',
'token' => 'meta.tag',
],
[
'foreground' => '0033cc',
'token' => 'declaration.tag',
],
[
'foreground' => '6782d3',
'token' => 'constant',
],
[
'foreground' => '6782d3',
'token' => 'support.constant',
],
[
'foreground' => '3333ff',
'fontStyle' => 'bold',
'token' => 'support',
],
[
'fontStyle' => 'bold',
'token' => 'storage',
],
[
'fontStyle' => 'bold underline',
'token' => 'entity.name.section',
],
[
'foreground' => '000000',
'fontStyle' => 'bold',
'token' => 'entity.name.function.frame',
],
[
'foreground' => '333333',
'token' => 'meta.tag.preprocessor.xml',
],
[
'foreground' => '3366cc',
'fontStyle' => 'italic',
'token' => 'entity.other.attribute-name',
],
[
'fontStyle' => 'bold',
'token' => 'entity.name.tag',
],
],
'colors' => [
'editor.foreground' => '#000000',
'editor.background' => '#EEEEEEEB',
'editor.selectionBackground' => '#BAD6FD',
'editor.lineHighlightBackground' => '#0000001A',
'editorCursor.foreground' => '#000000',
'editorWhitespace.foreground' => '#B3B3B3F4',
],
],
],
];

View File

@ -24,7 +24,7 @@ return [
*/
'service' => [
'author' => env('APP_SERVICE_AUTHOR', 'unknown@unknown.com'),
'author' => env('APP_SERVICE_AUTHOR', 'unknown@example.com'),
],
/*

View File

@ -1,28 +0,0 @@
<?php
return [
/*
* Set trusted proxy IP addresses.
*
* Both IPv4 and IPv6 addresses are
* supported, along with CIDR notation.
*
* The "*" character is syntactic sugar
* within TrustedProxy to trust any proxy
* that connects directly to your server,
* a requirement when you cannot know the address
* of your proxy (e.g. if using Rackspace balancers).
*
* The "**" character is syntactic sugar within
* TrustedProxy to trust not just any proxy that
* connects directly to your server, but also
* proxies that connect to those proxies, and all
* the way back until you reach the original source
* IP. It will mean that $request->getClientIp()
* always gets the originating client IP, no matter
* how many proxies that client's request has
* subsequently passed through.
*/
'proxies' => in_array(env('TRUSTED_PROXIES', []), ['*', '**']) ?
env('TRUSTED_PROXIES') : explode(',', env('TRUSTED_PROXIES') ?? ''),
];

View File

@ -0,0 +1,96 @@
Thank you for your interest in Pelican ("Pelican Developers"). To clarify the intellectual property license
granted with Contributions from any person or entity, the Pelican Developers
must have on file a signed Contributor License Agreement ("CLA")
from each Contributor, indicating agreement with the license
terms below. This agreement is for your protection as a Contributor
as well as the protection of the Pelican Developers and its users. It does not
change your rights to use your own Contributions for any other purpose.
You accept and agree to the following terms and conditions for Your
Contributions (present and future) that you submit to the Pelican Developers. In
return, the Pelican Developers shall not use Your Contributions in a way that
is contrary to the public benefit or inconsistent with its nonprofit
status and bylaws in effect at the time of the Contribution. Except
for the license granted herein to the Pelican Developers and recipients of
software distributed by the Pelican Developers, You reserve all right, title,
and interest in and to Your Contributions.
1. Definitions.
"You" (or "Your") shall mean the copyright owner or legal entity
authorized by the copyright owner that is making this Agreement
with the Pelican Developers. For legal entities, the entity making a
Contribution and all other entities that control, are controlled
by, or are under common control with that entity are considered to
be a single Contributor. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"Contribution" shall mean any original work of authorship,
including any modifications or additions to an existing work, that
is intentionally submitted by You to the Pelican Developers for inclusion
in, or documentation of, any of the products owned or managed by
the Pelican Developers (the "Work"). For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written
communication sent to the Pelican Developers or its representatives,
including but not limited to communication on electronic mailing
lists, source code control systems, and issue tracking systems that
are managed by, or on behalf of, the Pelican Developers for the purpose of
discussing and improving the Work, but excluding communication that
is conspicuously marked or otherwise designated in writing by You
as "Not a Contribution."
2. Grant of Copyright License. Subject to the terms and conditions of
this Agreement, You hereby grant to the Pelican Developers and to
recipients of software distributed by the Pelican Developers a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare derivative works of,
publicly display, publicly perform, sublicense, and distribute Your
Contributions and such derivative works.
3. Grant of Patent License. Subject to the terms and conditions of
this Agreement, You hereby grant to the Pelican Developers and to
recipients of software distributed by the Pelican Developers a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the
Work, where such license applies only to those patent claims
licensable by You that are necessarily infringed by Your
Contribution(s) alone or by combination of Your Contribution(s)
with the Work to which such Contribution(s) was submitted. If any
entity institutes patent litigation against You or any other entity
(including a cross-claim or counterclaim in a lawsuit) alleging
that your Contribution, or the Work to which you have contributed,
constitutes direct or contributory patent infringement, then any
patent licenses granted to that entity under this Agreement for
that Contribution or Work shall terminate as of the date such
litigation is filed.
4. You represent that you are legally entitled to grant the above
license. If your employer(s) has rights to intellectual property
that you create that includes your Contributions, you represent
that you have received permission to make Contributions on behalf
of that employer, that your employer has waived such rights for
your Contributions to the Pelican Developers, or that your employer has
executed a separate Corporate CLA with the Pelican Developers.
5. You represent that each of Your Contributions is Your original
creation (see section 7 for submissions on behalf of others). You
represent that Your Contribution submissions include complete
details of any third-party license or other restriction (including,
but not limited to, related patents and trademarks) of which you
are personally aware and which are associated with any part of Your
Contributions.
6. You are not expected to provide support for Your Contributions,
except to the extent You desire to provide support. You may provide
support for free, for a fee, or not at all. Unless required by
applicable law or agreed to in writing, You provide Your
Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
OF ANY KIND, either express or implied, including, without
limitation, any warranties or conditions of TITLE, NON-
INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
7. Should You wish to submit work that is not Your original creation,
You may submit it to the Pelican Developers separately from any
Contribution, identifying the complete details of its source and of
any license or other restriction (including, but not limited to,
related patents, trademarks, and license agreements) of which you
are personally aware, and conspicuously marking the work as
"Submitted on behalf of a third-party: [named here]".
8. You agree to notify the Pelican Developers of any facts or circumstances of
which you become aware that would make these representations
inaccurate in any respect.

11
crowdin.yml Normal file
View File

@ -0,0 +1,11 @@
files:
- source: /lang/en/*.php
translation: /lang/%two_letters_code%/%original_file_name%
- source: /lang/en/admin
translation: /lang/%two_letters_code%/admin/%original_file_name%
- source: /lang/en/command
translation: /lang/%two_letters_code%/command/%original_file_name%
- source: /lang/en/dashboard
translation: /lang/%two_letters_code%/dashboard/%original_file_name%
- source: /lang/en/server
translation: /lang/%two_letters_code%/server/%original_file_name%

View File

@ -14,10 +14,10 @@
"pid_limit"
],
"docker_images": {
"Java 17": "ghcr.io\/App\/yolks:java_17",
"Java 16": "ghcr.io\/App\/yolks:java_16",
"Java 11": "ghcr.io\/App\/yolks:java_11",
"Java 8": "ghcr.io\/App\/yolks:java_8"
"Java 17": "ghcr.io\/pterodactyl\/yolks:java_17",
"Java 16": "ghcr.io\/pterodactyl\/yolks:java_16",
"Java 11": "ghcr.io\/pterodactyl\/yolks:java_11",
"Java 8": "ghcr.io\/pterodactyl\/yolks:java_8"
},
"file_denylist": [],
"startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}",
@ -30,7 +30,7 @@
"scripts": {
"installation": {
"script": "#!\/bin\/ash\r\n# Bungeecord Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n\r\ncd \/mnt\/server\r\n\r\nif [ -z \"${BUNGEE_VERSION}\" ] || [ \"${BUNGEE_VERSION}\" == \"latest\" ]; then\r\n BUNGEE_VERSION=\"lastStableBuild\"\r\nfi\r\n\r\ncurl -o ${SERVER_JARFILE} https:\/\/ci.md-5.net\/job\/BungeeCord\/${BUNGEE_VERSION}\/artifact\/bootstrap\/target\/BungeeCord.jar",
"container": "ghcr.io\/App\/installers:alpine",
"container": "ghcr.io\/pterodactyl\/installers:alpine",
"entrypoint": "ash"
}
},

View File

@ -14,10 +14,10 @@
"pid_limit"
],
"docker_images": {
"Java 17": "ghcr.io\/App\/yolks:java_17",
"Java 16": "ghcr.io\/App\/yolks:java_16",
"Java 11": "ghcr.io\/App\/yolks:java_11",
"Java 8": "ghcr.io\/App\/yolks:java_8"
"Java 17": "ghcr.io\/pterodactyl\/yolks:java_17",
"Java 16": "ghcr.io\/pterodactyl\/yolks:java_16",
"Java 11": "ghcr.io\/pterodactyl\/yolks:java_11",
"Java 8": "ghcr.io\/pterodactyl\/yolks:java_8"
},
"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\" )",

View File

@ -14,10 +14,10 @@
"pid_limit"
],
"docker_images": {
"Java 17": "ghcr.io\/App\/yolks:java_17",
"Java 16": "ghcr.io\/App\/yolks:java_16",
"Java 11": "ghcr.io\/App\/yolks:java_11",
"Java 8": "ghcr.io\/App\/yolks:java_8"
"Java 17": "ghcr.io\/pterodactyl\/yolks:java_17",
"Java 16": "ghcr.io\/pterodactyl\/yolks:java_16",
"Java 11": "ghcr.io\/pterodactyl\/yolks:java_11",
"Java 8": "ghcr.io\/pterodactyl\/yolks:java_8"
},
"file_denylist": [],
"startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -Dterminal.jline=false -Dterminal.ansi=true -jar {{SERVER_JARFILE}}",
@ -30,7 +30,7 @@
"scripts": {
"installation": {
"script": "#!\/bin\/ash\r\n# Paper Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\nPROJECT=paper\r\n\r\nif [ -n \"${DL_PATH}\" ]; then\r\n\techo -e \"Using supplied download url: ${DL_PATH}\"\r\n\tDOWNLOAD_URL=`eval echo $(echo ${DL_PATH} | sed -e 's\/{{\/${\/g' -e 's\/}}\/}\/g')`\r\nelse\r\n\tVER_EXISTS=`curl -s https:\/\/api.papermc.io\/v2\/projects\/${PROJECT} | jq -r --arg VERSION $MINECRAFT_VERSION '.versions[] | contains($VERSION)' | grep -m1 true`\r\n\tLATEST_VERSION=`curl -s https:\/\/api.papermc.io\/v2\/projects\/${PROJECT} | jq -r '.versions' | jq -r '.[-1]'`\r\n\r\n\tif [ \"${VER_EXISTS}\" == \"true\" ]; then\r\n\t\techo -e \"Version is valid. Using version ${MINECRAFT_VERSION}\"\r\n\telse\r\n\t\techo -e \"Specified version not found. Defaulting to the latest ${PROJECT} version\"\r\n\t\tMINECRAFT_VERSION=${LATEST_VERSION}\r\n\tfi\r\n\r\n\tBUILD_EXISTS=`curl -s https:\/\/api.papermc.io\/v2\/projects\/${PROJECT}\/versions\/${MINECRAFT_VERSION} | jq -r --arg BUILD ${BUILD_NUMBER} '.builds[] | tostring | contains($BUILD)' | grep -m1 true`\r\n\tLATEST_BUILD=`curl -s https:\/\/api.papermc.io\/v2\/projects\/${PROJECT}\/versions\/${MINECRAFT_VERSION} | jq -r '.builds' | jq -r '.[-1]'`\r\n\r\n\tif [ \"${BUILD_EXISTS}\" == \"true\" ]; then\r\n\t\techo -e \"Build is valid for version ${MINECRAFT_VERSION}. Using build ${BUILD_NUMBER}\"\r\n\telse\r\n\t\techo -e \"Using the latest ${PROJECT} build for version ${MINECRAFT_VERSION}\"\r\n\t\tBUILD_NUMBER=${LATEST_BUILD}\r\n\tfi\r\n\r\n\tJAR_NAME=${PROJECT}-${MINECRAFT_VERSION}-${BUILD_NUMBER}.jar\r\n\r\n\techo \"Version being downloaded\"\r\n\techo -e \"MC Version: ${MINECRAFT_VERSION}\"\r\n\techo -e \"Build: ${BUILD_NUMBER}\"\r\n\techo -e \"JAR Name of Build: ${JAR_NAME}\"\r\n\tDOWNLOAD_URL=https:\/\/api.papermc.io\/v2\/projects\/${PROJECT}\/versions\/${MINECRAFT_VERSION}\/builds\/${BUILD_NUMBER}\/downloads\/${JAR_NAME}\r\nfi\r\n\r\ncd \/mnt\/server\r\n\r\necho -e \"Running curl -o ${SERVER_JARFILE} ${DOWNLOAD_URL}\"\r\n\r\nif [ -f ${SERVER_JARFILE} ]; then\r\n\tmv ${SERVER_JARFILE} ${SERVER_JARFILE}.old\r\nfi\r\n\r\ncurl -o ${SERVER_JARFILE} ${DOWNLOAD_URL}\r\n\r\nif [ ! -f server.properties ]; then\r\n echo -e \"Downloading MC server.properties\"\r\n curl -o server.properties https:\/\/raw.githubusercontent.com\/parkervcp\/eggs\/master\/minecraft\/java\/server.properties\r\nfi",
"container": "ghcr.io\/App\/installers:alpine",
"container": "ghcr.io\/pterodactyl\/installers:alpine",
"entrypoint": "ash"
}
},

View File

@ -14,9 +14,9 @@
"pid_limit"
],
"docker_images": {
"Java 16": "ghcr.io\/App\/yolks:java_16",
"Java 11": "ghcr.io\/App\/yolks:java_11",
"Java 8": "ghcr.io\/App\/yolks:java_8"
"Java 16": "ghcr.io\/pterodactyl\/yolks:java_16",
"Java 11": "ghcr.io\/pterodactyl\/yolks:java_11",
"Java 8": "ghcr.io\/pterodactyl\/yolks:java_8"
},
"file_denylist": [],
"startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}",
@ -29,7 +29,7 @@
"scripts": {
"installation": {
"script": "#!\/bin\/ash\r\n# Sponge Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n\r\ncd \/mnt\/server\r\n\r\ncurl -sSL \"https:\/\/repo.spongepowered.org\/maven\/org\/spongepowered\/spongevanilla\/${SPONGE_VERSION}\/spongevanilla-${SPONGE_VERSION}.jar\" -o ${SERVER_JARFILE}",
"container": "ghcr.io\/App\/installers:alpine",
"container": "ghcr.io\/pterodactyl\/installers:alpine",
"entrypoint": "ash"
}
},

View File

@ -14,10 +14,10 @@
"pid_limit"
],
"docker_images": {
"Java 17": "ghcr.io\/App\/yolks:java_17",
"Java 16": "ghcr.io\/App\/yolks:java_16",
"Java 11": "ghcr.io\/App\/yolks:java_11",
"Java 8": "ghcr.io\/App\/yolks:java_8"
"Java 17": "ghcr.io\/pterodactyl\/yolks:java_17",
"Java 16": "ghcr.io\/pterodactyl\/yolks:java_16",
"Java 11": "ghcr.io\/pterodactyl\/yolks:java_11",
"Java 8": "ghcr.io\/pterodactyl\/yolks:java_8"
},
"file_denylist": [],
"startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}",
@ -30,7 +30,7 @@
"scripts": {
"installation": {
"script": "#!\/bin\/ash\r\n# Vanilla MC Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\nmkdir -p \/mnt\/server\r\ncd \/mnt\/server\r\n\r\nLATEST_VERSION=`curl https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq -r '.latest.release'`\r\nLATEST_SNAPSHOT_VERSION=`curl https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq -r '.latest.snapshot'`\r\n\r\necho -e \"latest version is $LATEST_VERSION\"\r\necho -e \"latest snapshot is $LATEST_SNAPSHOT_VERSION\"\r\n\r\nif [ -z \"$VANILLA_VERSION\" ] || [ \"$VANILLA_VERSION\" == \"latest\" ]; then\r\n MANIFEST_URL=$(curl -sSL https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq --arg VERSION $LATEST_VERSION -r '.versions | .[] | select(.id== $VERSION )|.url')\r\nelif [ \"$VANILLA_VERSION\" == \"snapshot\" ]; then\r\n MANIFEST_URL=$(curl -sSL https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq --arg VERSION $LATEST_SNAPSHOT_VERSION -r '.versions | .[] | select(.id== $VERSION )|.url')\r\nelse\r\n MANIFEST_URL=$(curl -sSL https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq --arg VERSION $VANILLA_VERSION -r '.versions | .[] | select(.id== $VERSION )|.url')\r\nfi\r\n\r\nDOWNLOAD_URL=$(curl ${MANIFEST_URL} | jq .downloads.server | jq -r '. | .url')\r\n\r\necho -e \"running: curl -o ${SERVER_JARFILE} $DOWNLOAD_URL\"\r\ncurl -o ${SERVER_JARFILE} $DOWNLOAD_URL\r\n\r\necho -e \"Install Complete\"",
"container": "ghcr.io\/App\/installers:alpine",
"container": "ghcr.io\/pterodactyl\/installers:alpine",
"entrypoint": "ash"
}
},

View File

@ -12,7 +12,7 @@
"steam_disk_space"
],
"docker_images": {
"ghcr.io\/App\/games:rust": "ghcr.io\/App\/games:rust"
"ghcr.io\/pterodactyl\/games:rust": "ghcr.io\/pterodactyl\/games:rust"
},
"file_denylist": [],
"startup": ".\/RustDedicated -batchmode +server.port {{SERVER_PORT}} +server.queryport {{QUERY_PORT}} +server.identity \"rust\" +rcon.port {{RCON_PORT}} +rcon.web true +server.hostname \\\"{{HOSTNAME}}\\\" +server.level \\\"{{LEVEL}}\\\" +server.description \\\"{{DESCRIPTION}}\\\" +server.url \\\"{{SERVER_URL}}\\\" +server.headerimage \\\"{{SERVER_IMG}}\\\" +server.logoimage \\\"{{SERVER_LOGO}}\\\" +server.maxplayers {{MAX_PLAYERS}} +rcon.password \\\"{{RCON_PASS}}\\\" +server.saveinterval {{SAVEINTERVAL}} +app.port {{APP_PORT}} $( [ -z ${MAP_URL} ] && printf %s \"+server.worldsize \\\"{{WORLD_SIZE}}\\\" +server.seed \\\"{{WORLD_SEED}}\\\"\" || printf %s \"+server.levelurl {{MAP_URL}}\" ) {{ADDITIONAL_ARGS}}",
@ -25,7 +25,7 @@
"scripts": {
"installation": {
"script": "#!\/bin\/bash\r\n# steamcmd Base Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n\r\nSRCDS_APPID=258550\r\n\r\n## just in case someone removed the defaults.\r\nif [ \"${STEAM_USER}\" == \"\" ]; then\r\n echo -e \"steam user is not set.\\n\"\r\n echo -e \"Using anonymous user.\\n\"\r\n STEAM_USER=anonymous\r\n STEAM_PASS=\"\"\r\n STEAM_AUTH=\"\"\r\nelse\r\n echo -e \"user set to ${STEAM_USER}\"\r\nfi\r\n\r\n## download and install steamcmd\r\ncd \/tmp\r\nmkdir -p \/mnt\/server\/steamcmd\r\ncurl -sSL -o steamcmd.tar.gz https:\/\/steamcdn-a.akamaihd.net\/client\/installer\/steamcmd_linux.tar.gz\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\nmkdir -p \/mnt\/server\/steamapps # Fix steamcmd disk write error when this folder is missing\r\ncd \/mnt\/server\/steamcmd\r\n\r\n# SteamCMD fails otherwise for some reason, even running as root.\r\n# This is changed at the end of the install process anyways.\r\nchown -R root:root \/mnt\r\nexport HOME=\/mnt\/server\r\n\r\n## install game using steamcmd\r\n.\/steamcmd.sh +force_install_dir \/mnt\/server +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} validate +quit ## other flags may be needed depending on install. looking at you cs 1.6\r\n\r\n## set up 32 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\n## set up 64 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk64\r\ncp -v linux64\/steamclient.so ..\/.steam\/sdk64\/steamclient.so",
"container": "ghcr.io\/App\/installers:debian",
"container": "ghcr.io\/pterodactyl\/installers:debian",
"entrypoint": "bash"
}
},

View File

@ -13,7 +13,7 @@
"steam_disk_space"
],
"images": [
"ghcr.io\/App\/games:source"
"ghcr.io\/pterodactyl\/games:source"
],
"file_denylist": [],
"startup": ".\/srcds_run -game csgo -console -port {{SERVER_PORT}} +ip 0.0.0.0 +map {{SRCDS_MAP}} -strictportbind -norestart +sv_setsteamaccount {{STEAM_ACC}}",
@ -26,7 +26,7 @@
"scripts": {
"installation": {
"script": "#!\/bin\/bash\r\n# steamcmd Base Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n\r\n## just in case someone removed the defaults.\r\nif [ \"${STEAM_USER}\" == \"\" ]; then\r\n STEAM_USER=anonymous\r\n STEAM_PASS=\"\"\r\n STEAM_AUTH=\"\"\r\nfi\r\n\r\n## download and install steamcmd\r\ncd \/tmp\r\nmkdir -p \/mnt\/server\/steamcmd\r\ncurl -sSL -o steamcmd.tar.gz https:\/\/steamcdn-a.akamaihd.net\/client\/installer\/steamcmd_linux.tar.gz\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\nmkdir -p \/mnt\/server\/steamapps # Fix steamcmd disk write error when this folder is missing\r\ncd \/mnt\/server\/steamcmd\r\n\r\n# SteamCMD fails otherwise for some reason, even running as root.\r\n# This is changed at the end of the install process anyways.\r\nchown -R root:root \/mnt\r\nexport HOME=\/mnt\/server\r\n\r\n## install game using steamcmd\r\n.\/steamcmd.sh +force_install_dir \/mnt\/server +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} +quit ## other flags may be needed depending on install. looking at you cs 1.6\r\n\r\n## set up 32 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\n## set up 64 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk64\r\ncp -v linux64\/steamclient.so ..\/.steam\/sdk64\/steamclient.so",
"container": "ghcr.io\/App\/installers:debian",
"container": "ghcr.io\/pterodactyl\/installers:debian",
"entrypoint": "bash"
}
},

View File

@ -12,7 +12,7 @@
"steam_disk_space"
],
"images": [
"ghcr.io\/App\/games:source"
"ghcr.io\/pterodactyl\/games:source"
],
"file_denylist": [],
"startup": ".\/srcds_run -game {{SRCDS_GAME}} -console -port {{SERVER_PORT}} +map {{SRCDS_MAP}} +ip 0.0.0.0 -strictportbind -norestart",
@ -25,7 +25,7 @@
"scripts": {
"installation": {
"script": "#!\/bin\/bash\r\n# steamcmd Base Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n\r\n##\r\n#\r\n# Variables\r\n# STEAM_USER, STEAM_PASS, STEAM_AUTH - Steam user setup. If a user has 2fa enabled it will most likely fail due to timeout. Leave blank for anon install.\r\n# WINDOWS_INSTALL - if it's a windows server you want to install set to 1\r\n# SRCDS_APPID - steam app id ffound here - https:\/\/developer.valvesoftware.com\/wiki\/Dedicated_Servers_List\r\n# EXTRA_FLAGS - when a server has extra glas for things like beta installs or updates.\r\n#\r\n##\r\n\r\n\r\n## just in case someone removed the defaults.\r\nif [ \"${STEAM_USER}\" == \"\" ]; then\r\n echo -e \"steam user is not set.\\n\"\r\n echo -e \"Using anonymous user.\\n\"\r\n STEAM_USER=anonymous\r\n STEAM_PASS=\"\"\r\n STEAM_AUTH=\"\"\r\nelse\r\n echo -e \"user set to ${STEAM_USER}\"\r\nfi\r\n\r\n## download and install steamcmd\r\ncd \/tmp\r\nmkdir -p \/mnt\/server\/steamcmd\r\ncurl -sSL -o steamcmd.tar.gz https:\/\/steamcdn-a.akamaihd.net\/client\/installer\/steamcmd_linux.tar.gz\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\nmkdir -p \/mnt\/server\/steamapps # Fix steamcmd disk write error when this folder is missing\r\ncd \/mnt\/server\/steamcmd\r\n\r\n# SteamCMD fails otherwise for some reason, even running as root.\r\n# This is changed at the end of the install process anyways.\r\nchown -R root:root \/mnt\r\nexport HOME=\/mnt\/server\r\n\r\n## install game using steamcmd\r\n.\/steamcmd.sh +force_install_dir \/mnt\/server +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} $( [[ \"${WINDOWS_INSTALL}\" == \"1\" ]] && printf %s '+@sSteamCmdForcePlatformType windows' ) +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} validate +quit ## other flags may be needed depending on install. looking at you cs 1.6\r\n\r\n## set up 32 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\n## set up 64 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk64\r\ncp -v linux64\/steamclient.so ..\/.steam\/sdk64\/steamclient.so",
"container": "ghcr.io\/App\/installers:debian",
"container": "ghcr.io\/pterodactyl\/installers:debian",
"entrypoint": "bash"
}
},

View File

@ -13,7 +13,7 @@
"steam_disk_space"
],
"images": [
"ghcr.io\/App\/games:source"
"ghcr.io\/pterodactyl\/games:source"
],
"file_denylist": [],
"startup": ".\/srcds_run -game garrysmod -console -port {{SERVER_PORT}} +ip 0.0.0.0 +host_workshop_collection {{WORKSHOP_ID}} +map {{SRCDS_MAP}} +gamemode {{GAMEMODE}} -strictportbind -norestart +sv_setsteamaccount {{STEAM_ACC}} +maxplayers {{MAX_PLAYERS}} -tickrate {{TICKRATE}} $( [ \"$LUA_REFRESH\" == \"1\" ] || printf %s '-disableluarefresh' )",
@ -26,7 +26,7 @@
"scripts": {
"installation": {
"script": "#!\/bin\/bash\r\n# steamcmd Base Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n\r\n## just in case someone removed the defaults.\r\nif [ \"${STEAM_USER}\" == \"\" ]; then\r\n echo -e \"steam user is not set.\\n\"\r\n echo -e \"Using anonymous user.\\n\"\r\n STEAM_USER=anonymous\r\n STEAM_PASS=\"\"\r\n STEAM_AUTH=\"\"\r\nelse\r\n echo -e \"user set to ${STEAM_USER}\"\r\nfi\r\n\r\n## download and install steamcmd\r\ncd \/tmp\r\nmkdir -p \/mnt\/server\/steamcmd\r\ncurl -sSL -o steamcmd.tar.gz https:\/\/steamcdn-a.akamaihd.net\/client\/installer\/steamcmd_linux.tar.gz\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\nmkdir -p \/mnt\/server\/steamapps # Fix steamcmd disk write error when this folder is missing\r\ncd \/mnt\/server\/steamcmd\r\n\r\n# SteamCMD fails otherwise for some reason, even running as root.\r\n# This is changed at the end of the install process anyways.\r\nchown -R root:root \/mnt\r\nexport HOME=\/mnt\/server\r\n\r\n## install game using steamcmd\r\n.\/steamcmd.sh +force_install_dir \/mnt\/server +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} $( [[ \"${WINDOWS_INSTALL}\" == \"1\" ]] && printf %s '+@sSteamCmdForcePlatformType windows' ) +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} validate +quit ## other flags may be needed depending on install. looking at you cs 1.6\r\n\r\n## set up 32 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\n## set up 64 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk64\r\ncp -v linux64\/steamclient.so ..\/.steam\/sdk64\/steamclient.so\r\n\r\n# Creating needed default files for the game\r\ncd \/mnt\/server\/garrysmod\/lua\/autorun\/server\r\necho '\r\n-- Docs: https:\/\/wiki.garrysmod.com\/page\/resource\/AddWorkshop\r\n-- Place the ID of the workshop addon you want to be downloaded to people who join your server, not the collection ID\r\n-- Use https:\/\/beta.configcreator.com\/create\/gmod\/resources.lua to easily create a list based on your collection ID\r\n\r\nresource.AddWorkshop( \"\" )\r\n' > workshop.lua\r\n\r\ncd \/mnt\/server\/garrysmod\/cfg\r\necho '\r\n\/\/ Please do not set RCon in here, use the startup parameters.\r\n\r\nhostname\t\t\"New Gmod Server\"\r\nsv_password\t\t\"\"\r\nsv_loadingurl \"\"\r\nsv_downloadurl \"\"\r\n\r\n\/\/ Steam Server List Settings\r\n\/\/ sv_location \"eu\"\r\nsv_region \"255\"\r\nsv_lan \"0\"\r\nsv_max_queries_sec_global \"30000\"\r\nsv_max_queries_window \"45\"\r\nsv_max_queries_sec \"5\"\r\n\r\n\/\/ Server Limits\r\nsbox_maxprops\t\t100\r\nsbox_maxragdolls\t5\r\nsbox_maxnpcs\t\t10\r\nsbox_maxballoons\t10\r\nsbox_maxeffects\t\t10\r\nsbox_maxdynamite\t10\r\nsbox_maxlamps\t\t10\r\nsbox_maxthrusters\t10\r\nsbox_maxwheels\t\t10\r\nsbox_maxhoverballs\t10\r\nsbox_maxvehicles\t20\r\nsbox_maxbuttons\t\t10\r\nsbox_maxsents\t\t20\r\nsbox_maxemitters\t5\r\nsbox_godmode\t\t0\r\nsbox_noclip\t\t 0\r\n\r\n\/\/ Network Settings - Please keep these set to default.\r\n\r\nsv_minrate\t\t75000\r\nsv_maxrate\t\t0\r\ngmod_physiterations\t2\r\nnet_splitpacket_maxrate\t45000\r\ndecalfrequency\t\t12 \r\n\r\n\/\/ Execute Ban Files - Please do not edit\r\nexec banned_ip.cfg \r\nexec banned_user.cfg \r\n\r\n\/\/ Add custom lines under here\r\n' > server.cfg",
"container": "ghcr.io\/App\/installers:debian",
"container": "ghcr.io\/pterodactyl\/installers:debian",
"entrypoint": "bash"
}
},

View File

@ -12,7 +12,7 @@
"steam_disk_space"
],
"images": [
"ghcr.io\/App\/games:source"
"ghcr.io\/pterodactyl\/games:source"
],
"file_denylist": [],
"startup": ".\/srcds_run -game insurgency -console -port {{SERVER_PORT}} +map {{SRCDS_MAP}} +ip 0.0.0.0 -strictportbind -norestart",
@ -25,7 +25,7 @@
"scripts": {
"installation": {
"script": "#!\/bin\/bash\r\n# steamcmd Base Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n\r\n## download and install steamcmd\r\ncd \/tmp\r\nmkdir -p \/mnt\/server\/steamcmd\r\ncurl -sSL -o steamcmd.tar.gz https:\/\/steamcdn-a.akamaihd.net\/client\/installer\/steamcmd_linux.tar.gz\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\ncd \/mnt\/server\/steamcmd\r\n\r\n# SteamCMD fails otherwise for some reason, even running as root.\r\n# This is changed at the end of the install process anyways.\r\nchown -R root:root \/mnt\r\nexport HOME=\/mnt\/server\r\n\r\n## install game using steamcmd\r\n.\/steamcmd.sh +force_install_dir \/mnt\/server +login anonymous +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} +quit\r\n\r\n## set up 32 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\n## set up 64 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk64\r\ncp -v linux64\/steamclient.so ..\/.steam\/sdk64\/steamclient.so",
"container": "ghcr.io\/App\/installers:debian",
"container": "ghcr.io\/pterodactyl\/installers:debian",
"entrypoint": "bash"
}
},

View File

@ -13,7 +13,7 @@
"steam_disk_space"
],
"images": [
"ghcr.io\/App\/games:source"
"ghcr.io\/pterodactyl\/games:source"
],
"file_denylist": [],
"startup": ".\/srcds_run -game tf -console -port {{SERVER_PORT}} +map {{SRCDS_MAP}} +ip 0.0.0.0 -strictportbind -norestart +sv_setsteamaccount {{STEAM_ACC}}",
@ -26,7 +26,7 @@
"scripts": {
"installation": {
"script": "#!\/bin\/bash\r\n# steamcmd Base Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n# Image to install with is 'debian:buster-slim'\r\n\r\n##\r\n#\r\n# Variables\r\n# STEAM_USER, STEAM_PASS, STEAM_AUTH - Steam user setup. If a user has 2fa enabled it will most likely fail due to timeout. Leave blank for anon install.\r\n# WINDOWS_INSTALL - if it's a windows server you want to install set to 1\r\n# SRCDS_APPID - steam app id ffound here - https:\/\/developer.valvesoftware.com\/wiki\/Dedicated_Servers_List\r\n# EXTRA_FLAGS - when a server has extra glas for things like beta installs or updates.\r\n#\r\n##\r\n\r\n## just in case someone removed the defaults.\r\nif [ \"${STEAM_USER}\" == \"\" ]; then\r\n echo -e \"steam user is not set.\\n\"\r\n echo -e \"Using anonymous user.\\n\"\r\n STEAM_USER=anonymous\r\n STEAM_PASS=\"\"\r\n STEAM_AUTH=\"\"\r\nelse\r\n echo -e \"user set to ${STEAM_USER}\"\r\nfi\r\n\r\n## download and install steamcmd\r\ncd \/tmp\r\nmkdir -p \/mnt\/server\/steamcmd\r\ncurl -sSL -o steamcmd.tar.gz https:\/\/steamcdn-a.akamaihd.net\/client\/installer\/steamcmd_linux.tar.gz\r\ntar -xzvf steamcmd.tar.gz -C \/mnt\/server\/steamcmd\r\nmkdir -p \/mnt\/server\/steamapps # Fix steamcmd disk write error when this folder is missing\r\ncd \/mnt\/server\/steamcmd\r\n\r\n# SteamCMD fails otherwise for some reason, even running as root.\r\n# This is changed at the end of the install process anyways.\r\nchown -R root:root \/mnt\r\nexport HOME=\/mnt\/server\r\n\r\n## install game using steamcmd\r\n.\/steamcmd.sh +force_install_dir \/mnt\/server +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} $( [[ \"${WINDOWS_INSTALL}\" == \"1\" ]] && printf %s '+@sSteamCmdForcePlatformType windows' ) +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} validate +quit ## other flags may be needed depending on install. looking at you cs 1.6\r\n\r\n## set up 32 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk32\r\ncp -v linux32\/steamclient.so ..\/.steam\/sdk32\/steamclient.so\r\n\r\n## set up 64 bit libraries\r\nmkdir -p \/mnt\/server\/.steam\/sdk64\r\ncp -v linux64\/steamclient.so ..\/.steam\/sdk64\/steamclient.so",
"container": "ghcr.io\/App\/installers:debian",
"container": "ghcr.io\/pterodactyl\/installers:debian",
"entrypoint": "bash"
}
},

View File

@ -23,7 +23,7 @@
"scripts": {
"installation": {
"script": "#!\/bin\/ash\r\n\r\nif [ ! -d \/mnt\/server\/ ]; then\r\n mkdir \/mnt\/server\/\r\nfi\r\n\r\ncd \/mnt\/server\r\n\r\nFILE=\/mnt\/server\/murmur.ini\r\nif [ -f \"$FILE\" ]; then\r\n echo \"Config file already exists.\"\r\nelse \r\n echo \"Downloading the config file.\"\r\n apk add --no-cache murmur\r\n cp \/etc\/murmur.ini \/mnt\/server\/murmur.ini\r\n apk del murmur\r\nfi\r\necho \"done\"",
"container": "ghcr.io\/App\/installers:alpine",
"container": "ghcr.io\/pterodactyl\/installers:alpine",
"entrypoint": "ash"
}
},

Some files were not shown because too many files have changed in this diff Show More