Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot]
a0a0ece531 ci(release): bump version 2024-05-12 02:12:00 +00:00
215 changed files with 2767 additions and 4934 deletions

View File

@@ -13,10 +13,17 @@ LOG_LEVEL=debug
DB_CONNECTION=sqlite
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
CACHE_STORE=file
QUEUE_CONNECTION=database
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
HASHIDS_SALT=
HASHIDS_LENGTH=8
MAIL_MAILER=log
MAIL_HOST=smtp.example.com
MAIL_PORT=25
@@ -30,8 +37,3 @@ MAIL_FROM_NAME="Pelican Admin"
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
# Set this to true, and set start & end ports to auto create allocations.
PANEL_CLIENT_ALLOCATIONS_ENABLED=false
PANEL_CLIENT_ALLOCATIONS_RANGE_START=
PANEL_CLIENT_ALLOCATIONS_RANGE_END=

2
.github/FUNDING.yml vendored
View File

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

View File

@@ -34,6 +34,7 @@ jobs:
MAIL_MAILER: array
SESSION_DRIVER: array
QUEUE_CONNECTION: sync
HASHIDS_SALT: alittlebitofsalt1234
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
DB_DATABASE: testing
@@ -59,7 +60,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
extensions: bcmath, cli, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none
@@ -96,8 +97,9 @@ jobs:
MAIL_MAILER: array
SESSION_DRIVER: array
QUEUE_CONNECTION: sync
HASHIDS_SALT: alittlebitofsalt1234
DB_CONNECTION: sqlite
DB_DATABASE: testing.sqlite
DB_DATABASE: ${{ github.workspace }}/database/testing.sqlite
steps:
- name: Code Checkout
uses: actions/checkout@v4
@@ -119,7 +121,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
extensions: bcmath, cli, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none

59
.gitignore vendored
View File

@@ -1,28 +1,41 @@
/.phpunit.cache
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/storage/clockwork/*
/vendor
*.DS_Store*
.env
.env.backup
.env.production
.phpactor.json
.phpunit.result.cache
Homestead.json
Homestead.yaml
auth.json
npm-debug.log
yarn-error.log
/.fleet
!.env.ci
!.env.example
.env*
.vagrant/*
.vscode/*
storage/framework/*
/.idea
/.vscode
/nbproject
/.direnv
node_modules
*.log
_ide_helper.php
_ide_helper_models.php
.phpstorm.meta.php
.yarn
public/assets/manifest.json
/database/*.sqlite
filament-monaco-editor/
_ide_helper*
/.phpstorm.meta.php
*.sqlite
# For local development with docker
# Remove if we ever put the Dockerfile in the repo
.dockerignore
docker-compose.yml
# for image related files
misc
.php-cs-fixer.cache
coverage.xml
resources/lang/locales.js
.phpunit.result.cache
/public/build
/public/hot
result
docker-compose.yaml
public/css/filament-monaco-editor/
public/js/filament-monaco-editor/

View File

@@ -10,28 +10,30 @@ use Illuminate\Support\Facades\Artisan;
class AppSettingsCommand extends Command
{
use EnvironmentWriterTrait;
public const CACHE_DRIVERS = [
'file' => 'Filesystem (recommended)',
'redis' => 'Redis',
'memcached' => 'Memcached',
'file' => 'Filesystem (recommended)',
];
public const SESSION_DRIVERS = [
'file' => 'Filesystem (recommended)',
'redis' => 'Redis',
'database' => 'Database',
'memcached' => 'Memcached',
'database' => 'MySQL Database',
'file' => 'Filesystem (recommended)',
'cookie' => 'Cookie',
];
public const QUEUE_DRIVERS = [
'database' => 'Database (recommended)',
'redis' => 'Redis',
'sync' => 'Synchronous',
'database' => 'MySQL Database',
'sync' => 'Sync (recommended)',
];
protected $description = 'Configure basic environment settings for the Panel.';
protected $signature = 'p:environment:setup
{--new-salt : Whether or not to generate a new salt for Hashids.}
{--url= : The URL that this Panel is running on.}
{--cache= : The cache driver backend to use.}
{--session= : The session driver backend to use.}
@@ -60,6 +62,10 @@ class AppSettingsCommand extends Command
{
$this->variables['APP_TIMEZONE'] = 'UTC';
if (empty(config('hashids.salt')) || $this->option('new-salt')) {
$this->variables['HASHIDS_SALT'] = str_random(20);
}
$this->output->comment(__('commands.appsettings.comment.url'));
$this->variables['APP_URL'] = $this->option('url') ?? $this->ask(
'Application URL',
@@ -80,7 +86,7 @@ class AppSettingsCommand extends Command
array_key_exists($selected, self::SESSION_DRIVERS) ? $selected : null
);
$selected = config('queue.default', 'database');
$selected = config('queue.default', 'sync');
$this->variables['QUEUE_CONNECTION'] = $this->option('queue') ?? $this->choice(
'Queue Driver',
self::QUEUE_DRIVERS,
@@ -98,13 +104,7 @@ class AppSettingsCommand extends Command
$this->variables['SESSION_SECURE_COOKIE'] = 'true';
}
$redisUsed = count(collect($this->variables)->filter(function ($item) {
return $item === 'redis';
})) !== 0;
if ($redisUsed) {
$this->requestRedisSettings();
}
$this->checkForRedis();
$path = base_path('.env');
if (!file_exists($path)) {
@@ -117,22 +117,25 @@ class AppSettingsCommand extends Command
Artisan::call('key:generate');
}
if ($this->variables['QUEUE_CONNECTION'] !== 'sync') {
$this->call('p:environment:queue-service', [
'--use-redis' => $redisUsed,
]);
}
$this->info($this->console->output());
return 0;
}
/**
* Request redis connection details and verify them.
* Check if redis is selected, if so, request connection details and verify them.
*/
private function requestRedisSettings(): void
private function checkForRedis()
{
$items = collect($this->variables)->filter(function ($item) {
return $item === 'redis';
});
// Redis was not selected, no need to continue.
if (count($items) === 0) {
return;
}
$this->output->note(__('commands.appsettings.redis.note'));
$this->variables['REDIS_HOST'] = $this->option('redis-host') ?? $this->ask(
'Redis Host',

View File

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

View File

@@ -31,7 +31,7 @@ class EmailSettingsCommand extends Command
*/
public function handle(): void
{
$this->variables['MAIL_MAILER'] = $this->option('driver') ?? $this->choice(
$this->variables['MAIL_DRIVER'] = $this->option('driver') ?? $this->choice(
trans('command/messages.environment.mail.ask_driver'),
[
'log' => 'Log',
@@ -41,10 +41,10 @@ class EmailSettingsCommand extends Command
'mandrill' => 'Mandrill',
'postmark' => 'Postmark',
],
env('MAIL_MAILER', env('MAIL_DRIVER', 'smtp')),
'smtp',
);
$method = 'setup' . studly_case($this->variables['MAIL_MAILER']) . 'DriverVariables';
$method = 'setup' . studly_case($this->variables['MAIL_DRIVER']) . 'DriverVariables';
if (method_exists($this, $method)) {
$this->{$method}();
}

View File

@@ -1,84 +0,0 @@
<?php
namespace App\Console\Commands\Environment;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Process;
class QueueWorkerServiceCommand extends Command
{
protected $description = 'Create the service for the queue worker.';
protected $signature = 'p:environment:queue-service
{--service-name= : Name of the queue worker service.}
{--user= : The user that PHP runs under.}
{--group= : The group that PHP runs under.}
{--use-redis : Whether redis is used.}
{--overwrite : Force overwrite if the service file already exists.}';
public function handle(): void
{
$serviceName = $this->option('service-name') ?? $this->ask('Queue worker service name', 'pelican-queue');
$path = '/etc/systemd/system/' . $serviceName . '.service';
$fileExists = file_exists($path);
if ($fileExists && !$this->option('overwrite') && !$this->confirm('The service file already exists. Do you want to overwrite it?')) {
$this->line('Creation of queue worker service file aborted because serive file already exists.');
return;
}
$user = $this->option('user') ?? $this->ask('Webserver User', 'www-data');
$group = $this->option('group') ?? $this->ask('Webserver Group', 'www-data');
$afterRedis = $this->option('use-redis') ? '\nAfter=redis-server.service' : '';
$basePath = base_path();
$success = File::put($path, "# Pelican Queue File
# ----------------------------------
[Unit]
Description=Pelican Queue Service$afterRedis
[Service]
User=$user
Group=$group
Restart=always
ExecStart=/usr/bin/php $basePath/artisan queue:work --tries=3
StartLimitInterval=180
StartLimitBurst=30
RestartSec=5s
[Install]
WantedBy=multi-user.target
");
if (!$success) {
$this->error('Error creating service file');
return;
}
if ($fileExists) {
$result = Process::run("systemctl restart $serviceName.service");
if ($result->failed()) {
$this->error('Error restarting service: ' . $result->errorOutput());
return;
}
$this->line('Queue worker service file updated successfully.');
} else {
$result = Process::run("systemctl enable --now $serviceName.service");
if ($result->failed()) {
$this->error('Error enabling service: ' . $result->errorOutput());
return;
}
$this->line('Queue worker service file created successfully.');
}
}
}

View File

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

View File

@@ -20,12 +20,9 @@ class MakeNodeCommand extends Command
{--overallocateMemory= : Enter the amount of ram to overallocate (% or -1 to overallocate the maximum).}
{--maxDisk= : Set the max disk amount.}
{--overallocateDisk= : Enter the amount of disk to overallocate (% or -1 to overallocate the maximum).}
{--maxCpu= : Set the max cpu amount.}
{--overallocateCpu= : Enter the amount of cpu to overallocate (% or -1 to overallocate the maximum).}
{--uploadSize= : Enter the maximum upload filesize.}
{--daemonListeningPort= : Enter the daemon listening port.}
{--daemonSFTPPort= : Enter the daemon SFTP listening port.}
{--daemonSFTPAlias= : Enter the daemon SFTP alias.}
{--daemonBase= : Enter the base folder.}';
protected $description = 'Creates a new node on the system via the CLI.';
@@ -61,12 +58,9 @@ class MakeNodeCommand extends Command
$data['memory_overallocate'] = $this->option('overallocateMemory') ?? $this->ask(__('commands.make_node.memory_overallocate'));
$data['disk'] = $this->option('maxDisk') ?? $this->ask(__('commands.make_node.disk'));
$data['disk_overallocate'] = $this->option('overallocateDisk') ?? $this->ask(__('commands.make_node.disk_overallocate'));
$data['cpu'] = $this->option('maxCpu') ?? $this->ask(__('commands.make_node.cpu'));
$data['cpu_overallocate'] = $this->option('overallocateCpu') ?? $this->ask(__('commands.make_node.cpu_overallocate'));
$data['upload_size'] = $this->option('uploadSize') ?? $this->ask(__('commands.make_node.upload_size'), '100');
$data['daemon_listen'] = $this->option('daemonListeningPort') ?? $this->ask(__('commands.make_node.daemonListen'), '8080');
$data['daemon_sftp'] = $this->option('daemonSFTPPort') ?? $this->ask(__('commands.make_node.daemonSFTP'), '2022');
$data['daemon_sftp_alias'] = $this->option('daemonSFTPAlias') ?? $this->ask(__('commands.make_node.daemonSFTPAlias'), '');
$data['daemon_base'] = $this->option('daemonBase') ?? $this->ask(__('commands.make_node.daemonBase'), '/var/lib/pelican/volumes');
$node = $this->creationService->handle($data);

View File

@@ -24,7 +24,7 @@ class ProcessRunnableCommand extends Command
->whereRelation('server', fn (Builder $builder) => $builder->whereNull('status'))
->where('is_active', true)
->where('is_processing', false)
->whereDate('next_run_at', '<=', Carbon::now()->toDateTimeString())
->whereDate('next_run_at', '<=', Carbon::now()->toDateString())
->get();
if ($schedules->count() < 1) {
@@ -62,7 +62,7 @@ class ProcessRunnableCommand extends Command
$this->line(trans('command/messages.schedule.output_line', [
'schedule' => $schedule->name,
'id' => $schedule->id,
'hash' => $schedule->hashid,
]));
} catch (\Throwable|\Exception $exception) {
logger()->error($exception, ['schedule_id' => $schedule->id]);

View File

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

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Contracts\Extensions;
use Hashids\HashidsInterface as VendorHashidsInterface;
interface HashidsInterface extends VendorHashidsInterface
{
/**
* Decode an encoded hashid and return the first result.
*
* @throws \InvalidArgumentException
*/
public function decodeFirst(string $encoded, string $default = null): mixed;
}

View File

@@ -7,7 +7,9 @@ use Filament\Notifications\Notification;
use Illuminate\Http\Request;
use Psr\Log\LoggerInterface;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use Illuminate\Container\Container;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
@@ -48,7 +50,7 @@ class DisplayException extends PanelException implements HttpExceptionInterface
*/
public function render(Request $request)
{
if ($request->is('livewire/update')) {
if (str($request->url())->contains('livewire')) {
Notification::make()
->title(static::class)
->body($this->getMessage())

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Exceptions\Service\Deployment;
use App\Exceptions\DisplayException;
class NoViableNodeException extends DisplayException
{
}

View File

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

View File

@@ -25,7 +25,7 @@ class DynamicDatabaseConnection
'port' => $host->port,
'database' => $database,
'username' => $host->username,
'password' => $host->password,
'password' => decrypt($host->password),
'charset' => self::DB_CHARSET,
'collation' => self::DB_COLLATION,
]);

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Extensions;
use Hashids\Hashids as VendorHashids;
use App\Contracts\Extensions\HashidsInterface;
class Hashids extends VendorHashids implements HashidsInterface
{
/**
* {@inheritdoc}
*/
public function decodeFirst(string $encoded, string $default = null): mixed
{
$result = $this->decode($encoded);
if (!is_array($result)) {
return $default;
}
return array_first($result, null, $default);
}
}

View File

@@ -7,7 +7,6 @@ use App\Models\Egg;
use App\Models\Node;
use App\Models\Server;
use App\Models\User;
use App\Services\Helpers\SoftwareVersionService;
use Filament\Actions\CreateAction;
use Filament\Pages\Page;
@@ -30,14 +29,8 @@ class Dashboard extends Page
public function getViewData(): array
{
/** @var SoftwareVersionService $softwareVersionService */
$softwareVersionService = app(SoftwareVersionService::class);
return [
'inDevelopment' => config('app.version') === 'canary',
'version' => $softwareVersionService->versionData()['version'],
'latestVersion' => $softwareVersionService->getPanel(),
'isLatest' => $softwareVersionService->isLatestPanel(),
'eggsCount' => Egg::query()->count(),
'nodesList' => ListNodes::getUrl(),
'nodesCount' => Node::query()->count(),
@@ -46,17 +39,15 @@ class Dashboard extends Page
'devActions' => [
CreateAction::make()
->label('Bugs & Features')
->label(trans('dashboard/index.sections.intro-developers.button_issues'))
->icon('tabler-brand-github')
->url('https://github.com/pelican-dev/panel/issues/new/choose', true)
->color('warning'),
CreateAction::make()
->label(trans('dashboard/index.sections.intro-developers.button_features'))
->icon('tabler-brand-github')
->url('https://github.com/pelican-dev/panel/discussions', true),
],
'updateActions' => [
CreateAction::make()
->label('Read Documentation')
->icon('tabler-clipboard-text')
->url('https://pelican.dev/docs/panel/update', true)
->color('warning'),
],
'nodeActions' => [
CreateAction::make()
->label(trans('dashboard/index.sections.intro-first-node.button_label'))
@@ -64,10 +55,14 @@ class Dashboard extends Page
->url(route('filament.admin.resources.nodes.create')),
],
'supportActions' => [
CreateAction::make()
->label(trans('dashboard/index.sections.intro-support.button_translate'))
->icon('tabler-language')
->url('https://crowdin.com/project/pelican-dev', true),
CreateAction::make()
->label(trans('dashboard/index.sections.intro-support.button_donate'))
->icon('tabler-cash')
->url($softwareVersionService->getDonations(), true)
->url('https://pelican.dev/donate', true)
->color('success'),
],
'helpActions' => [
@@ -75,6 +70,11 @@ class Dashboard extends Page
->label(trans('dashboard/index.sections.intro-help.button_docs'))
->icon('tabler-speedboat')
->url('https://pelican.dev/docs', true),
CreateAction::make()
->label(trans('dashboard/index.sections.intro-help.button_discord'))
->icon('tabler-brand-discord')
->url('https://discord.gg/pelican-panel', true)
->color('blurple'),
],
];
}

View File

@@ -4,24 +4,36 @@ namespace App\Filament\Resources;
use App\Filament\Resources\ApiKeyResource\Pages;
use App\Models\ApiKey;
use Filament\Resources\Components\Tab;
use Filament\Resources\Resource;
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 getNavigationBadge(): ?string
{
return static::getModel()::where('key_type', '2')->count() ?: null;
}
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 getRelations(): array
{
return [

View File

@@ -19,16 +19,30 @@ class CreateApiKey extends CreateRecord
return $form
->schema([
Forms\Components\Hidden::make('identifier')->default(ApiKey::generateTokenIdentifier(ApiKey::TYPE_APPLICATION)),
Forms\Components\Hidden::make('token')->default(str_random(ApiKey::KEY_LENGTH)),
Forms\Components\Hidden::make('token')->default(encrypt(str_random(ApiKey::KEY_LENGTH))),
Forms\Components\Hidden::make('user_id')
->default(auth()->user()->id)
->required(),
Forms\Components\Hidden::make('key_type')
Forms\Components\Select::make('key_type')
->inlineLabel()
->default(ApiKey::TYPE_APPLICATION)
->required(),
->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();
})
->selectablePlaceholder(false)
->required()
->default(ApiKey::TYPE_APPLICATION),
Forms\Components\Fieldset::make('Permissions')
->columns([

View File

@@ -5,8 +5,10 @@ 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 Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Filament\Tables;
class ListApiKeys extends ListRecords
@@ -17,12 +19,16 @@ class ListApiKeys extends ListRecords
{
return $table
->searchable(false)
->modifyQueryUsing(fn ($query) => $query->where('key_type', ApiKey::TYPE_APPLICATION))
->columns([
Tables\Columns\TextColumn::make('user.username')
->hidden()
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('key')
->copyable()
->icon('tabler-clipboard-text')
->state(fn (ApiKey $key) => $key->identifier . $key->token),
->state(fn (ApiKey $key) => $key->identifier . decrypt($key->token)),
Tables\Columns\TextColumn::make('memo')
->label('Description')
@@ -35,7 +41,6 @@ class ListApiKeys extends ListRecords
Tables\Columns\TextColumn::make('last_used_at')
->label('Last Used')
->placeholder('Not Used')
->dateTime()
->sortable(),
@@ -43,13 +48,13 @@ class ListApiKeys extends ListRecords
->label('Created')
->dateTime()
->sortable(),
Tables\Columns\TextColumn::make('user.username')
->label('Created By')
->url(fn (ApiKey $apiKey): string => route('filament.admin.resources.users.edit', ['record' => $apiKey->user])),
])
->filters([
//
])
->actions([
Tables\Actions\DeleteAction::make(),
//Tables\Actions\EditAction::make()
]);
}
@@ -59,4 +64,22 @@ class ListApiKeys extends ListRecords
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

@@ -14,11 +14,6 @@ class DatabaseHostResource extends Resource
protected static ?string $navigationIcon = 'tabler-database';
public static function getNavigationBadge(): ?string
{
return static::getModel()::count() ?: null;
}
public static function getRelations(): array
{
return [

View File

@@ -22,68 +22,55 @@ class CreateDatabaseHost extends CreateRecord
{
return $form
->schema([
Section::make()
->columns([
'default' => 2,
'sm' => 3,
'md' => 3,
'lg' => 4,
])
->schema([
Forms\Components\TextInput::make('host')
->columnSpan(2)
->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(onBlur: true)
->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state))
->maxLength(191),
Forms\Components\TextInput::make('port')
->columnSpan(1)
->helperText('The port that MySQL is running on for this host.')
->required()
->numeric()
->default(3306)
->minValue(0)
->maxValue(65535),
Forms\Components\TextInput::make('max_databases')
->label('Max databases')
->helpertext('Blank is unlimited.')
->numeric(),
Forms\Components\TextInput::make('name')
->label('Display 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\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\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'),
]),
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,
]),
]);
}
protected function getHeaderActions(): array
protected function mutateFormDataBeforeSave(array $data): array
{
return [
$this->getCreateFormAction()->formId('form'),
];
if (isset($data['password'])) {
$data['password'] = encrypt($data['password']);
}
return $data;
}
protected function getFormActions(): array
{
return [];
}
}

View File

@@ -3,7 +3,6 @@
namespace App\Filament\Resources\DatabaseHostResource\Pages;
use App\Filament\Resources\DatabaseHostResource;
use App\Models\DatabaseHost;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
use Filament\Forms;
@@ -18,76 +17,61 @@ class EditDatabaseHost extends EditRecord
{
return $form
->schema([
Section::make()
->columns([
'default' => 2,
'sm' => 3,
'md' => 3,
'lg' => 4,
])
->schema([
Forms\Components\TextInput::make('host')
->columnSpan(2)
->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(onBlur: true)
->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state))
->maxLength(191),
Forms\Components\TextInput::make('port')
->columnSpan(1)
->helperText('The port that MySQL is running on for this host.')
->required()
->numeric()
->minValue(0)
->maxValue(65535),
Forms\Components\TextInput::make('max_databases')
->label('Max databases')
->helpertext('Blank is unlimited.')
->numeric(),
Forms\Components\TextInput::make('name')
->label('Display 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\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\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'),
]),
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,
]),
]);
}
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make()
->label(fn (DatabaseHost $databaseHost) => $databaseHost->databases()->count() > 0 ? 'Database Host Has Databases' : 'Delete')
->disabled(fn (DatabaseHost $databaseHost) => $databaseHost->databases()->count() > 0),
$this->getSaveFormAction()->formId('form'),
Actions\DeleteAction::make(),
];
}
protected function getFormActions(): array
protected function mutateFormDataBeforeSave(array $data): array
{
return [];
}
if (isset($data['password'])) {
$data['password'] = encrypt($data['password']);
}
public function getRelationManagers(): array
{
return [
DatabaseHostResource\RelationManagers\DatabasesRelationManager::class,
];
return $data;
}
}

View File

@@ -12,8 +12,6 @@ class ListDatabaseHosts extends ListRecords
{
protected static string $resource = DatabaseHostResource::class;
protected ?string $heading = 'Database Hosts';
public function table(Table $table): Table
{
return $table
@@ -50,7 +48,7 @@ class ListDatabaseHosts extends ListRecords
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make('create')->label('New Database Host'),
Actions\CreateAction::make(),
];
}
}

View File

@@ -1,70 +0,0 @@
<?php
namespace App\Filament\Resources\DatabaseHostResource\RelationManagers;
use App\Models\Database;
use App\Services\Databases\DatabasePasswordService;
use Filament\Forms;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
class DatabasesRelationManager extends RelationManager
{
protected static string $relationship = 'databases';
public function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('database')->columnSpanFull(),
Forms\Components\TextInput::make('username'),
Forms\Components\TextInput::make('password')
->hintAction(
Action::make('rotate')
->icon('tabler-refresh')
->requiresConfirmation()
->action(fn (DatabasePasswordService $service, Database $database, $set, $get) => $this->rotatePassword($service, $database, $set, $get))
)
->formatStateUsing(fn (Database $database) => $database->password),
Forms\Components\TextInput::make('remote')->label('Connections From'),
Forms\Components\TextInput::make('max_connections'),
Forms\Components\TextInput::make('JDBC')
->label('JDBC Connection String')
->columnSpanFull()
->formatStateUsing(fn (Forms\Get $get, Database $database) => 'jdbc:mysql://' . $get('username') . ':' . urlencode($database->password) . '@' . $database->host->host . ':' . $database->host->port . '/' . $get('database')),
]);
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('servers')
->columns([
Tables\Columns\TextColumn::make('database')->icon('tabler-database'),
Tables\Columns\TextColumn::make('username')->icon('tabler-user'),
//Tables\Columns\TextColumn::make('password'),
Tables\Columns\TextColumn::make('remote'),
Tables\Columns\TextColumn::make('server.name')
->icon('tabler-brand-docker')
->url(fn (Database $database) => route('filament.admin.resources.servers.edit', ['record' => $database->server_id])),
Tables\Columns\TextColumn::make('max_connections'),
Tables\Columns\TextColumn::make('created_at')->dateTime(),
])
->actions([
Tables\Actions\DeleteAction::make(),
Tables\Actions\ViewAction::make()->color('primary'),
//Tables\Actions\EditAction::make(),
]);
}
protected function rotatePassword(DatabasePasswordService $service, Database $database, $set, $get): void
{
$newPassword = $service->handle($database);
$jdbcString = 'jdbc:mysql://' . $get('username') . ':' . urlencode($newPassword) . '@' . $database->host->host . ':' . $database->host->port . '/' . $get('database');
$set('password', $newPassword);
$set('JDBC', $jdbcString);
}
}

View File

@@ -14,11 +14,6 @@ class DatabaseResource extends Resource
protected static bool $shouldRegisterNavigation = false;
public static function getNavigationBadge(): ?string
{
return static::getModel()::count() ?: null;
}
public static function getRelations(): array
{
return [

View File

@@ -16,11 +16,6 @@ class EggResource extends Resource
protected static ?string $recordRouteKeyName = 'id';
public static function getNavigationBadge(): ?string
{
return static::getModel()::count() ?: null;
}
public static function getRelations(): array
{
return [

View File

@@ -31,7 +31,6 @@ class CreateEgg extends CreateRecord
Forms\Components\TextInput::make('author')
->maxLength(191)
->required()
->email()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->helperText('The author of this version of the Egg.'),
Forms\Components\Textarea::make('description')

View File

@@ -25,16 +25,12 @@ class EditEgg extends EditRecord
Forms\Components\TextInput::make('name')
->required()
->maxLength(191)
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 1])
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->helperText('A simple, human-readable name to use as an identifier for this Egg.'),
Forms\Components\TextInput::make('uuid')
->label('Egg UUID')
->disabled()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->helperText('This is the globally unique identifier for this Egg which Wings uses as an identifier.'),
Forms\Components\TextInput::make('id')
->label('Egg ID')
->disabled(),
Forms\Components\Textarea::make('description')
->rows(3)
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
@@ -42,7 +38,6 @@ class EditEgg extends EditRecord
Forms\Components\TextInput::make('author')
->required()
->maxLength(191)
->email()
->disabled()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->helperText('The author of this version of the Egg. Uploading a new Egg configuration from a different author will change this.'),
@@ -207,19 +202,6 @@ class EditEgg extends EditRecord
->color('primary')
// TODO uses old admin panel export service
->url(fn (Egg $egg): string => route('admin.eggs.export', ['egg' => $egg['id']])),
$this->getSaveFormAction()->formId('form'),
];
}
protected function getFormActions(): array
{
return [];
}
public function getRelationManagers(): array
{
return [
EggResource\RelationManagers\ServersRelationManager::class,
];
}
}

View File

@@ -8,7 +8,6 @@ use App\Services\Eggs\Sharing\EggImporterService;
use Exception;
use Filament\Actions;
use Filament\Forms;
use Filament\Forms\Components\Tabs;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Table;
@@ -32,13 +31,28 @@ class ListEggs extends ListRecords
->searchable(),
Tables\Columns\TextColumn::make('name')
->icon('tabler-egg')
->description(fn ($record): ?string => (strlen($record->description) > 120) ? substr($record->description, 0, 120).'...' : $record->description)
->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(),
@@ -49,6 +63,9 @@ class ListEggs extends ListRecords
// TODO uses old admin panel export service
->url(fn (Egg $egg): string => route('admin.eggs.export', ['egg' => $egg])),
])
->headerActions([
//
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
@@ -63,58 +80,21 @@ class ListEggs extends ListRecords
Actions\Action::make('import')
->label('Import')
->form([
Tabs::make('Tabs')
->tabs([
Tabs\Tab::make('From File')
->icon('tabler-file-upload')
->schema([
Forms\Components\FileUpload::make('egg')
->label('Egg')
->hint('This should be the json file ( egg-minecraft.json )')
->acceptedFileTypes(['application/json'])
->storeFiles(false)
->multiple(),
]),
Tabs\Tab::make('From URL')
->icon('tabler-world-upload')
->schema([
Forms\Components\TextInput::make('url')
->label('URL')
->hint('This URL should point to a single json file')
->url(),
]),
])
->contained(false),
Forms\Components\FileUpload::make('egg')
->acceptedFileTypes(['application/json'])
->storeFiles(false)
->multiple(),
])
->action(function (array $data): void {
/** @var TemporaryUploadedFile $eggFile */
$eggFile = $data['egg'];
/** @var EggImporterService $eggImportService */
$eggImportService = resolve(EggImporterService::class);
if (!empty($data['egg'])) {
/** @var TemporaryUploadedFile[] $eggFile */
$eggFile = $data['egg'];
foreach ($eggFile as $file) {
try {
$eggImportService->fromFile($file);
} catch (Exception $exception) {
Notification::make()
->title('Import Failed')
->danger()
->send();
report($exception);
return;
}
}
}
if (!empty($data['url'])) {
foreach ($eggFile as $file) {
try {
$eggImportService->fromUrl($data['url']);
$eggImportService->handle($file);
} catch (Exception $exception) {
Notification::make()
->title('Import Failed')

View File

@@ -1,42 +0,0 @@
<?php
namespace App\Filament\Resources\EggResource\RelationManagers;
use App\Models\Server;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
class ServersRelationManager extends RelationManager
{
protected static string $relationship = 'servers';
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('servers')
->emptyStateDescription('No Servers')->emptyStateHeading('No servers are assigned this egg.')
->searchable(false)
->columns([
Tables\Columns\TextColumn::make('user.username')
->label('Owner')
->icon('tabler-user')
->url(fn (Server $server): string => route('filament.admin.resources.users.edit', ['record' => $server->user]))
->sortable(),
Tables\Columns\TextColumn::make('name')
->icon('tabler-brand-docker')
->url(fn (Server $server): string => route('filament.admin.resources.servers.edit', ['record' => $server]))
->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])),
Tables\Columns\TextColumn::make('image')
->label('Docker Image'),
Tables\Columns\SelectColumn::make('allocation.id')
->label('Primary Allocation')
->options(fn ($state, Server $server) => [$server->allocation->id => $server->allocation->address])
->selectablePlaceholder(false)
->sortable(),
]);
}
}

View File

@@ -12,11 +12,6 @@ class MountResource extends Resource
protected static ?string $navigationIcon = 'tabler-layers-linked';
public static function getNavigationBadge(): ?string
{
return static::getModel()::count() ?: null;
}
public static function getRelations(): array
{
return [

View File

@@ -3,6 +3,7 @@
namespace App\Filament\Resources\MountResource\Pages;
use App\Filament\Resources\MountResource;
use App\Services\Servers\ServerCreationService;
use Filament\Forms\Components\Group;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;

View File

@@ -98,12 +98,6 @@ class EditMount extends EditRecord
{
return [
Actions\DeleteAction::make(),
$this->getSaveFormAction()->formId('form'),
];
}
protected function getFormActions(): array
{
return [];
}
}

View File

@@ -15,11 +15,6 @@ class NodeResource extends Resource
protected static ?string $recordTitleAttribute = 'name';
public static function getNavigationBadge(): ?string
{
return static::getModel()::count() ?: null;
}
public static function getRelations(): array
{
return [

View File

@@ -4,7 +4,6 @@ namespace App\Filament\Resources\NodeResource\Pages;
use App\Filament\Resources\NodeResource;
use Filament\Forms;
use Filament\Forms\Components\Tabs;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Support\HtmlString;
@@ -18,343 +17,179 @@ class CreateNode extends CreateRecord
public function form(Forms\Form $form): Forms\Form
{
return $form->schema([
Tabs::make('Tabs')
->columns([
'default' => 2,
'sm' => 3,
'md' => 3,
'lg' => 4,
])
->persistTabInQueryString()
->columnSpanFull()
->tabs([
Tabs\Tab::make('Basic Settings')
->icon('tabler-server')
->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 '
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 '';
}
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';
}
})
->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);
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);
}
[$subdomain] = str($state)->explode('.', 2);
if (!is_numeric($subdomain)) {
$set('name', $subdomain);
}
if (!$state || is_ip($state)) {
$set('dns', null);
if (!$state || is_ip($state)) {
$set('dns', null);
return;
}
return;
}
$validRecords = gethostbynamel($state);
if ($validRecords) {
$set('dns', true);
$validRecords = gethostbynamel($state);
if ($validRecords) {
$set('dns', true);
$set('ip', collect($validRecords)->first());
$set('ip', collect($validRecords)->first());
return;
}
return;
}
$set('dns', false);
})
->maxLength(191),
$set('dns', false);
})
->maxLength(191),
Forms\Components\TextInput::make('ip')
->disabled()
->hidden(),
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\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('daemon_listen')
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 1,
])
->label(trans('strings.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('daemon_listen')
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 1,
])
->label(trans('strings.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\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.');
}
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.';
}
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'),
]),
Tabs\Tab::make('Advanced Settings')
->icon('tabler-server-cog')
->schema([
Forms\Components\TextInput::make('upload_size')
->label('Upload Limit')
->helperText('Enter the maximum size of files that can be uploaded through the web-based file manager.')
->columnSpan(1)
->numeric()->required()
->default(256)
->minValue(1)
->maxValue(1024)
->suffix('MiB'),
Forms\Components\ToggleButtons::make('public')
->label('Automatic Allocation')->inline()
->default(true)
->columnSpan(1)
->options([
true => 'Yes',
false => 'No',
])
->colors([
true => 'success',
false => 'danger',
]),
Forms\Components\ToggleButtons::make('maintenance_mode')
->label('Maintenance Mode')->inline()
->columnSpan(1)
->default(false)
->hinticon('tabler-question-mark')
->hintIconTooltip("If the node is marked 'Under Maintenance' users won't be able to access servers that are on this node.")
->options([
true => 'Enable',
false => 'Disable',
])
->colors([
true => 'danger',
false => 'success',
]),
Forms\Components\TagsInput::make('tags')
->label('Tags')
->disabled()
->placeholder('Not Implemented')
->hintIcon('tabler-question-mark')
->hintIconTooltip('Not Implemented')
->columnSpan(1),
Forms\Components\Grid::make()
->columns(6)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_mem')
->label('Memory')->inlineLabel()->inline()
->afterStateUpdated(fn (Forms\Set $set) => $set('memory', 0))
->afterStateUpdated(fn (Forms\Set $set) => $set('memory_overallocate', 0))
->formatStateUsing(fn (Forms\Get $get) => $get('memory') == 0)
->live()
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
Forms\Components\TextInput::make('memory')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
->label('Memory Limit')->inlineLabel()
->suffix('MiB')
->columnSpan(2)
->numeric()
->minValue(0)
->default(0),
Forms\Components\TextInput::make('memory_overallocate')
->dehydratedWhenHidden()
->label('Overallocate')->inlineLabel()
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
->hintIcon('tabler-question-mark')
->hintIconTooltip('The % allowable to go over the set limit.')
->columnSpan(2)
->numeric()
->minValue(-1)
->maxValue(100)
->default(0)
->suffix('%'),
]),
Forms\Components\Grid::make()
->columns(6)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_disk')
->label('Disk')->inlineLabel()->inline()
->live()
->afterStateUpdated(fn (Forms\Set $set) => $set('disk', 0))
->afterStateUpdated(fn (Forms\Set $set) => $set('disk_overallocate', 0))
->formatStateUsing(fn (Forms\Get $get) => $get('disk') == 0)
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
Forms\Components\TextInput::make('disk')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
->label('Disk Limit')->inlineLabel()
->suffix('MiB')
->columnSpan(2)
->numeric()
->minValue(0)
->default(0),
Forms\Components\TextInput::make('disk_overallocate')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
->label('Overallocate')->inlineLabel()
->hintIcon('tabler-question-mark')
->hintIconTooltip('The % allowable to go over the set limit.')
->columnSpan(2)
->numeric()
->minValue(-1)
->maxValue(100)
->default(0)
->suffix('%'),
]),
Forms\Components\Grid::make()
->columns(6)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_cpu')
->label('CPU')->inlineLabel()->inline()
->live()
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu', 0))
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu_overallocate', 0))
->formatStateUsing(fn (Forms\Get $get) => $get('cpu') == 0)
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
Forms\Components\TextInput::make('cpu')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_cpu'))
->label('CPU Limit')->inlineLabel()
->suffix('%')
->columnSpan(2)
->numeric()
->default(0)
->minValue(0),
Forms\Components\TextInput::make('cpu_overallocate')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_cpu'))
->label('Overallocate')->inlineLabel()
->hintIcon('tabler-question-mark')
->hintIconTooltip('The % allowable to go over the set limit.')
->columnSpan(2)
->numeric()
->default(0)
->minValue(-1)
->maxValue(100)
->suffix('%'),
]),
]),
]),
]);
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')
->label('strings.description')
->hidden()
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 2,
'lg' => 4,
])
->rows(5),
Forms\Components\Hidden::make('skipValidation')->default(true),
]);
}
protected function getRedirectUrlParameters(): array

View File

@@ -6,11 +6,9 @@ use App\Filament\Resources\NodeResource;
use App\Filament\Resources\NodeResource\Widgets\NodeMemoryChart;
use App\Filament\Resources\NodeResource\Widgets\NodeStorageChart;
use App\Models\Node;
use App\Services\Nodes\NodeUpdateService;
use Filament\Actions;
use Filament\Forms;
use Filament\Forms\Components\Tabs;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use Illuminate\Support\HtmlString;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
@@ -34,347 +32,13 @@ class EditNode extends EditRecord
->tabs([
Tabs\Tab::make('Basic Settings')
->icon('tabler-server')
->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('daemon_listen')
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 1,
])
->label(trans('strings.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'), ]),
Tabs\Tab::make('Advanced Settings')
->columns(['default' => 1, 'sm' => 1, 'md' => 4, 'lg' => 6])
->icon('tabler-server-cog')
->schema([
Forms\Components\TextInput::make('id')
->label('Node ID')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 1])
->disabled(),
Forms\Components\TextInput::make('uuid')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->label('Node UUID')
->hintAction(CopyAction::make())
->disabled(),
Forms\Components\TagsInput::make('tags')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->label('Tags')
->disabled()
->placeholder('Not Implemented')
->hintIcon('tabler-question-mark')
->hintIconTooltip('Not Implemented'),
Forms\Components\TextInput::make('upload_size')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 1])
->label('Upload Limit')
->hintIcon('tabler-question-mark')
->hintIconTooltip('Enter the maximum size of files that can be uploaded through the web-based file manager.')
->numeric()->required()
->minValue(1)
->maxValue(1024)
->suffix('MiB'),
Forms\Components\TextInput::make('daemon_sftp')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
->label('SFTP Port')
->minValue(0)
->maxValue(65536)
->default(2022)
->required()
->integer(),
Forms\Components\TextInput::make('daemon_sftp_alias')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
->label('SFTP Alias')
->helperText('Display alias for the SFTP address. Leave empty to use the Node FQDN.'),
Forms\Components\ToggleButtons::make('public')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
->label('Automatic Allocation')->inline()
->options([
true => 'Yes',
false => 'No',
])
->colors([
true => 'success',
false => 'danger',
]),
Forms\Components\ToggleButtons::make('maintenance_mode')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
->label('Maintenance Mode')->inline()
->hinticon('tabler-question-mark')
->hintIconTooltip("If the node is marked 'Under Maintenance' users won't be able to access servers that are on this node.")
->options([
false => 'Disable',
true => 'Enable',
])
->colors([
false => 'success',
true => 'danger',
]),
Forms\Components\Grid::make()
->columns(['default' => 1, 'sm' => 1, 'md' => 3, 'lg' => 6])
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_mem')
->label('Memory')->inlineLabel()->inline()
->afterStateUpdated(fn (Forms\Set $set) => $set('memory', 0))
->afterStateUpdated(fn (Forms\Set $set) => $set('memory_overallocate', 0))
->formatStateUsing(fn (Forms\Get $get) => $get('memory') == 0)
->live()
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2]),
Forms\Components\TextInput::make('memory')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
->label('Memory Limit')->inlineLabel()
->suffix('MiB')
->required()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->numeric()
->minValue(0),
Forms\Components\TextInput::make('memory_overallocate')
->dehydratedWhenHidden()
->label('Overallocate')->inlineLabel()
->required()
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
->hintIcon('tabler-question-mark')
->hintIconTooltip('The % allowable to go over the set limit.')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->numeric()
->minValue(-1)
->maxValue(100)
->suffix('%'),
]),
Forms\Components\Grid::make()
->columns(['default' => 1, 'sm' => 1, 'md' => 3, 'lg' => 6])
->schema([
Forms\Components\ToggleButtons::make('unlimited_disk')
->label('Disk')->inlineLabel()->inline()
->live()
->afterStateUpdated(fn (Forms\Set $set) => $set('disk', 0))
->afterStateUpdated(fn (Forms\Set $set) => $set('disk_overallocate', 0))
->formatStateUsing(fn (Forms\Get $get) => $get('disk') == 0)
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2]),
Forms\Components\TextInput::make('disk')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
->label('Disk Limit')->inlineLabel()
->suffix('MiB')
->required()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->numeric()
->minValue(0),
Forms\Components\TextInput::make('disk_overallocate')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
->label('Overallocate')->inlineLabel()
->hintIcon('tabler-question-mark')
->hintIconTooltip('The % allowable to go over the set limit.')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->required()
->numeric()
->minValue(-1)
->maxValue(100)
->suffix('%'),
]),
Forms\Components\Grid::make()
->columns(6)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_cpu')
->label('CPU')->inlineLabel()->inline()
->live()
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu', 0))
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu_overallocate', 0))
->formatStateUsing(fn (Forms\Get $get) => $get('cpu') == 0)
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
Forms\Components\TextInput::make('cpu')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_cpu'))
->label('CPU Limit')->inlineLabel()
->suffix('%')
->required()
->columnSpan(2)
->numeric()
->minValue(0),
Forms\Components\TextInput::make('cpu_overallocate')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_cpu'))
->label('Overallocate')->inlineLabel()
->hintIcon('tabler-question-mark')
->hintIconTooltip('The % allowable to go over the set limit.')
->columnSpan(2)
->required()
->numeric()
->minValue(-1)
->maxValue(100)
->suffix('%'),
]),
]),
Tabs\Tab::make('Configuration File')
->schema((new CreateNode())->form($form)->getComponents()),
// Tabs\Tab::make('Advanced Settings')
// ->icon('tabler-server-cog')
// ->schema([
// Forms\Components\Placeholder::make('Coming soon!'),
// ]),
Tabs\Tab::make('Configuration')
->icon('tabler-code')
->schema([
Forms\Components\Placeholder::make('instructions')
@@ -388,18 +52,6 @@ class EditNode extends EditRecord
->rows(19)
->hintAction(CopyAction::make())
->columnSpanFull(),
Forms\Components\Actions::make([
Forms\Components\Actions\Action::make('resetKey')
->label('Reset Daemon Token')
->color('danger')
->requiresConfirmation()
->modalHeading('Reset Daemon Token?')
->modalDescription('Resetting the daemon token will void any request coming from the old token. This token is used for all sensitive operations on the daemon including server creation and deletion. We suggest changing this token regularly for security.')
->action(fn (NodeUpdateService $nodeUpdateService, Node $node) => $nodeUpdateService->handle($node, [], true)
&& Notification::make()->success()->title('Daemon Key Reset')->send()
&& $this->fillForm()
),
]),
]),
]),
]);
@@ -414,17 +66,18 @@ class EditNode extends EditRecord
return $data;
}
protected function getFormActions(): array
protected function getSteps(): array
{
return [];
return [
];
}
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make()
->disabled(fn (Node $node) => $node->servers()->count() > 0)
->label(fn (Node $node) => $node->servers()->count() > 0 ? 'Node Has Servers' : 'Delete'),
$this->getSaveFormAction()->formId('form'),
];
}
@@ -435,9 +88,4 @@ class EditNode extends EditRecord
NodeMemoryChart::class,
];
}
protected function afterSave(): void
{
$this->fillForm();
}
}

View File

@@ -42,21 +42,15 @@ class ListNodes extends ListRecords
->visibleFrom('sm')
->icon('tabler-device-desktop-analytics')
->numeric()
->suffix(' GiB')
->formatStateUsing(fn ($state) => number_format($state / 1024, 2))
->suffix(' GB')
->formatStateUsing(fn ($state) => number_format($state / 1000, 2))
->sortable(),
Tables\Columns\TextColumn::make('disk')
->visibleFrom('sm')
->icon('tabler-file')
->numeric()
->suffix(' GiB')
->formatStateUsing(fn ($state) => number_format($state / 1024, 2))
->sortable(),
Tables\Columns\TextColumn::make('cpu')
->visibleFrom('sm')
->icon('tabler-file')
->numeric()
->suffix(' %')
->suffix(' GB')
->formatStateUsing(fn ($state) => number_format($state / 1000, 2))
->sortable(),
Tables\Columns\IconColumn::make('scheme')
->visibleFrom('xl')

View File

@@ -40,21 +40,17 @@ class AllocationsRelationManager extends RelationManager
->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->server_id === null)
->searchable()
->columns([
Tables\Columns\TextColumn::make('id'),
Tables\Columns\TextColumn::make('port')
->searchable()
->label('Port'),
Tables\Columns\TextColumn::make('server.name')
->label('Server')
->icon('tabler-brand-docker')
->searchable()
->url(fn (Allocation $allocation): string => $allocation->server ? route('filament.admin.resources.servers.edit', ['record' => $allocation->server]) : ''),
Tables\Columns\TextInputColumn::make('ip_alias')
->searchable()
Tables\Columns\TextColumn::make('ip_alias')
->label('Alias'),
Tables\Columns\TextInputColumn::make('ip')
->searchable()
Tables\Columns\TextColumn::make('ip')
->label('IP'),
Tables\Columns\TextColumn::make('port')
->searchable()
->label('Port'),
])
->filters([
//
@@ -113,7 +109,7 @@ class AllocationsRelationManager extends RelationManager
$start = max((int) $start, 0);
$end = min((int) $end, 2 ** 16 - 1);
foreach (range($start, $end) as $i) {
for ($i = $start; $i <= $end; $i++) {
$ports->push($i);
}
}
@@ -130,8 +126,6 @@ class AllocationsRelationManager extends RelationManager
$ports = $sortedPorts;
}
$ports = $ports->filter(fn ($port) => $port > 1024 && $port < 65535)->values();
if ($update) {
$set('allocation_ports', $ports->all());
}

View File

@@ -40,8 +40,8 @@ class NodeMemoryChart extends ChartWidget
/** @var Node $node */
$node = $this->record;
$total = ($node->statistics()['memory_total'] ?? 0) / 1024 / 1024 / 1024;
$used = ($node->statistics()['memory_used'] ?? 0) / 1024 / 1024 / 1024;
$total = $node->statistics()['memory_total'] ?? 0;
$used = $node->statistics()['memory_used'] ?? 0;
$unused = $total - $used;
return [

View File

@@ -40,8 +40,8 @@ class NodeStorageChart extends ChartWidget
/** @var Node $node */
$node = $this->record;
$total = ($node->statistics()['disk_total'] ?? 0) / 1024 / 1024 / 1024;
$used = ($node->statistics()['disk_used'] ?? 0) / 1024 / 1024 / 1024;
$total = $node->statistics()['disk_total'] ?? 0;
$used = $node->statistics()['disk_used'] ?? 0;
$unused = $total - $used;
return [

View File

@@ -14,11 +14,6 @@ class ServerResource extends Resource
protected static ?string $recordTitleAttribute = 'name';
public static function getNavigationBadge(): ?string
{
return static::getModel()::count() ?: null;
}
public static function getRelations(): array
{
return [

View File

@@ -23,8 +23,6 @@ class CreateServer extends CreateRecord
protected static string $resource = ServerResource::class;
protected static bool $canCreateAnother = false;
public ?Node $node = null;
public function form(Form $form): Form
{
return $form
@@ -79,16 +77,13 @@ class CreateServer extends CreateRecord
Forms\Components\Select::make('node_id')
->disabledOn('edit')
->prefixIcon('tabler-server-2')
->default(fn () => ($this->node = Node::query()->latest()->first())?->id)
->default(fn () => Node::query()->latest()->first()?->id)
->columnSpan(2)
->live()
->relationship('node', 'name')
->searchable()
->preload()
->afterStateUpdated(function (Forms\Set $set, $state) {
$set('allocation_id', null);
$this->node = Node::find($state);
})
->afterStateUpdated(fn (Forms\Set $set) => $set('allocation_id', null))
->required(),
Forms\Components\Select::make('allocation_id')
@@ -314,6 +309,55 @@ class CreateServer extends CreateRecord
->inline()
->required(),
Forms\Components\Select::make('select_image')
->label('Docker Image Name')
->prefixIcon('tabler-brand-docker')
->live()
->afterStateUpdated(fn (Forms\Set $set, $state) => $set('image', $state))
->options(function ($state, Forms\Get $get, Forms\Set $set) {
$egg = Egg::query()->find($get('egg_id'));
$images = $egg->docker_images ?? [];
$currentImage = $get('image');
if (!$currentImage && $images) {
$defaultImage = collect($images)->first();
$set('image', $defaultImage);
$set('select_image', $defaultImage);
}
return array_flip($images) + ['ghcr.io/custom-image' => 'Custom Image'];
})
->selectablePlaceholder(false)
->columnSpan([
'default' => 2,
'sm' => 2,
'md' => 2,
'lg' => 3,
]),
Forms\Components\TextInput::make('image')
->label('Docker Image')
->prefixIcon('tabler-brand-docker')
->live()
->debounce(500)
->afterStateUpdated(function ($state, Forms\Get $get, Forms\Set $set) {
$egg = Egg::query()->find($get('egg_id'));
$images = $egg->docker_images ?? [];
if (in_array($state, $images)) {
$set('select_image', $state);
} else {
$set('select_image', 'ghcr.io/custom-image');
}
})
->placeholder('Enter a custom Image')
->columnSpan([
'default' => 2,
'sm' => 2,
'md' => 2,
'lg' => 3,
]),
Forms\Components\Textarea::make('startup')
->hintIcon('tabler-code')
->label('Startup Command')
@@ -349,12 +393,7 @@ class CreateServer extends CreateRecord
]))
->schema([
Forms\Components\Placeholder::make('Select an egg first to show its variables!')
->hidden(fn (Forms\Get $get) => $get('egg_id')),
Forms\Components\Placeholder::make('The selected egg has no variables!')
->hidden(fn (Forms\Get $get) => !$get('egg_id') ||
Egg::query()->find($get('egg_id'))?->variables()?->count()
),
->hidden(fn (Forms\Get $get) => !empty($get('server_variables'))),
Forms\Components\Repeater::make('server_variables')
->relationship('serverVariables')
@@ -371,20 +410,19 @@ class CreateServer extends CreateRecord
$text = Forms\Components\TextInput::make('variable_value')
->hidden($this->shouldHideComponent(...))
->maxLength(191)
->required(fn (Forms\Get $get) => in_array('required', explode('|', $get('rules'))))
->rules(
->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'))->toString();
$message = str($validator->errors()->first())->replace('validatorkey', $get('name'));
$fail($message);
}
},
);
]);
$select = Forms\Components\Select::make('variable_value')
->hidden($this->shouldHideComponent(...))
@@ -414,7 +452,7 @@ class CreateServer extends CreateRecord
->columnSpan(2),
]),
Forms\Components\Section::make('Environment Management')
Forms\Components\Section::make('Resource Management')
->collapsed()
->icon('tabler-server-cog')
->iconColor('primary')
@@ -426,190 +464,171 @@ class CreateServer extends CreateRecord
])
->columnSpanFull()
->schema([
Forms\Components\Fieldset::make('Resource Limits')
->columnSpan([
'default' => 2,
'sm' => 4,
'md' => 4,
'lg' => 6,
])
->columns([
'default' => 1,
'sm' => 2,
'md' => 3,
'lg' => 3,
])
Forms\Components\Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_mem')
->label('Memory')->inlineLabel()->inline()
->default(true)
->afterStateUpdated(fn (Forms\Set $set) => $set('memory', 0))
->live()
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
Forms\Components\ToggleButtons::make('unlimited_mem')
->label('Memory')->inlineLabel()->inline()
->default(true)
->afterStateUpdated(fn (Forms\Set $set) => $set('memory', 0))
->live()
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
Forms\Components\TextInput::make('memory')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
->label('Memory Limit')->inlineLabel()
->suffix('MiB')
->default(0)
->required()
->columnSpan(2)
->numeric()
->minValue(0),
]),
Forms\Components\Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_disk')
->label('Disk Space')->inlineLabel()->inline()
->default(true)
->live()
->afterStateUpdated(fn (Forms\Set $set) => $set('disk', 0))
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
Forms\Components\TextInput::make('disk')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
->label('Disk Space Limit')->inlineLabel()
->suffix('MiB')
->default(0)
->required()
->columnSpan(2)
->numeric()
->minValue(0),
]),
Forms\Components\Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_cpu')
->label('CPU')->inlineLabel()->inline()
->default(true)
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu', 0))
->live()
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
Forms\Components\TextInput::make('cpu')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_cpu'))
->label('CPU Limit')->inlineLabel()
->suffix('%')
->default(0)
->required()
->columnSpan(2)
->numeric()
->minValue(0)
->helperText('100% equals one CPU core.'),
]),
Forms\Components\Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('swap_support')
->live()
->label('Enable Swap Memory')
->inlineLabel()
->inline()
->columnSpan(2)
->default('disabled')
->afterStateUpdated(function ($state, Forms\Set $set) {
$value = match ($state) {
'unlimited' => -1,
'disabled' => 0,
'limited' => 128,
};
$set('swap', $value);
})
->options([
'unlimited' => 'Unlimited',
'limited' => 'Limited',
'disabled' => 'Disabled',
])
->colors([
'unlimited' => 'primary',
'limited' => 'warning',
'disabled' => 'danger',
]),
Forms\Components\TextInput::make('swap')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => match ($get('swap_support')) {
'disabled', 'unlimited' => true,
'limited' => false,
})
->label('Swap Memory')
->default(0)
->suffix('MiB')
->minValue(-1)
->columnSpan(2)
->inlineLabel()
->required()
->integer(),
]),
Forms\Components\Hidden::make('io')
->helperText('The IO performance relative to other running containers')
->label('Block IO Proportion')
->default(500),
Forms\Components\Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('oom_killer')
->label('OOM Killer')
->inlineLabel()->inline()
->default(false)
->columnSpan(2)
->options([
false => 'Disabled',
true => 'Enabled',
])
->colors([
false => 'success',
true => 'danger',
]),
Forms\Components\TextInput::make('oom_disabled_hidden')
->hidden(),
]),
Forms\Components\TextInput::make('memory')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
->label('Memory Limit')->inlineLabel()
->suffix('MB')
->default(0)
->required()
->columnSpan(2)
->numeric(),
]),
Forms\Components\Fieldset::make('Feature Limits')
Forms\Components\Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_disk')
->label('Disk Space')->inlineLabel()->inline()
->default(true)
->live()
->afterStateUpdated(fn (Forms\Set $set) => $set('disk', 0))
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
Forms\Components\TextInput::make('disk')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
->label('Disk Space Limit')->inlineLabel()
->suffix('MB')
->default(0)
->required()
->columnSpan(2)
->numeric(),
]),
Forms\Components\Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_cpu')
->label('CPU')->inlineLabel()->inline()
->default(true)
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu', 0))
->live()
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
Forms\Components\TextInput::make('cpu')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_cpu'))
->label('CPU Limit')->inlineLabel()
->suffix('%')
->default(0)
->required()
->columnSpan(2)
->numeric(),
]),
Forms\Components\Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('swap_support')
->live()
->label('Enable Swap Memory')
->inlineLabel()
->inline()
->columnSpan(2)
->default('disabled')
->afterStateUpdated(function ($state, Forms\Set $set) {
$value = match ($state) {
'unlimited' => -1,
'disabled' => 0,
'limited' => 128,
};
$set('swap', $value);
})
->options([
'unlimited' => 'Unlimited',
'limited' => 'Limited',
'disabled' => 'Disabled',
])
->colors([
'unlimited' => 'primary',
'limited' => 'warning',
'disabled' => 'danger',
]),
Forms\Components\TextInput::make('swap')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => match ($get('swap_support')) {
'disabled', 'unlimited' => true,
'limited' => false,
})
->label('Swap Memory')
->default(0)
->suffix('MB')
->minValue(-1)
->columnSpan(2)
->inlineLabel()
->required()
->integer(),
]),
Forms\Components\Hidden::make('io')
->helperText('The IO performance relative to other running containers')
->label('Block IO Proportion')
->default(500),
Forms\Components\Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('oom_disabled')
->label('OOM Killer')
->inlineLabel()->inline()
->default(false)
->columnSpan(2)
->options([
false => 'Disabled',
true => 'Enabled',
])
->colors([
false => 'success',
true => 'danger',
]),
Forms\Components\TextInput::make('oom_disabled_hidden')
->hidden(),
]),
Forms\Components\Fieldset::make('Application Feature Limits')
->inlineLabel()
->columnSpan([
'default' => 2,
@@ -640,70 +659,6 @@ class CreateServer extends CreateRecord
->numeric()
->default(0),
]),
Forms\Components\Fieldset::make('Docker Settings')
->columnSpan([
'default' => 2,
'sm' => 4,
'md' => 4,
'lg' => 6,
])
->columns([
'default' => 1,
'sm' => 2,
'md' => 3,
'lg' => 3,
])
->schema([
Forms\Components\Select::make('select_image')
->label('Image Name')
->afterStateUpdated(fn (Forms\Set $set, $state) => $set('image', $state))
->options(function ($state, Forms\Get $get, Forms\Set $set) {
$egg = Egg::query()->find($get('egg_id'));
$images = $egg->docker_images ?? [];
$currentImage = $get('image');
if (!$currentImage && $images) {
$defaultImage = collect($images)->first();
$set('image', $defaultImage);
$set('select_image', $defaultImage);
}
return array_flip($images) + ['ghcr.io/custom-image' => 'Custom Image'];
})
->selectablePlaceholder(false)
->columnSpan(1),
Forms\Components\TextInput::make('image')
->label('Image')
->debounce(500)
->afterStateUpdated(function ($state, Forms\Get $get, Forms\Set $set) {
$egg = Egg::query()->find($get('egg_id'));
$images = $egg->docker_images ?? [];
if (in_array($state, $images)) {
$set('select_image', $state);
} else {
$set('select_image', 'ghcr.io/custom-image');
}
})
->placeholder('Enter a custom Image')
->columnSpan(1),
Forms\Components\KeyValue::make('docker_labels')
->label('Container Labels')
->keyLabel('Title')
->valueLabel('Description')
->columnSpan(3),
Forms\Components\CheckboxList::make('mounts')
->live()
->relationship('mounts')
->options(fn () => $this->node?->mounts->mapWithKeys(fn ($mount) => [$mount->id => $mount->name]) ?? [])
->descriptions(fn () => $this->node?->mounts->mapWithKeys(fn ($mount) => [$mount->id => "$mount->source -> $mount->target"]) ?? [])
->label('Mounts')
->helperText(fn () => $this->node?->mounts->isNotEmpty() ? '' : 'No Mounts exist for this Node')
->columnSpanFull(),
]),
]),
]);
}

File diff suppressed because it is too large Load Diff

View File

@@ -27,15 +27,13 @@ class AllocationsRelationManager extends RelationManager
{
return $table
->recordTitleAttribute('ip')
->recordTitle(fn (Allocation $allocation) => "$allocation->ip:$allocation->port")
->checkIfRecordIsSelectableUsing(fn (Allocation $record) => $record->id !== $this->getOwnerRecord()->allocation_id)
// ->actions
// ->groups
->inverseRelationship('server')
->columns([
Tables\Columns\TextColumn::make('ip_alias')->label('Alias'),
Tables\Columns\TextColumn::make('ip')->label('IP'),
Tables\Columns\TextColumn::make('port')->label('Port'),
Tables\Columns\TextInputColumn::make('ip_alias')->label('Alias'),
Tables\Columns\IconColumn::make('primary')
->icon(fn ($state) => match ($state) {
false => 'tabler-star',
@@ -58,12 +56,8 @@ class AllocationsRelationManager extends RelationManager
->label(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id ? '' : 'Make Primary'),
])
->headerActions([
//TODO Tables\Actions\CreateAction::make()->label('Create Allocation'),
Tables\Actions\AssociateAction::make()
->multiple()
->preloadRecordSelect()
->recordSelectOptionsQuery(fn ($query) => $query->whereBelongsTo($this->getOwnerRecord()->node))
->label('Add Allocation'),
Tables\Actions\CreateAction::make()->label('Create Allocation'),
//Tables\Actions\AssociateAction::make()->label('Add Allocation'),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([

View File

@@ -15,11 +15,6 @@ class UserResource extends Resource
protected static ?string $recordTitleAttribute = 'username';
public static function getNavigationBadge(): ?string
{
return static::getModel()::count() ?: null;
}
public static function getRelations(): array
{
return [
@@ -31,6 +26,7 @@ class UserResource extends Resource
{
return [
'index' => Pages\ListUsers::route('/'),
'create' => Pages\CreateUser::route('/create'),
'edit' => Pages\EditUser::route('/{record}/edit'),
];
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource;
use Filament\Resources\Pages\CreateRecord;
use App\Models\User;
use Filament\Forms;
use Filament\Forms\Components\Section;
use Filament\Forms\Form;
use Illuminate\Support\Facades\Hash;
class CreateUser extends CreateRecord
{
protected static string $resource = UserResource::class;
protected static bool $canCreateAnother = false;
public 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('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),
]);
}
}

View File

@@ -2,12 +2,10 @@
namespace App\Filament\Resources\UserResource\Pages;
use App\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid;
use App\Facades\Activity;
use App\Models\ActivityLog;
use App\Models\ApiKey;
use App\Models\User;
use App\Services\Users\ToggleTwoFactorService;
use App\Services\Users\TwoFactorSetupService;
use chillerlan\QRCode\Common\EccLevel;
use chillerlan\QRCode\Common\Version;
@@ -22,10 +20,8 @@ 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\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Get;
use Filament\Notifications\Notification;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\HtmlString;
@@ -103,26 +99,12 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
if ($this->getUser()->use_totp) {
return [
Placeholder::make('2fa-already-enabled')
->label('Two Factor Authentication is currently enabled!'),
Textarea::make('backup-tokens')
->hidden(fn () => !cache()->get("users.{$this->getUser()->id}.2fa.tokens"))
->rows(10)
->readOnly()
->formatStateUsing(fn () => cache()->get("users.{$this->getUser()->id}.2fa.tokens"))
->helperText('These will not be shown again!')
->label('Backup Tokens:'),
TextInput::make('2fa-disable-code')
->label('Disable 2FA')
->helperText('Enter your current 2FA code to disable Two Factor Authentication'),
Placeholder::make('2FA already enabled!'),
];
}
$setupService = app(TwoFactorSetupService::class);
['image_url_data' => $url, 'secret' => $secret] = cache()->remember(
"users.{$this->getUser()->id}.2fa.state",
now()->addMinutes(5), fn () => $setupService->handle($this->getUser())
);
['image_url_data' => $url] = $setupService->handle($this->getUser());
$options = new QROptions([
'svgLogo' => public_path('pelican.svg'),
@@ -165,19 +147,9 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
Placeholder::make('qr')
->label('Scan QR Code')
->content(fn () => new HtmlString("
<div style='width: 300px; background-color: rgb(24, 24, 27);'>$image</div>
<div style='width: 300px'>$image</div>
"))
->helperText('Setup Key: '. $secret),
TextInput::make('2facode')
->label('Code')
->requiredWith('2fapassword')
->helperText('Scan the QR code above using your two-step authentication app, then enter the code generated.'),
TextInput::make('2fapassword')
->label('Current Password')
->requiredWith('2facode')
->currentPassword()
->password()
->helperText('Enter your current password to verify.'),
->default('asdfasdf'),
];
}),
@@ -186,7 +158,7 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
->schema([
Grid::make('asdf')->columns(5)->schema([
Section::make('Create API Key')->columnSpan(3)->schema([
TextInput::make('description')->required(),
TextInput::make('description'),
TagsInput::make('allowed_ips')
->splitKeys([',', ' ', 'Tab'])
->placeholder('Example: 127.0.0.1 or 192.168.1.1')
@@ -210,9 +182,8 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
$action->success();
}),
]),
Section::make('Keys')->columnSpan(2)->schema([
Section::make('API Keys')->columnSpan(2)->schema([
Repeater::make('keys')
->label('')
->relationship('apiKeys')
->addable(false)
->itemLabel(fn ($state) => $state['identifier'])
@@ -264,43 +235,4 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
),
];
}
protected function handleRecordUpdate($record, $data): \Illuminate\Database\Eloquent\Model
{
if ($token = $data['2facode'] ?? null) {
/** @var ToggleTwoFactorService $service */
$service = resolve(ToggleTwoFactorService::class);
$tokens = $service->handle($record, $token, true);
cache()->set("users.$record->id.2fa.tokens", implode("\n", $tokens), now()->addSeconds(15));
$this->redirectRoute('filament.admin.auth.profile', ['tab' => '-2fa-tab']);
}
if ($token = $data['2fa-disable-code'] ?? null) {
/** @var ToggleTwoFactorService $service */
$service = resolve(ToggleTwoFactorService::class);
$service->handle($record, $token, false);
cache()->forget("users.$record->id.2fa.state");
}
return parent::handleRecordUpdate($record, $data);
}
public function exception($e, $stopPropagation): void
{
if ($e instanceof TwoFactorAuthenticationTokenInvalid) {
Notification::make()
->title('Invalid 2FA Code')
->body($e->getMessage())
->color('danger')
->icon('tabler-2fa')
->danger()
->send();
$stopPropagation();
}
}
}

View File

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

View File

@@ -4,13 +4,10 @@ namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource;
use App\Models\User;
use App\Services\Users\UserCreationService;
use Filament\Actions;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Table;
use Filament\Tables;
use Filament\Forms;
class ListUsers extends ListRecords
{
@@ -76,49 +73,8 @@ class ListUsers extends ListRecords
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make('create')
->label('Create User')
->createAnother(false)
->form([
Forms\Components\Grid::make()
->schema([
Forms\Components\TextInput::make('username')
->alphaNum()
->required()
->maxLength(191),
Forms\Components\TextInput::make('email')
->email()
->required()
->unique()
->maxLength(191),
Forms\Components\TextInput::make('password')
->hintIcon('tabler-question-mark')
->hintIconTooltip('Providing a user password is optional. New user email will prompt users to create a password the first time they login.')
->password(),
Forms\Components\ToggleButtons::make('root_admin')
->label('Administrator (Root)')
->options([
false => 'No',
true => 'Admin',
])
->colors([
false => 'primary',
true => 'danger',
])
->inline()
->required()
->default(false),
]),
])
->successRedirectUrl(route('filament.admin.resources.users.index'))
->action(function (array $data) {
resolve(UserCreationService::class)->handle($data);
Notification::make()->title('User Created!')->success()->send();
return redirect()->route('filament.admin.resources.users.index');
}),
Actions\CreateAction::make()
->label('Create User'),
];
}
}

View File

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

View File

@@ -56,7 +56,7 @@ class NodeAutoDeployController extends Controller
return new JsonResponse([
'node' => $node->id,
'token' => $key->identifier . $key->token,
'token' => $key->identifier . decrypt($key->token),
]);
}
}

View File

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

View File

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

View File

@@ -53,6 +53,7 @@ class CreateServerController extends Controller
* @throws \Illuminate\Validation\ValidationException
* @throws \App\Exceptions\DisplayException
* @throws \App\Exceptions\Service\Deployment\NoViableAllocationException
* @throws \App\Exceptions\Service\Deployment\NoViableNodeException
* @throws \Throwable
*/
public function store(ServerFormRequest $request): RedirectResponse

View File

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

View File

@@ -2,12 +2,21 @@
namespace App\Http\Controllers\Admin\Servers;
use App\Http\Controllers\Controller;
use App\Models\Server;
use App\Services\Servers\TransferServerService;
use Illuminate\Http\RedirectResponse;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use App\Models\Allocation;
use App\Models\Node;
use Carbon\CarbonImmutable;
use GuzzleHttp\Exception\TransferException;
use Illuminate\Http\Request;
use App\Models\Server;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Http;
use Lcobucci\JWT\Token\Plain;
use Prologue\Alerts\AlertsMessageBag;
use App\Models\ServerTransfer;
use Illuminate\Database\ConnectionInterface;
use App\Http\Controllers\Controller;
use App\Services\Nodes\NodeJWTService;
class ServerTransferController extends Controller
{
@@ -16,10 +25,30 @@ class ServerTransferController extends Controller
*/
public function __construct(
private AlertsMessageBag $alert,
private TransferServerService $transferServerService,
private ConnectionInterface $connection,
private NodeJWTService $nodeJWTService,
) {
}
private function notify(Server $server, Plain $token): void
{
try {
Http::daemon($server->node)->post('/api/transfer', [
'json' => [
'server_id' => $server->uuid,
'url' => $server->node->getConnectionAddress() . "/api/servers/$server->uuid/archive",
'token' => 'Bearer ' . $token->toString(),
'server' => [
'uuid' => $server->uuid,
'start_on_completion' => false,
],
],
])->toPsrResponse();
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
}
/**
* Starts a transfer of a server to a new node.
*
@@ -33,12 +62,85 @@ class ServerTransferController extends Controller
'allocation_additional' => 'nullable',
]);
if ($this->transferServerService->handle($server, $validatedData)) {
$this->alert->success(trans('admin/server.alerts.transfer_started'))->flash();
} else {
$node_id = $validatedData['node_id'];
$allocation_id = intval($validatedData['allocation_id']);
$additional_allocations = array_map('intval', $validatedData['allocation_additional'] ?? []);
// Check if the node is viable for the transfer.
$node = Node::query()
->select(['nodes.id', 'nodes.fqdn', 'nodes.scheme', 'nodes.daemon_token', 'nodes.daemon_listen', 'nodes.memory', 'nodes.disk', 'nodes.memory_overallocate', 'nodes.disk_overallocate'])
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk')
->leftJoin('servers', 'servers.node_id', '=', 'nodes.id')
->where('nodes.id', $node_id)
->first();
if (!$node->isViable($server->memory, $server->disk)) {
$this->alert->danger(trans('admin/server.alerts.transfer_not_viable'))->flash();
return redirect()->route('admin.servers.view.manage', $server->id);
}
$server->validateTransferState();
$this->connection->transaction(function () use ($server, $node_id, $allocation_id, $additional_allocations) {
// Create a new ServerTransfer entry.
$transfer = new ServerTransfer();
$transfer->server_id = $server->id;
$transfer->old_node = $server->node_id;
$transfer->new_node = $node_id;
$transfer->old_allocation = $server->allocation_id;
$transfer->new_allocation = $allocation_id;
$transfer->old_additional_allocations = $server->allocations->where('id', '!=', $server->allocation_id)->pluck('id')->all();
$transfer->new_additional_allocations = $additional_allocations;
$transfer->save();
// Add the allocations to the server, so they cannot be automatically assigned while the transfer is in progress.
$this->assignAllocationsToServer($server, $node_id, $allocation_id, $additional_allocations);
// Generate a token for the destination node that the source node can use to authenticate with.
$token = $this->nodeJWTService
->setExpiresAt(CarbonImmutable::now()->addMinutes(15))
->setSubject($server->uuid)
->handle($transfer->newNode, $server->uuid, 'sha256');
// Notify the source node of the pending outgoing transfer.
$this->notify($server, $token);
return $transfer;
});
$this->alert->success(trans('admin/server.alerts.transfer_started'))->flash();
return redirect()->route('admin.servers.view.manage', $server->id);
}
/**
* Assigns the specified allocations to the specified server.
*/
private function assignAllocationsToServer(Server $server, int $node_id, int $allocation_id, array $additional_allocations)
{
$allocations = $additional_allocations;
$allocations[] = $allocation_id;
$node = Node::query()->findOrFail($node_id);
$unassigned = $node->allocations()
->whereNull('server_id')
->pluck('id')
->toArray();
$updateIds = [];
foreach ($allocations as $allocation) {
if (!in_array($allocation, $unassigned)) {
continue;
}
$updateIds[] = $allocation;
}
if (!empty($updateIds)) {
Allocation::query()->whereIn('id', $updateIds)->update(['server_id' => $server->id]);
}
}
}

View File

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

View File

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

View File

@@ -1,100 +0,0 @@
<?php
namespace App\Http\Controllers\Api\Application\DatabaseHosts;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use App\Models\DatabaseHost;
use Spatie\QueryBuilder\QueryBuilder;
use App\Services\Databases\Hosts\HostUpdateService;
use App\Services\Databases\Hosts\HostCreationService;
use App\Transformers\Api\Application\DatabaseHostTransformer;
use App\Http\Controllers\Api\Application\ApplicationApiController;
use App\Http\Requests\Api\Application\DatabaseHosts\GetDatabaseHostRequest;
use App\Http\Requests\Api\Application\DatabaseHosts\StoreDatabaseHostRequest;
use App\Http\Requests\Api\Application\DatabaseHosts\DeleteDatabaseHostRequest;
use App\Http\Requests\Api\Application\DatabaseHosts\UpdateDatabaseHostRequest;
class DatabaseHostController extends ApplicationApiController
{
/**
* DatabaseHostController constructor.
*/
public function __construct(
private HostCreationService $creationService,
private HostUpdateService $updateService
) {
parent::__construct();
}
/**
* Return all the database hosts currently registered on the Panel.
*/
public function index(GetDatabaseHostRequest $request): array
{
$databases = QueryBuilder::for(DatabaseHost::query())
->allowedFilters(['name', 'host'])
->allowedSorts(['id', 'name', 'host'])
->paginate($request->query('per_page') ?? 10);
return $this->fractal->collection($databases)
->transformWith($this->getTransformer(DatabaseHostTransformer::class))
->toArray();
}
/**
* Return a single database host.
*/
public function view(GetDatabaseHostRequest $request, DatabaseHost $databaseHost): array
{
return $this->fractal->item($databaseHost)
->transformWith($this->getTransformer(DatabaseHostTransformer::class))
->toArray();
}
/**
* Store a new database host on the Panel and return an HTTP/201 response code with the
* new database host attached.
*
* @throws \Throwable
*/
public function store(StoreDatabaseHostRequest $request): JsonResponse
{
$databaseHost = $this->creationService->handle($request->validated());
return $this->fractal->item($databaseHost)
->transformWith($this->getTransformer(DatabaseHostTransformer::class))
->addMeta([
'resource' => route('api.application.databases.view', [
'database_host' => $databaseHost->id,
]),
])
->respond(201);
}
/**
* Update a database host on the Panel and return the updated record to the user.
*
* @throws \Throwable
*/
public function update(UpdateDatabaseHostRequest $request, DatabaseHost $databaseHost): array
{
$databaseHost = $this->updateService->handle($databaseHost->id, $request->validated());
return $this->fractal->item($databaseHost)
->transformWith($this->getTransformer(DatabaseHostTransformer::class))
->toArray();
}
/**
* Delete a database host from the Panel.
*
* @throws \Exception
*/
public function delete(DeleteDatabaseHostRequest $request, DatabaseHost $databaseHost): Response
{
$databaseHost->delete();
return $this->returnNoContent();
}
}

View File

@@ -1,165 +0,0 @@
<?php
namespace App\Http\Controllers\Api\Application\Mounts;
use Ramsey\Uuid\Uuid;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Contracts\Translation\Translator;
use Spatie\QueryBuilder\QueryBuilder;
use App\Models\Mount;
use App\Http\Controllers\Api\Application\ApplicationApiController;
use App\Transformers\Api\Application\MountTransformer;
use App\Http\Requests\Api\Application\Mounts\GetMountRequest;
use App\Http\Requests\Api\Application\Mounts\StoreMountRequest;
use App\Http\Requests\Api\Application\Mounts\DeleteMountRequest;
use App\Http\Requests\Api\Application\Mounts\UpdateMountRequest;
use App\Exceptions\Service\HasActiveServersException;
class MountController extends ApplicationApiController
{
/**
* MountController constructor.
*/
public function __construct(
protected Translator $translator
) {
parent::__construct();
}
/**
* Return all the mounts currently available on the Panel.
*/
public function index(GetMountRequest $request): array
{
$mounts = QueryBuilder::for(Mount::query())
->allowedFilters(['uuid', 'name'])
->allowedSorts(['id', 'uuid'])
->paginate($request->query('per_page') ?? 50);
return $this->fractal->collection($mounts)
->transformWith($this->getTransformer(MountTransformer::class))
->toArray();
}
/**
* Return data for a single instance of a mount.
*/
public function view(GetMountRequest $request, Mount $mount): array
{
return $this->fractal->item($mount)
->transformWith($this->getTransformer(MountTransformer::class))
->toArray();
}
/**
* Create a new mount on the Panel. Returns the created mount and an HTTP/201
* status response on success.
*
* @throws \App\Exceptions\Model\DataValidationException
*/
public function store(StoreMountRequest $request): JsonResponse
{
$model = (new Mount())->fill($request->validated());
$model->forceFill(['uuid' => Uuid::uuid4()->toString()]);
$model->saveOrFail();
$mount = $model->fresh();
return $this->fractal->item($mount)
->transformWith($this->getTransformer(MountTransformer::class))
->addMeta([
'resource' => route('api.application.mounts.view', [
'mount' => $mount->id,
]),
])
->respond(201);
}
/**
* Update an existing mount on the Panel.
*
* @throws \Throwable
*/
public function update(UpdateMountRequest $request, Mount $mount): array
{
$mount->forceFill($request->validated())->save();
return $this->fractal->item($mount)
->transformWith($this->getTransformer(MountTransformer::class))
->toArray();
}
/**
* Deletes a given mount from the Panel as long as there are no servers
* currently attached to it.
*
* @throws \App\Exceptions\Service\HasActiveServersException
*/
public function delete(DeleteMountRequest $request, Mount $mount): JsonResponse
{
if ($mount->servers()->count() > 0) {
throw new HasActiveServersException($this->translator->get('exceptions.mount.servers_attached'));
}
$mount->delete();
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
}
/**
* Adds eggs to the mount's many-to-many relation.
*/
public function addEggs(Request $request, Mount $mount): array
{
$validatedData = $request->validate([
'eggs' => 'required|exists:eggs,id',
]);
$eggs = $validatedData['eggs'] ?? [];
if (count($eggs) > 0) {
$mount->eggs()->attach($eggs);
}
return $this->fractal->item($mount)
->transformWith($this->getTransformer(MountTransformer::class))
->toArray();
}
/**
* Adds nodes to the mount's many-to-many relation.
*/
public function addNodes(Request $request, Mount $mount): array
{
$data = $request->validate(['nodes' => 'required|exists:nodes,id']);
$nodes = $data['nodes'] ?? [];
if (count($nodes) > 0) {
$mount->nodes()->attach($nodes);
}
return $this->fractal->item($mount)
->transformWith($this->getTransformer(MountTransformer::class))
->toArray();
}
/**
* Deletes an egg from the mount's many-to-many relation.
*/
public function deleteEgg(Mount $mount, int $egg_id): JsonResponse
{
$mount->eggs()->detach($egg_id);
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
}
/**
* Deletes a node from the mount's many-to-many relation.
*/
public function deleteNode(Mount $mount, int $node_id): JsonResponse
{
$mount->nodes()->detach($node_id);
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
}
}

View File

@@ -36,7 +36,7 @@ class NodeController extends ApplicationApiController
{
$nodes = QueryBuilder::for(Node::query())
->allowedFilters(['uuid', 'name', 'fqdn', 'daemon_token_id'])
->allowedSorts(['id', 'uuid', 'memory', 'disk', 'cpu'])
->allowedSorts(['id', 'uuid', 'memory', 'disk'])
->paginate($request->query('per_page') ?? 50);
return $this->fractal->collection($nodes)

View File

@@ -9,6 +9,9 @@ use App\Http\Requests\Api\Application\Nodes\GetDeployableNodesRequest;
class NodeDeploymentController extends ApplicationApiController
{
/**
* NodeDeploymentController constructor.
*/
public function __construct(private FindViableNodesService $viableNodesService)
{
parent::__construct();
@@ -18,17 +21,16 @@ class NodeDeploymentController extends ApplicationApiController
* Finds any nodes that are available using the given deployment criteria. This works
* similarly to the server creation process, but allows you to pass the deployment object
* to this endpoint and get back a list of all Nodes satisfying the requirements.
*
* @throws \App\Exceptions\Service\Deployment\NoViableNodeException
*/
public function __invoke(GetDeployableNodesRequest $request): array
{
$data = $request->validated();
$nodes = $this->viableNodesService->handle(
$data['memory'] ?? 0,
$data['disk'] ?? 0,
$data['cpu'] ?? 0,
$data['tags'] ?? $data['location_ids'] ?? [],
);
$nodes = $this->viableNodesService
->setMemory($data['memory'])
->setDisk($data['disk'])
->handle((int) $request->query('per_page'), (int) $request->query('page'));
return $this->fractal->collection($nodes)
->transformWith($this->getTransformer(NodeTransformer::class))

View File

@@ -50,6 +50,7 @@ class ServerController extends ApplicationApiController
* @throws \App\Exceptions\DisplayException
* @throws \App\Exceptions\Model\DataValidationException
* @throws \App\Exceptions\Service\Deployment\NoViableAllocationException
* @throws \App\Exceptions\Service\Deployment\NoViableNodeException
*/
public function store(StoreServerRequest $request): JsonResponse
{

View File

@@ -2,14 +2,12 @@
namespace App\Http\Controllers\Api\Application\Servers;
use App\Http\Controllers\Api\Application\ApplicationApiController;
use App\Http\Requests\Api\Application\Servers\ServerWriteRequest;
use App\Models\Server;
use App\Repositories\Daemon\DaemonServerRepository;
use App\Services\Servers\ReinstallServerService;
use App\Services\Servers\SuspensionService;
use App\Services\Servers\TransferServerService;
use Illuminate\Http\Response;
use App\Models\Server;
use App\Services\Servers\SuspensionService;
use App\Services\Servers\ReinstallServerService;
use App\Http\Requests\Api\Application\Servers\ServerWriteRequest;
use App\Http\Controllers\Api\Application\ApplicationApiController;
class ServerManagementController extends ApplicationApiController
{
@@ -18,9 +16,7 @@ class ServerManagementController extends ApplicationApiController
*/
public function __construct(
private ReinstallServerService $reinstallServerService,
private SuspensionService $suspensionService,
private TransferServerService $transferServerService,
private DaemonServerRepository $daemonServerRepository,
private SuspensionService $suspensionService
) {
parent::__construct();
}
@@ -61,44 +57,4 @@ class ServerManagementController extends ApplicationApiController
return $this->returnNoContent();
}
/**
* Starts a transfer of a server to a new node.
*/
public function startTransfer(ServerWriteRequest $request, Server $server): Response
{
$validatedData = $request->validate([
'node_id' => 'required|exists:nodes,id',
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
'allocation_additional' => 'nullable',
]);
if ($this->transferServerService->handle($server, $validatedData)) {
// Transfer started
$this->returnNoContent();
} else {
// Node was not viable
return new Response('', Response::HTTP_NOT_ACCEPTABLE);
}
}
/**
* Cancels a transfer of a server to a new node.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
*/
public function cancelTransfer(ServerWriteRequest $request, Server $server): Response
{
if (!$transfer = $server->transfer) {
// Server is not transferring
return new Response('', Response::HTTP_NOT_ACCEPTABLE);
}
$transfer->successful = true;
$transfer->save();
$this->daemonServerRepository->setServer($server)->cancelTransfer();
return $this->returnNoContent();
}
}

View File

@@ -11,7 +11,6 @@ use App\Extensions\Backups\BackupManager;
use App\Extensions\Filesystem\S3Filesystem;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use App\Exceptions\Http\HttpForbiddenException;
class BackupRemoteUploadController extends Controller
{
@@ -33,32 +32,18 @@ class BackupRemoteUploadController extends Controller
*/
public function __invoke(Request $request, string $backup): JsonResponse
{
// Get the node associated with the request.
/** @var \App\Models\Node $node */
$node = $request->attributes->get('node');
// Get the size query parameter.
$size = (int) $request->query('size');
if (empty($size)) {
throw new BadRequestHttpException('A non-empty "size" query parameter must be provided.');
}
/** @var \App\Models\Backup $model */
$model = Backup::query()
->where('uuid', $backup)
->firstOrFail();
// Check that the backup is "owned" by the node making the request. This avoids other nodes
// from messing with backups that they don't own.
/** @var \App\Models\Server $server */
$server = $model->server;
if ($server->node_id !== $node->id) {
throw new HttpForbiddenException('You do not have permission to access that backup.');
}
/** @var \App\Models\Backup $backup */
$backup = Backup::query()->where('uuid', $backup)->firstOrFail();
// Prevent backups that have already been completed from trying to
// be uploaded again.
if (!is_null($model->completed_at)) {
if (!is_null($backup->completed_at)) {
throw new ConflictHttpException('This backup is already in a completed state.');
}
@@ -69,7 +54,7 @@ class BackupRemoteUploadController extends Controller
}
// The path where backup will be uploaded to
$path = sprintf('%s/%s.tar.gz', $model->server->uuid, $model->uuid);
$path = sprintf('%s/%s.tar.gz', $backup->server->uuid, $backup->uuid);
// Get the S3 client
$client = $adapter->getClient();
@@ -107,7 +92,7 @@ class BackupRemoteUploadController extends Controller
}
// Set the upload_id on the backup in the database.
$model->update(['upload_id' => $params['UploadId']]);
$backup->update(['upload_id' => $params['UploadId']]);
return new JsonResponse([
'parts' => $parts,

View File

@@ -13,7 +13,6 @@ use App\Extensions\Backups\BackupManager;
use App\Extensions\Filesystem\S3Filesystem;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use App\Http\Requests\Api\Remote\ReportBackupCompleteRequest;
use App\Exceptions\Http\HttpForbiddenException;
class BackupStatusController extends Controller
{
@@ -31,22 +30,8 @@ class BackupStatusController extends Controller
*/
public function index(ReportBackupCompleteRequest $request, string $backup): JsonResponse
{
// Get the node associated with the request.
/** @var \App\Models\Node $node */
$node = $request->attributes->get('node');
/** @var \App\Models\Backup $model */
$model = Backup::query()
->where('uuid', $backup)
->firstOrFail();
// Check that the backup is "owned" by the node making the request. This avoids other nodes
// from messing with backups that they don't own.
/** @var \App\Models\Server $server */
$server = $model->server;
if ($server->node_id !== $node->id) {
throw new HttpForbiddenException('You do not have permission to access that backup.');
}
$model = Backup::query()->where('uuid', $backup)->firstOrFail();
if ($model->is_successful) {
throw new BadRequestHttpException('Cannot update the status of a backup that is already marked as completed.');

View File

@@ -65,7 +65,9 @@ class LoginCheckpointController extends AbstractLoginController
return $this->sendLoginResponse($user, $request);
}
} else {
if ($this->google2FA->verifyKey($user->totp_secret, (string) $request->input('authentication_code'), config('panel.auth.2fa.window'))) {
$decrypted = decrypt($user->totp_secret);
if ($this->google2FA->verifyKey($decrypted, (string) $request->input('authentication_code'), config('panel.auth.2fa.window'))) {
Event::dispatch(new ProvidedAuthenticationToken($user));
return $this->sendLoginResponse($user, $request);

View File

@@ -41,7 +41,7 @@ class DaemonAuthenticate
/** @var Node $node */
$node = Node::query()->where('daemon_token_id', $parts[0])->firstOrFail();
if (hash_equals((string) $node->daemon_token, $parts[1])) {
if (hash_equals((string) decrypt($node->daemon_token), $parts[1])) {
$request->attributes->set('node', $node);
return $next($request);

View File

@@ -1,13 +0,0 @@
<?php
namespace App\Http\Requests\Api\Application\DatabaseHosts;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
class DeleteDatabaseHostRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_DATABASE_HOSTS;
protected int $permission = AdminAcl::WRITE;
}

View File

@@ -1,13 +0,0 @@
<?php
namespace App\Http\Requests\Api\Application\DatabaseHosts;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
class GetDatabaseHostRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_DATABASE_HOSTS;
protected int $permission = AdminAcl::READ;
}

View File

@@ -1,19 +0,0 @@
<?php
namespace App\Http\Requests\Api\Application\DatabaseHosts;
use App\Models\DatabaseHost;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
class StoreDatabaseHostRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_DATABASE_HOSTS;
protected int $permission = AdminAcl::WRITE;
public function rules(array $rules = null): array
{
return $rules ?? DatabaseHost::getRules();
}
}

View File

@@ -1,16 +0,0 @@
<?php
namespace App\Http\Requests\Api\Application\DatabaseHosts;
use App\Models\DatabaseHost;
class UpdateDatabaseHostRequest extends StoreDatabaseHostRequest
{
public function rules(array $rules = null): array
{
/** @var DatabaseHost $databaseHost */
$databaseHost = $this->route()->parameter('database_host');
return $rules ?? DatabaseHost::getRulesForUpdate($databaseHost->id);
}
}

View File

@@ -1,13 +0,0 @@
<?php
namespace App\Http\Requests\Api\Application\Mounts;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
class DeleteMountRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_MOUNTS;
protected int $permission = AdminAcl::WRITE;
}

View File

@@ -1,13 +0,0 @@
<?php
namespace App\Http\Requests\Api\Application\Mounts;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
class GetMountRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_MOUNTS;
protected int $permission = AdminAcl::READ;
}

View File

@@ -1,13 +0,0 @@
<?php
namespace App\Http\Requests\Api\Application\Mounts;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
class StoreMountRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_MOUNTS;
protected int $permission = AdminAcl::WRITE;
}

View File

@@ -1,20 +0,0 @@
<?php
namespace App\Http\Requests\Api\Application\Mounts;
use App\Models\Mount;
class UpdateMountRequest extends StoreMountRequest
{
/**
* Apply validation rules to this request. Uses the parent class rules()
* function but passes in the rules for updating rather than creating.
*/
public function rules(array $rules = null): array
{
/** @var Mount $mount */
$mount = $this->route()->parameter('mount');
return parent::rules(Mount::getRulesForUpdate($mount->id));
}
}

View File

@@ -10,11 +10,6 @@ class GetDeployableNodesRequest extends GetNodesRequest
'page' => 'integer',
'memory' => 'required|integer|min:0',
'disk' => 'required|integer|min:0',
'cpu' => 'sometimes|integer|min:0',
'tags' => 'sometimes|array',
/** @deprecated use tags instead */
'location_ids' => 'sometimes|array',
];
}
}

View File

@@ -28,14 +28,13 @@ class StoreNodeRequest extends ApplicationApiRequest
'memory_overallocate',
'disk',
'disk_overallocate',
'cpu',
'cpu_overallocate',
'upload_size',
'daemon_listen',
'daemon_sftp',
'daemon_sftp_alias',
'daemon_base',
])->mapWithKeys(function ($value, $key) {
$key = ($key === 'daemon_sftp') ? 'daemon_sftp' : $key;
return [snake_case($key) => $value];
})->toArray();
}
@@ -59,8 +58,12 @@ class StoreNodeRequest extends ApplicationApiRequest
public function validated($key = null, $default = null): array
{
$response = parent::validated();
$response['daemon_listen'] = $response['daemon_listen'];
$response['daemon_sftp'] = $response['daemon_sftp'];
$response['daemon_base'] = $response['daemon_base'] ?? (new Node())->getAttribute('daemon_base');
unset($response['daemon_base'], $response['daemon_listen'], $response['daemon_sftp']);
return $response;
}
}

View File

@@ -32,7 +32,7 @@ class StoreServerRequest extends ApplicationApiRequest
'startup' => $rules['startup'],
'environment' => 'present|array',
'skip_scripts' => 'sometimes|boolean',
'oom_killer' => 'sometimes|boolean',
'oom_disabled' => 'sometimes|boolean',
// Resource limitations
'limits' => 'required|array',
@@ -56,10 +56,11 @@ class StoreServerRequest extends ApplicationApiRequest
// Automatic deployment rules
'deploy' => 'sometimes|required|array',
'deploy.locations' => 'array',
'deploy.locations.*' => 'required_with:deploy.locations,integer|min:1',
'deploy.locations.*' => 'integer|min:1',
'deploy.dedicated_ip' => 'required_with:deploy,boolean',
'deploy.port_range' => 'array',
'deploy.port_range.*' => 'string',
'start_on_completion' => 'sometimes|boolean',
];
}
@@ -93,7 +94,7 @@ class StoreServerRequest extends ApplicationApiRequest
'database_limit' => array_get($data, 'feature_limits.databases'),
'allocation_limit' => array_get($data, 'feature_limits.allocations'),
'backup_limit' => array_get($data, 'feature_limits.backups'),
'oom_killer' => array_get($data, 'oom_killer'),
'oom_disabled' => array_get($data, 'oom_disabled'),
];
}
@@ -122,15 +123,6 @@ class StoreServerRequest extends ApplicationApiRequest
return !$input->deploy;
});
/** @deprecated use tags instead */
$validator->sometimes('deploy.locations', 'present', function ($input) {
return $input->deploy;
});
$validator->sometimes('deploy.tags', 'present', function ($input) {
return $input->deploy;
});
$validator->sometimes('deploy.port_range', 'present', function ($input) {
return $input->deploy;
});
@@ -147,7 +139,6 @@ class StoreServerRequest extends ApplicationApiRequest
$object = new DeploymentObject();
$object->setDedicated($this->input('deploy.dedicated_ip', false));
$object->setTags($this->input('deploy.tags', $this->input('deploy.locations', [])));
$object->setPorts($this->input('deploy.port_range', []));
return $object;

View File

@@ -16,7 +16,7 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest
return [
'allocation' => $rules['allocation_id'],
'oom_killer' => $rules['oom_killer'],
'oom_disabled' => $rules['oom_disabled'],
'limits' => 'sometimes|array',
'limits.memory' => $this->requiredToOptional('memory', $rules['memory'], true),

View File

@@ -19,11 +19,9 @@ class StoreScheduleRequest extends ViewScheduleRequest
return [
'name' => $rules['name'],
'is_active' => array_merge(['filled'], $rules['is_active']),
'only_when_online' => $rules['only_when_online'],
'minute' => $rules['cron_minute'],
'hour' => $rules['cron_hour'],
'day_of_month' => $rules['cron_day_of_month'],
'month' => $rules['cron_month'],
'day_of_week' => $rules['cron_day_of_week'],
];
}

View File

@@ -22,6 +22,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property bool $has_alias
* @property \App\Models\Server|null $server
* @property \App\Models\Node $node
* @property string $hashid
*
* @method static \Database\Factories\AllocationFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Builder|Allocation newModelQuery()
@@ -87,6 +88,14 @@ class Allocation extends Model
return $this->getKeyName();
}
/**
* Return a hashid encoded string to represent the ID of the allocation.
*/
public function getHashidAttribute(): string
{
return app()->make('hashids')->encode($this->id);
}
/**
* Accessor to automatically provide the IP alias if defined.
*/

View File

@@ -28,7 +28,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property int $r_eggs
* @property int $r_database_hosts
* @property int $r_server_databases
* @property int $r_mounts
* @property \App\Models\User $tokenable
* @property \App\Models\User $user
*
@@ -84,7 +83,7 @@ class ApiKey extends Model
*/
public const KEY_LENGTH = 32;
public const RESOURCES = ['servers', 'nodes', 'allocations', 'users', 'eggs', 'database_hosts', 'server_databases', 'mounts'];
public const RESOURCES = ['servers', 'nodes', 'allocations', 'users', 'eggs', 'database_hosts', 'server_databases'];
/**
* The table associated with the model.
@@ -110,7 +109,6 @@ class ApiKey extends Model
'r_' . AdminAcl::RESOURCE_EGGS,
'r_' . AdminAcl::RESOURCE_NODES,
'r_' . AdminAcl::RESOURCE_SERVERS,
'r_' . AdminAcl::RESOURCE_MOUNTS,
];
/**
@@ -139,7 +137,6 @@ class ApiKey extends Model
'r_' . AdminAcl::RESOURCE_EGGS => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_NODES => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_SERVERS => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_MOUNTS => 'integer|min:0|max:3',
];
protected function casts(): array
@@ -149,7 +146,6 @@ class ApiKey extends Model
'user_id' => 'int',
'last_used_at' => 'datetime',
'expires_at' => 'datetime',
'token' => 'encrypted',
self::CREATED_AT => 'datetime',
self::UPDATED_AT => 'datetime',
'r_' . AdminAcl::RESOURCE_USERS => 'int',
@@ -159,7 +155,6 @@ class ApiKey extends Model
'r_' . AdminAcl::RESOURCE_EGGS => 'int',
'r_' . AdminAcl::RESOURCE_NODES => 'int',
'r_' . AdminAcl::RESOURCE_SERVERS => 'int',
'r_' . AdminAcl::RESOURCE_MOUNTS => 'int',
];
}
@@ -189,7 +184,7 @@ class ApiKey extends Model
$identifier = substr($token, 0, self::IDENTIFIER_LENGTH);
$model = static::where('identifier', $identifier)->first();
if (!is_null($model) && $model->token === substr($token, strlen($identifier))) {
if (!is_null($model) && decrypt($model->token) === substr($token, strlen($identifier))) {
return $model;
}

View File

@@ -2,7 +2,9 @@
namespace App\Models;
use Illuminate\Container\Container;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use App\Contracts\Extensions\HashidsInterface;
use Illuminate\Support\Facades\DB;
/**
@@ -62,7 +64,6 @@ class Database extends Model
'server_id' => 'integer',
'database_host_id' => 'integer',
'max_connections' => 'integer',
'password' => 'encrypted',
];
}
@@ -71,6 +72,26 @@ class Database extends Model
return $this->getKeyName();
}
/**
* Resolves the database using the ID by checking if the value provided is a HashID
* string value, or just the ID to the database itself.
*
* @param mixed $value
* @param string|null $field
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function resolveRouteBinding($value, $field = null): ?\Illuminate\Database\Eloquent\Model
{
if (is_scalar($value) && ($field ?? $this->getRouteKeyName()) === 'id') {
$value = ctype_digit((string) $value)
? $value
: Container::getInstance()->make(HashidsInterface::class)->decodeFirst($value);
}
return $this->where($field ?? $this->getRouteKeyName(), $value)->firstOrFail();
}
/**
* Gets the host database server associated with a database.
*/

View File

@@ -60,7 +60,6 @@ class DatabaseHost extends Model
'id' => 'integer',
'max_databases' => 'integer',
'node_id' => 'integer',
'password' => 'encrypted',
'created_at' => 'immutable_datetime',
'updated_at' => 'immutable_datetime',
];

View File

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

View File

@@ -26,14 +26,11 @@ use Illuminate\Database\Eloquent\Relations\HasManyThrough;
* @property int $memory_overallocate
* @property int $disk
* @property int $disk_overallocate
* @property int $cpu
* @property int $cpu_overallocate
* @property int $upload_size
* @property string $daemon_token_id
* @property string $daemon_token
* @property int $daemon_listen
* @property int $daemon_sftp
* @property string|null $daemon_sftp_alias
* @property string $daemon_base
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
@@ -64,6 +61,9 @@ class Node extends Model
*/
protected $hidden = ['daemon_token_id', 'daemon_token'];
public int $sum_memory;
public int $sum_disk;
/**
* Fields that are mass assignable.
*/
@@ -71,9 +71,8 @@ class Node extends Model
'public', 'name',
'fqdn', 'scheme', 'behind_proxy',
'memory', 'memory_overallocate', 'disk',
'disk_overallocate', 'cpu', 'cpu_overallocate',
'upload_size', 'daemon_base',
'daemon_sftp', 'daemon_sftp_alias', 'daemon_listen',
'disk_overallocate', 'upload_size', 'daemon_base',
'daemon_sftp', 'daemon_listen',
'description', 'maintenance_mode',
];
@@ -88,11 +87,8 @@ class Node extends Model
'memory_overallocate' => 'required|numeric|min:-1',
'disk' => 'required|numeric|min:0',
'disk_overallocate' => 'required|numeric|min:-1',
'cpu' => 'required|numeric|min:0',
'cpu_overallocate' => 'required|numeric|min:-1',
'daemon_base' => 'sometimes|required|regex:/^([\/][\d\w.\-\/]+)$/',
'daemon_sftp' => 'required|numeric|between:1,65535',
'daemon_sftp_alias' => 'nullable|string',
'daemon_listen' => 'required|numeric|between:1,65535',
'maintenance_mode' => 'boolean',
'upload_size' => 'int|between:1,1024',
@@ -108,8 +104,6 @@ class Node extends Model
'memory_overallocate' => 0,
'disk' => 0,
'disk_overallocate' => 0,
'cpu' => 0,
'cpu_overallocate' => 0,
'daemon_base' => '/var/lib/pelican/volumes',
'daemon_sftp' => 2022,
'daemon_listen' => 8080,
@@ -122,10 +116,8 @@ class Node extends Model
return [
'memory' => 'integer',
'disk' => 'integer',
'cpu' => 'integer',
'daemon_listen' => 'integer',
'daemon_sftp' => 'integer',
'daemon_token' => 'encrypted',
'behind_proxy' => 'boolean',
'public' => 'boolean',
'maintenance_mode' => 'boolean',
@@ -142,7 +134,7 @@ class Node extends Model
{
static::creating(function (self $node) {
$node->uuid = Str::uuid();
$node->daemon_token = Str::random(self::DAEMON_TOKEN_LENGTH);
$node->daemon_token = encrypt(Str::random(self::DAEMON_TOKEN_LENGTH));
$node->daemon_token_id = Str::random(self::DAEMON_TOKEN_ID_LENGTH);
return true;
@@ -170,7 +162,7 @@ class Node extends Model
'debug' => false,
'uuid' => $this->uuid,
'token_id' => $this->daemon_token_id,
'token' => $this->daemon_token,
'token' => decrypt($this->daemon_token),
'api' => [
'host' => '0.0.0.0',
'port' => $this->daemon_listen,
@@ -208,6 +200,16 @@ class Node extends Model
return json_encode($this->getConfiguration(), $pretty ? JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT : JSON_UNESCAPED_SLASHES);
}
/**
* Helper function to return the decrypted key for a node.
*/
public function getDecryptedKey(): string
{
return (string) decrypt(
$this->daemon_token
);
}
public function isUnderMaintenance(): bool
{
return $this->maintenance_mode;
@@ -237,30 +239,12 @@ class Node extends Model
/**
* Returns a boolean if the node is viable for an additional server to be placed on it.
*/
public function isViable(int $memory, int $disk, int $cpu): bool
public function isViable(int $memory, int $disk): bool
{
if ($this->memory_overallocate >= 0) {
$memoryLimit = $this->memory * (1 + ($this->memory_overallocate / 100));
if ($this->servers_sum_memory + $memory > $memoryLimit) {
return false;
}
}
$memoryLimit = $this->memory * (1 + ($this->memory_overallocate / 100));
$diskLimit = $this->disk * (1 + ($this->disk_overallocate / 100));
if ($this->disk_overallocate >= 0) {
$diskLimit = $this->disk * (1 + ($this->disk_overallocate / 100));
if ($this->servers_sum_disk + $disk > $diskLimit) {
return false;
}
}
if ($this->cpu_overallocate >= 0) {
$cpuLimit = $this->cpu * (1 + ($this->cpu_overallocate / 100));
if ($this->servers_sum_cpu + $cpu > $cpuLimit) {
return false;
}
}
return true;
return ($this->sum_memory + $memory) <= $memoryLimit && ($this->sum_disk + $disk) <= $diskLimit;
}
public static function getForServerCreation()

View File

@@ -6,8 +6,6 @@ class DeploymentObject
{
private bool $dedicated = false;
private array $tags = [];
private array $ports = [];
public function isDedicated(): bool
@@ -33,17 +31,4 @@ class DeploymentObject
return $this;
}
public function getTags(): array
{
return $this->tags;
}
public function setTags(array $tags): self
{
$this->tags = $tags;
return $this;
}
}

View File

@@ -4,8 +4,10 @@ namespace App\Models;
use Cron\CronExpression;
use Carbon\CarbonImmutable;
use Illuminate\Container\Container;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use App\Contracts\Extensions\HashidsInterface;
/**
* @property int $id
@@ -23,6 +25,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property \Carbon\Carbon|null $next_run_at
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property string $hashid
* @property \App\Models\Server $server
* @property \App\Models\Task[]|\Illuminate\Support\Collection $tasks
*/
@@ -121,6 +124,14 @@ class Schedule extends Model
);
}
/**
* Return a hashid encoded string to represent the ID of the schedule.
*/
public function getHashidAttribute(): string
{
return Container::getInstance()->make(HashidsInterface::class)->encode($this->id);
}
/**
* Return tasks belonging to a schedule.
*/

View File

@@ -5,7 +5,6 @@ namespace App\Models;
use App\Enums\ServerState;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Facades\Http;
@@ -14,6 +13,7 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use App\Exceptions\Http\Server\ServerStateConflictException;
/**
@@ -35,7 +35,7 @@ use App\Exceptions\Http\Server\ServerStateConflictException;
* @property int $io
* @property int $cpu
* @property string|null $threads
* @property bool $oom_killer
* @property bool $oom_disabled
* @property int $allocation_id
* @property int $egg_id
* @property string $startup
@@ -90,7 +90,7 @@ use App\Exceptions\Http\Server\ServerStateConflictException;
* @method static \Illuminate\Database\Eloquent\Builder|Server whereMemory($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereNodeId($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereOomKiller($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereOomDisabled($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereOwnerId($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereSkipScripts($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereStartup($value)
@@ -124,7 +124,7 @@ class Server extends Model
*/
protected $attributes = [
'status' => ServerState::Installing,
'oom_killer' => false,
'oom_disabled' => true,
'installed_at' => null,
];
@@ -150,7 +150,7 @@ class Server extends Model
'io' => 'required|numeric|between:0,1000',
'cpu' => 'required|numeric|min:0',
'threads' => 'nullable|regex:/^[0-9-,]+$/',
'oom_killer' => 'sometimes|boolean',
'oom_disabled' => 'sometimes|boolean',
'disk' => 'required|numeric|min:0',
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
'egg_id' => 'required|exists:eggs,id',
@@ -174,7 +174,7 @@ class Server extends Model
'disk' => 'integer',
'io' => 'integer',
'cpu' => 'integer',
'oom_killer' => 'boolean',
'oom_disabled' => 'boolean',
'allocation_id' => 'integer',
'egg_id' => 'integer',
'database_limit' => 'integer',
@@ -184,7 +184,6 @@ class Server extends Model
self::UPDATED_AT => 'datetime',
'deleted_at' => 'datetime',
'installed_at' => 'datetime',
'docker_labels' => 'array',
];
}
@@ -311,9 +310,12 @@ class Server extends Model
return $this->hasMany(Backup::class);
}
public function mounts(): BelongsToMany
/**
* Returns all mounts that have this server has mounted.
*/
public function mounts(): HasManyThrough
{
return $this->belongsToMany(Mount::class);
return $this->hasManyThrough(Mount::class, MountServer::class, 'server_id', 'id', 'id', 'mount_id');
}
/**
@@ -324,11 +326,6 @@ class Server extends Model
return $this->morphToMany(ActivityLog::class, 'subject', 'activity_log_subjects');
}
public function getRouteKeyName(): string
{
return 'id';
}
public function resolveRouteBinding($value, $field = null): ?self
{
return match ($field) {

View File

@@ -52,6 +52,14 @@ class Subuser extends Model
];
}
/**
* Return a hashid encoded string to represent the ID of the subuser.
*/
public function getHashidAttribute(): string
{
return app()->make('hashids')->encode($this->id);
}
/**
* Gets the server associated with a subuser.
*/

View File

@@ -2,8 +2,10 @@
namespace App\Models;
use Illuminate\Container\Container;
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use App\Contracts\Extensions\HashidsInterface;
/**
* @property int $id
@@ -16,6 +18,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property bool $continue_on_failure
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property string $hashid
* @property \App\Models\Schedule $schedule
* @property \App\Models\Server $server
*/
@@ -93,6 +96,14 @@ class Task extends Model
return $this->getKeyName();
}
/**
* Return a hashid encoded string to represent the ID of the task.
*/
public function getHashidAttribute(): string
{
return Container::getInstance()->make(HashidsInterface::class)->encode($this->id);
}
/**
* Return the schedule that a task belongs to.
*/

View File

@@ -31,7 +31,7 @@ trait HasAccessTokens
'user_id' => $this->id,
'key_type' => ApiKey::TYPE_ACCOUNT,
'identifier' => ApiKey::generateTokenIdentifier(ApiKey::TYPE_ACCOUNT),
'token' => $plain = Str::random(ApiKey::KEY_LENGTH),
'token' => encrypt($plain = Str::random(ApiKey::KEY_LENGTH)),
'memo' => $memo ?? '',
'allowed_ips' => $ips ?? [],
]);

View File

@@ -171,7 +171,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
'use_totp' => 'boolean',
'gravatar' => 'boolean',
'totp_authenticated_at' => 'datetime',
'totp_secret' => 'encrypted',
];
}

View File

@@ -6,14 +6,9 @@ use App\Extensions\Themes\Theme;
use App\Models;
use App\Models\ApiKey;
use App\Models\Node;
use App\Services\Helpers\SoftwareVersionService;
use Dedoc\Scramble\Scramble;
use Dedoc\Scramble\Support\Generator\OpenApi;
use Dedoc\Scramble\Support\Generator\SecurityScheme;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\URL;
@@ -31,9 +26,8 @@ class AppServiceProvider extends ServiceProvider
{
Schema::defaultStringLength(191);
$versionData = app(SoftwareVersionService::class)->versionData();
View::share('appVersion', $versionData['version'] ?? 'undefined');
View::share('appIsGit', $versionData['is_git'] ?? false);
View::share('appVersion', $this->versionData()['version'] ?? 'undefined');
View::share('appIsGit', $this->versionData()['is_git'] ?? false);
Paginator::useBootstrap();
@@ -62,7 +56,7 @@ class AppServiceProvider extends ServiceProvider
'daemon',
fn (Node $node, array $headers = []) => Http::acceptJson()
->asJson()
->withToken($node->daemon_token)
->withToken($node->getDecryptedKey())
->withHeaders($headers)
->withOptions(['verify' => (bool) app()->environment('production')])
->timeout(config('panel.guzzle.timeout'))
@@ -72,12 +66,6 @@ class AppServiceProvider extends ServiceProvider
$this->bootAuth();
$this->bootBroadcast();
$bearerTokens = fn (OpenApi $openApi) => $openApi->secure(SecurityScheme::http('bearer'));
Gate::define('viewApiDocs', fn () => true);
Scramble::registerApi('application', ['api_path' => 'api/application', 'info' => ['version' => '1.0']]);
Scramble::registerApi('client', ['api_path' => 'api/client', 'info' => ['version' => '1.0']])->afterOpenApiGenerated($bearerTokens);
Scramble::registerApi('remote', ['api_path' => 'api/remote', 'info' => ['version' => '1.0']])->afterOpenApiGenerated($bearerTokens);
}
/**
@@ -93,9 +81,34 @@ class AppServiceProvider extends ServiceProvider
$this->app->singleton('extensions.themes', function () {
return new Theme();
});
}
Scramble::extendOpenApi(fn (OpenApi $openApi) => $openApi->secure(SecurityScheme::http('bearer')));
Scramble::ignoreDefaultRoutes();
/**
* Return version information for the footer.
*/
protected function versionData(): array
{
return cache()->remember('git-version', 5, function () {
if (file_exists(base_path('.git/HEAD'))) {
$head = explode(' ', file_get_contents(base_path('.git/HEAD')));
if (array_key_exists(1, $head)) {
$path = base_path('.git/' . trim($head[1]));
}
}
if (isset($path) && file_exists($path)) {
return [
'version' => substr(file_get_contents($path), 0, 8),
'is_git' => true,
];
}
return [
'version' => config('app.version'),
'is_git' => false,
];
});
}
public function bootAuth(): void

View File

@@ -25,7 +25,7 @@ class AdminPanelProvider extends PanelProvider
public function boot()
{
FilamentAsset::registerCssVariables([
'sidebar-width' => '14rem !important',
'sidebar-width' => '12rem !important',
]);
}
@@ -35,13 +35,10 @@ class AdminPanelProvider extends PanelProvider
->default()
->id('admin')
->path('admin')
->topNavigation(config('panel.filament.top-navigation', true))
->login()
->brandName('Pelican')
->homeUrl('/')
->favicon(config('app.favicon', '/pelican.ico'))
->brandName(config('app.name', 'Pelican'))
->brandLogo(config('app.logo'))
->brandLogoHeight('2rem')
->favicon('/pelican.ico')
->profile(EditProfile::class, false)
->colors([
'danger' => Color::Red,

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Providers;
use App\Extensions\Hashids;
use Illuminate\Support\ServiceProvider;
use App\Contracts\Extensions\HashidsInterface;
class HashidsServiceProvider extends ServiceProvider
{
/**
* Register the ability to use Hashids.
*/
public function register(): void
{
$this->app->singleton(HashidsInterface::class, function () {
return new Hashids(
config('hashids.salt', ''),
config('hashids.length', 0),
config('hashids.alphabet', 'abcdefghijkmlnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890')
);
});
$this->app->alias(HashidsInterface::class, 'hashids');
}
}

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