mirror of
https://github.com/pelican-dev/panel.git
synced 2026-03-01 11:21:31 +03:00
Compare commits
157 Commits
v1.0.0-bet
...
v1.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd7a01aa04 | ||
|
|
55badb5644 | ||
|
|
93f059025c | ||
|
|
7be0cd6928 | ||
|
|
0156456919 | ||
|
|
b9d1ce4438 | ||
|
|
9ce262bf56 | ||
|
|
7ee52affb2 | ||
|
|
93bfe925b9 | ||
|
|
cc1ac1eba1 | ||
|
|
02d24b8a36 | ||
|
|
16fac3b5c6 | ||
|
|
eb99f53d87 | ||
|
|
643e4168b9 | ||
|
|
51cd7a8e81 | ||
|
|
91bf38b63d | ||
|
|
e3699f34d8 | ||
|
|
dc3da2dc98 | ||
|
|
d245751c97 | ||
|
|
e0d7a094ab | ||
|
|
3010e3d61e | ||
|
|
d68e7218a8 | ||
|
|
a4435a7454 | ||
|
|
df26c4f9f5 | ||
|
|
6f1de67523 | ||
|
|
6f009ee126 | ||
|
|
328e159c6b | ||
|
|
f9fd426aca | ||
|
|
6166fac929 | ||
|
|
4bd1070025 | ||
|
|
2d6e30b646 | ||
|
|
f61c6b9dc2 | ||
|
|
5e29737dc5 | ||
|
|
d996019204 | ||
|
|
91d8dbd084 | ||
|
|
bb03ddda50 | ||
|
|
1c66681c0e | ||
|
|
0728266826 | ||
|
|
d81c9faac6 | ||
|
|
cff54f1969 | ||
|
|
201563a13b | ||
|
|
8f2261f6cd | ||
|
|
29cc92f0dc | ||
|
|
33f10cbcb9 | ||
|
|
b538532e34 | ||
|
|
a892821b4f | ||
|
|
5a3b50b31f | ||
|
|
51b217571b | ||
|
|
6e75c76c60 | ||
|
|
e22c5c3e0a | ||
|
|
f3171939a4 | ||
|
|
189d564f87 | ||
|
|
7926f97c8e | ||
|
|
f4d39c1c68 | ||
|
|
6c2d0a2d50 | ||
|
|
f6899301fd | ||
|
|
cbb4ef1da2 | ||
|
|
f6ef76d98e | ||
|
|
65a697d8f7 | ||
|
|
9515a82a75 | ||
|
|
44f5ea567f | ||
|
|
88f910f3e7 | ||
|
|
020f028008 | ||
|
|
0cb7f737b0 | ||
|
|
53aa52f519 | ||
|
|
e884eda5a7 | ||
|
|
58d1fd3917 | ||
|
|
0b0952650e | ||
|
|
aa55a7ed83 | ||
|
|
c7fa7a1bad | ||
|
|
4a3bdd78ef | ||
|
|
a1067fd4aa | ||
|
|
110cc1248b | ||
|
|
04a1ccc97e | ||
|
|
5e7f5c2a4c | ||
|
|
b804878d7b | ||
|
|
118977c8c5 | ||
|
|
c31b7b8c6a | ||
|
|
eefe59b153 | ||
|
|
cd4b7cbf9e | ||
|
|
67cb3d4816 | ||
|
|
7762e68a6c | ||
|
|
7a327ea378 | ||
|
|
b3ca7b7ac9 | ||
|
|
abc99cd928 | ||
|
|
cb638369cf | ||
|
|
9174de2d8c | ||
|
|
7cda358b66 | ||
|
|
33f6551b21 | ||
|
|
b1928e89b4 | ||
|
|
c956cd0106 | ||
|
|
5081cc3f63 | ||
|
|
8eb2c23420 | ||
|
|
cfe385f53a | ||
|
|
264d3498a6 | ||
|
|
065f3f2468 | ||
|
|
957638d4ac | ||
|
|
7d0ce1627b | ||
|
|
8cec7368ab | ||
|
|
5519931ee5 | ||
|
|
97ac0fe54b | ||
|
|
7657364208 | ||
|
|
ef1a208b95 | ||
|
|
aa82c6dd04 | ||
|
|
8ecabef6b5 | ||
|
|
a6d07ede5a | ||
|
|
f6325c07c4 | ||
|
|
7674ee0e2b | ||
|
|
5760e72b8f | ||
|
|
b6e46f758d | ||
|
|
e980877bbc | ||
|
|
dd223b47c0 | ||
|
|
639fa3399d | ||
|
|
82fd547484 | ||
|
|
d461242f08 | ||
|
|
dec1cf8e74 | ||
|
|
15caac51fb | ||
|
|
183c274a0d | ||
|
|
a8b2fb440f | ||
|
|
f8e4514998 | ||
|
|
deeebf73d3 | ||
|
|
422fc102c9 | ||
|
|
e715e92f9d | ||
|
|
73babfa2b3 | ||
|
|
e0a92d733b | ||
|
|
1e67cd9944 | ||
|
|
3946116dff | ||
|
|
b77fd3d653 | ||
|
|
f4672c6cb1 | ||
|
|
5b9e4b1729 | ||
|
|
48f715ae69 | ||
|
|
51460782cc | ||
|
|
b007e63937 | ||
|
|
4dd833562b | ||
|
|
b579f14f3f | ||
|
|
eadaec1b30 | ||
|
|
a9e58bb493 | ||
|
|
5c33c7495a | ||
|
|
f9aa8cf218 | ||
|
|
da698a3666 | ||
|
|
2808a3dd35 | ||
|
|
7ea365e8de | ||
|
|
ae399f9bad | ||
|
|
53a5ff6e6d | ||
|
|
54ae4b3dc1 | ||
|
|
859a721e17 | ||
|
|
03cbdd5bdd | ||
|
|
4c43fd1683 | ||
|
|
0c61a63191 | ||
|
|
b1f99ca8a3 | ||
|
|
0a5810358a | ||
|
|
1bae239971 | ||
|
|
597f74f105 | ||
|
|
5344d99a40 | ||
|
|
1db1a1a3e0 | ||
|
|
712b6a285b | ||
|
|
38b92ae21d |
@@ -17,9 +17,6 @@ CACHE_STORE=file
|
||||
QUEUE_CONNECTION=database
|
||||
SESSION_DRIVER=file
|
||||
|
||||
HASHIDS_SALT=
|
||||
HASHIDS_LENGTH=8
|
||||
|
||||
MAIL_MAILER=log
|
||||
MAIL_HOST=smtp.example.com
|
||||
MAIL_PORT=25
|
||||
@@ -33,3 +30,8 @@ MAIL_FROM_NAME="Pelican Admin"
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
# Set this to true, and set start & end ports to auto create allocations.
|
||||
PANEL_CLIENT_ALLOCATIONS_ENABLED=false
|
||||
PANEL_CLIENT_ALLOCATIONS_RANGE_START=
|
||||
PANEL_CLIENT_ALLOCATIONS_RANGE_END=
|
||||
|
||||
8
.github/workflows/ci.yaml
vendored
8
.github/workflows/ci.yaml
vendored
@@ -34,7 +34,6 @@ 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
|
||||
@@ -60,7 +59,7 @@ jobs:
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: bcmath, cli, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
|
||||
@@ -97,9 +96,8 @@ jobs:
|
||||
MAIL_MAILER: array
|
||||
SESSION_DRIVER: array
|
||||
QUEUE_CONNECTION: sync
|
||||
HASHIDS_SALT: alittlebitofsalt1234
|
||||
DB_CONNECTION: sqlite
|
||||
DB_DATABASE: ${{ github.workspace }}/database/testing.sqlite
|
||||
DB_DATABASE: testing.sqlite
|
||||
steps:
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -121,7 +119,7 @@ jobs:
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: bcmath, cli, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
|
||||
|
||||
65
.gitignore
vendored
65
.gitignore
vendored
@@ -1,41 +1,28 @@
|
||||
/vendor
|
||||
*.DS_Store*
|
||||
!.env.ci
|
||||
!.env.example
|
||||
.env*
|
||||
.vagrant/*
|
||||
.vscode/*
|
||||
storage/framework/*
|
||||
/.idea
|
||||
/nbproject
|
||||
/.direnv
|
||||
|
||||
node_modules
|
||||
*.log
|
||||
_ide_helper.php
|
||||
_ide_helper_models.php
|
||||
.phpstorm.meta.php
|
||||
.yarn
|
||||
public/assets/manifest.json
|
||||
*.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
|
||||
|
||||
/.phpunit.cache
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/hot
|
||||
result
|
||||
docker-compose.yaml
|
||||
|
||||
public/css/filament-monaco-editor/
|
||||
|
||||
public/js/filament-monaco-editor/
|
||||
/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
|
||||
/.idea
|
||||
/.vscode
|
||||
|
||||
public/assets/manifest.json
|
||||
/database/*.sqlite
|
||||
filament-monaco-editor/
|
||||
_ide_helper*
|
||||
/.phpstorm.meta.php
|
||||
|
||||
@@ -24,15 +24,14 @@ class AppSettingsCommand extends Command
|
||||
];
|
||||
|
||||
public const QUEUE_DRIVERS = [
|
||||
'sync' => 'Synchronous (recommended)',
|
||||
'database' => 'Database',
|
||||
'database' => 'Database (recommended)',
|
||||
'redis' => 'Redis',
|
||||
'sync' => 'Synchronous',
|
||||
];
|
||||
|
||||
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.}
|
||||
@@ -61,10 +60,6 @@ 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',
|
||||
@@ -103,7 +98,13 @@ class AppSettingsCommand extends Command
|
||||
$this->variables['SESSION_SECURE_COOKIE'] = 'true';
|
||||
}
|
||||
|
||||
$this->checkForRedis();
|
||||
$redisUsed = count(collect($this->variables)->filter(function ($item) {
|
||||
return $item === 'redis';
|
||||
})) !== 0;
|
||||
|
||||
if ($redisUsed) {
|
||||
$this->requestRedisSettings();
|
||||
}
|
||||
|
||||
$path = base_path('.env');
|
||||
if (!file_exists($path)) {
|
||||
@@ -116,25 +117,22 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if redis is selected, if so, request connection details and verify them.
|
||||
* Request redis connection details and verify them.
|
||||
*/
|
||||
private function checkForRedis()
|
||||
private function requestRedisSettings(): void
|
||||
{
|
||||
$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',
|
||||
|
||||
@@ -98,7 +98,7 @@ class DatabaseSettingsCommand extends Command
|
||||
} elseif ($this->variables['DB_CONNECTION'] === 'sqlite') {
|
||||
$this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask(
|
||||
'Database Path',
|
||||
config('database.connections.sqlite.database', database_path('database.sqlite'))
|
||||
env('DB_DATABASE', 'database.sqlite')
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<?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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ class InfoCommand extends Command
|
||||
{
|
||||
$this->output->title('Version Information');
|
||||
$this->table([], [
|
||||
['Panel Version', config('app.version')],
|
||||
['Panel Version', $this->versionService->versionData()['version']],
|
||||
['Latest Version', $this->versionService->getPanel()],
|
||||
['Up-to-Date', $this->versionService->isLatestPanel() ? 'Yes' : $this->formatText('No', 'bg=red')],
|
||||
], 'compact');
|
||||
|
||||
@@ -20,9 +20,12 @@ 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.';
|
||||
@@ -58,9 +61,12 @@ 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);
|
||||
|
||||
@@ -24,7 +24,7 @@ class ProcessRunnableCommand extends Command
|
||||
->whereRelation('server', fn (Builder $builder) => $builder->whereNull('status'))
|
||||
->where('is_active', true)
|
||||
->where('is_processing', false)
|
||||
->whereDate('next_run_at', '<=', Carbon::now()->toDateString())
|
||||
->whereDate('next_run_at', '<=', Carbon::now()->toDateTimeString())
|
||||
->get();
|
||||
|
||||
if ($schedules->count() < 1) {
|
||||
@@ -62,7 +62,7 @@ class ProcessRunnableCommand extends Command
|
||||
|
||||
$this->line(trans('command/messages.schedule.output_line', [
|
||||
'schedule' => $schedule->name,
|
||||
'hash' => $schedule->hashid,
|
||||
'id' => $schedule->id,
|
||||
]));
|
||||
} catch (\Throwable|\Exception $exception) {
|
||||
logger()->error($exception, ['schedule_id' => $schedule->id]);
|
||||
|
||||
@@ -30,7 +30,7 @@ class MakeUserCommand extends Command
|
||||
public function handle(): int
|
||||
{
|
||||
try {
|
||||
DB::select('select 1 where 1');
|
||||
DB::connection()->getPdo();
|
||||
} catch (Exception $exception) {
|
||||
$this->error($exception->getMessage());
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -48,7 +48,7 @@ class DisplayException extends PanelException implements HttpExceptionInterface
|
||||
*/
|
||||
public function render(Request $request)
|
||||
{
|
||||
if (str($request->url())->contains('livewire')) {
|
||||
if ($request->is('livewire/update')) {
|
||||
Notification::make()
|
||||
->title(static::class)
|
||||
->body($this->getMessage())
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Deployment;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class NoViableNodeException extends DisplayException
|
||||
{
|
||||
}
|
||||
@@ -6,9 +6,9 @@ use App\Exceptions\DisplayException;
|
||||
|
||||
class TwoFactorAuthenticationTokenInvalid extends DisplayException
|
||||
{
|
||||
/**
|
||||
* TwoFactorAuthenticationTokenInvalid constructor.
|
||||
*/
|
||||
public string $title = 'Invalid 2FA Code';
|
||||
public string $icon = 'tabler-2fa';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('The provided two-factor authentication token was not valid.');
|
||||
|
||||
@@ -25,7 +25,7 @@ class DynamicDatabaseConnection
|
||||
'port' => $host->port,
|
||||
'database' => $database,
|
||||
'username' => $host->username,
|
||||
'password' => decrypt($host->password),
|
||||
'password' => $host->password,
|
||||
'charset' => self::DB_CHARSET,
|
||||
'collation' => self::DB_COLLATION,
|
||||
]);
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use App\Models\Egg;
|
||||
use App\Models\Node;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use App\Services\Helpers\SoftwareVersionService;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Pages\Page;
|
||||
|
||||
@@ -29,8 +30,14 @@ class Dashboard extends Page
|
||||
|
||||
public function getViewData(): array
|
||||
{
|
||||
/** @var SoftwareVersionService $softwareVersionService */
|
||||
$softwareVersionService = app(SoftwareVersionService::class);
|
||||
|
||||
return [
|
||||
'inDevelopment' => config('app.version') === 'canary',
|
||||
'version' => $softwareVersionService->versionData()['version'],
|
||||
'latestVersion' => $softwareVersionService->getPanel(),
|
||||
'isLatest' => $softwareVersionService->isLatestPanel(),
|
||||
'eggsCount' => Egg::query()->count(),
|
||||
'nodesList' => ListNodes::getUrl(),
|
||||
'nodesCount' => Node::query()->count(),
|
||||
@@ -39,15 +46,17 @@ class Dashboard extends Page
|
||||
|
||||
'devActions' => [
|
||||
CreateAction::make()
|
||||
->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'))
|
||||
->label('Bugs & 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'))
|
||||
@@ -55,14 +64,10 @@ 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('https://pelican.dev/donate', true)
|
||||
->url($softwareVersionService->getDonations(), true)
|
||||
->color('success'),
|
||||
],
|
||||
'helpActions' => [
|
||||
@@ -70,11 +75,6 @@ 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'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@ 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
|
||||
{
|
||||
@@ -16,7 +14,7 @@ class ApiKeyResource extends Resource
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count() ?: null;
|
||||
return static::getModel()::where('key_type', '2')->count() ?: null;
|
||||
}
|
||||
|
||||
public static function canEdit($record): bool
|
||||
@@ -24,20 +22,6 @@ class ApiKeyResource extends Resource
|
||||
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 [
|
||||
|
||||
@@ -19,30 +19,16 @@ class CreateApiKey extends CreateRecord
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\Hidden::make('identifier')->default(ApiKey::generateTokenIdentifier(ApiKey::TYPE_APPLICATION)),
|
||||
Forms\Components\Hidden::make('token')->default(encrypt(str_random(ApiKey::KEY_LENGTH))),
|
||||
Forms\Components\Hidden::make('token')->default(str_random(ApiKey::KEY_LENGTH)),
|
||||
|
||||
Forms\Components\Hidden::make('user_id')
|
||||
->default(auth()->user()->id)
|
||||
->required(),
|
||||
|
||||
Forms\Components\Select::make('key_type')
|
||||
Forms\Components\Hidden::make('key_type')
|
||||
->inlineLabel()
|
||||
->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),
|
||||
->default(ApiKey::TYPE_APPLICATION)
|
||||
->required(),
|
||||
|
||||
Forms\Components\Fieldset::make('Permissions')
|
||||
->columns([
|
||||
|
||||
@@ -5,10 +5,8 @@ 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
|
||||
@@ -19,16 +17,12 @@ 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 . decrypt($key->token)),
|
||||
->state(fn (ApiKey $key) => $key->identifier . $key->token),
|
||||
|
||||
Tables\Columns\TextColumn::make('memo')
|
||||
->label('Description')
|
||||
@@ -41,6 +35,7 @@ class ListApiKeys extends ListRecords
|
||||
|
||||
Tables\Columns\TextColumn::make('last_used_at')
|
||||
->label('Last Used')
|
||||
->placeholder('Not Used')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
|
||||
@@ -48,13 +43,13 @@ class ListApiKeys extends ListRecords
|
||||
->label('Created')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
|
||||
Tables\Columns\TextColumn::make('user.username')
|
||||
->label('Created By')
|
||||
->url(fn (ApiKey $apiKey): string => route('filament.admin.resources.users.edit', ['record' => $apiKey->user])),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\DeleteAction::make(),
|
||||
//Tables\Actions\EditAction::make()
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -64,22 +59,4 @@ 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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,15 +74,6 @@ class CreateDatabaseHost extends CreateRecord
|
||||
]);
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
if (isset($data['password'])) {
|
||||
$data['password'] = encrypt($data['password']);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Filament\Resources\DatabaseHostResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DatabaseHostResource;
|
||||
use App\Models\DatabaseHost;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Forms;
|
||||
@@ -71,20 +72,13 @@ class EditDatabaseHost extends EditRecord
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make(),
|
||||
Actions\DeleteAction::make()
|
||||
->label(fn (DatabaseHost $databaseHost) => $databaseHost->databases()->count() > 0 ? 'Database Host Has Databases' : 'Delete')
|
||||
->disabled(fn (DatabaseHost $databaseHost) => $databaseHost->databases()->count() > 0),
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
if (isset($data['password'])) {
|
||||
$data['password'] = encrypt($data['password']);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
|
||||
@@ -15,8 +15,6 @@ class DatabasesRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'databases';
|
||||
|
||||
protected $listeners = ['refresh' => 'refreshForm'];
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
@@ -28,15 +26,15 @@ class DatabasesRelationManager extends RelationManager
|
||||
Action::make('rotate')
|
||||
->icon('tabler-refresh')
|
||||
->requiresConfirmation()
|
||||
->action(fn (DatabasePasswordService $service, Database $database) => $service->handle($database))
|
||||
->action(fn (DatabasePasswordService $service, Database $database, $set, $get) => $this->rotatePassword($service, $database, $set, $get))
|
||||
)
|
||||
->formatStateUsing(fn (Database $database) => decrypt($database->password)),
|
||||
->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(decrypt($database->password)) . '@' . $database->host->host . ':' . $database->host->port . '/' . $get('database')),
|
||||
->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
|
||||
@@ -60,4 +58,13 @@ class DatabasesRelationManager extends RelationManager
|
||||
//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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,12 +25,12 @@ class EditEgg extends EditRecord
|
||||
Forms\Components\TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(191)
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 1])
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 1])
|
||||
->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' => 2, 'lg' => 2])
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, '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')
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Services\Eggs\Sharing\EggImporterService;
|
||||
use Exception;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Table;
|
||||
@@ -31,28 +32,13 @@ class ListEggs extends ListRecords
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->icon('tabler-egg')
|
||||
->description(fn ($record): ?string => $record->description)
|
||||
->description(fn ($record): ?string => (strlen($record->description) > 120) ? substr($record->description, 0, 120).'...' : $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(),
|
||||
@@ -63,9 +49,6 @@ 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(),
|
||||
@@ -80,21 +63,58 @@ class ListEggs extends ListRecords
|
||||
Actions\Action::make('import')
|
||||
->label('Import')
|
||||
->form([
|
||||
Forms\Components\FileUpload::make('egg')
|
||||
->acceptedFileTypes(['application/json'])
|
||||
->storeFiles(false)
|
||||
->multiple(),
|
||||
Tabs::make('Tabs')
|
||||
->tabs([
|
||||
Tabs\Tab::make('From File')
|
||||
->icon('tabler-file-upload')
|
||||
->schema([
|
||||
Forms\Components\FileUpload::make('egg')
|
||||
->label('Egg')
|
||||
->hint('This should be the json file ( egg-minecraft.json )')
|
||||
->acceptedFileTypes(['application/json'])
|
||||
->storeFiles(false)
|
||||
->multiple(),
|
||||
]),
|
||||
Tabs\Tab::make('From URL')
|
||||
->icon('tabler-world-upload')
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('url')
|
||||
->label('URL')
|
||||
->hint('This URL should point to a single json file')
|
||||
->url(),
|
||||
]),
|
||||
])
|
||||
->contained(false),
|
||||
|
||||
])
|
||||
->action(function (array $data): void {
|
||||
/** @var TemporaryUploadedFile $eggFile */
|
||||
$eggFile = $data['egg'];
|
||||
|
||||
/** @var EggImporterService $eggImportService */
|
||||
$eggImportService = resolve(EggImporterService::class);
|
||||
|
||||
foreach ($eggFile as $file) {
|
||||
if (!empty($data['egg'])) {
|
||||
/** @var TemporaryUploadedFile[] $eggFile */
|
||||
$eggFile = $data['egg'];
|
||||
|
||||
foreach ($eggFile as $file) {
|
||||
try {
|
||||
$eggImportService->fromFile($file);
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Import Failed')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
report($exception);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($data['url'])) {
|
||||
try {
|
||||
$eggImportService->handle($file);
|
||||
$eggImportService->fromUrl($data['url']);
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Import Failed')
|
||||
|
||||
@@ -311,6 +311,47 @@ class CreateNode extends CreateRecord
|
||||
->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('%'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
|
||||
@@ -6,9 +6,11 @@ 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;
|
||||
@@ -185,26 +187,49 @@ class EditNode extends EditRecord
|
||||
])
|
||||
->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())
|
||||
->columnSpan(2)
|
||||
->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')
|
||||
->columnSpan(1),
|
||||
->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()
|
||||
->columnSpan(1)
|
||||
->options([
|
||||
true => 'Yes',
|
||||
false => 'No',
|
||||
@@ -214,29 +239,20 @@ class EditNode extends EditRecord
|
||||
false => 'danger',
|
||||
]),
|
||||
Forms\Components\ToggleButtons::make('maintenance_mode')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
|
||||
->label('Maintenance Mode')->inline()
|
||||
->columnSpan(1)
|
||||
->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',
|
||||
true => 'Enable',
|
||||
])
|
||||
->colors([
|
||||
true => 'danger',
|
||||
false => 'success',
|
||||
true => 'danger',
|
||||
]),
|
||||
Forms\Components\TextInput::make('upload_size')
|
||||
->label('Upload Limit')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('Enter the maximum size of files that can be uploaded through the web-based file manager.')
|
||||
->columnStart(4)->columnSpan(1)
|
||||
->numeric()->required()
|
||||
->minValue(1)
|
||||
->maxValue(1024)
|
||||
->suffix('MiB'),
|
||||
Forms\Components\Grid::make()
|
||||
->columns(6)
|
||||
->columns(['default' => 1, 'sm' => 1, 'md' => 3, 'lg' => 6])
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('unlimited_mem')
|
||||
@@ -253,14 +269,14 @@ class EditNode extends EditRecord
|
||||
true => 'primary',
|
||||
false => 'warning',
|
||||
])
|
||||
->columnSpan(2),
|
||||
->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(2)
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
|
||||
->numeric()
|
||||
->minValue(0),
|
||||
Forms\Components\TextInput::make('memory_overallocate')
|
||||
@@ -270,15 +286,14 @@ class EditNode extends EditRecord
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The % allowable to go over the set limit.')
|
||||
->columnSpan(2)
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
|
||||
->numeric()
|
||||
->minValue(-1)
|
||||
->maxValue(100)
|
||||
->suffix('%'),
|
||||
]),
|
||||
Forms\Components\Grid::make()
|
||||
->columns(6)
|
||||
->columnSpanFull()
|
||||
->columns(['default' => 1, 'sm' => 1, 'md' => 3, 'lg' => 6])
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('unlimited_disk')
|
||||
->label('Disk')->inlineLabel()->inline()
|
||||
@@ -294,14 +309,14 @@ class EditNode extends EditRecord
|
||||
true => 'primary',
|
||||
false => 'warning',
|
||||
])
|
||||
->columnSpan(2),
|
||||
->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(2)
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
|
||||
->numeric()
|
||||
->minValue(0),
|
||||
Forms\Components\TextInput::make('disk_overallocate')
|
||||
@@ -310,6 +325,47 @@ class EditNode extends EditRecord
|
||||
->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()
|
||||
@@ -332,6 +388,18 @@ 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()
|
||||
),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
@@ -367,4 +435,9 @@ class EditNode extends EditRecord
|
||||
NodeMemoryChart::class,
|
||||
];
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
$this->fillForm();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,12 @@ class ListNodes extends ListRecords
|
||||
->suffix(' GiB')
|
||||
->formatStateUsing(fn ($state) => number_format($state / 1024, 2))
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('cpu')
|
||||
->visibleFrom('sm')
|
||||
->icon('tabler-file')
|
||||
->numeric()
|
||||
->suffix(' %')
|
||||
->sortable(),
|
||||
Tables\Columns\IconColumn::make('scheme')
|
||||
->visibleFrom('xl')
|
||||
->label('SSL')
|
||||
|
||||
@@ -40,6 +40,10 @@ 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')
|
||||
@@ -51,9 +55,6 @@ class AllocationsRelationManager extends RelationManager
|
||||
Tables\Columns\TextInputColumn::make('ip')
|
||||
->searchable()
|
||||
->label('IP'),
|
||||
Tables\Columns\TextColumn::make('port')
|
||||
->searchable()
|
||||
->label('Port'),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
@@ -112,7 +113,7 @@ class AllocationsRelationManager extends RelationManager
|
||||
|
||||
$start = max((int) $start, 0);
|
||||
$end = min((int) $end, 2 ** 16 - 1);
|
||||
for ($i = $start; $i <= $end; $i++) {
|
||||
foreach (range($start, $end) as $i) {
|
||||
$ports->push($i);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ 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
|
||||
@@ -77,13 +79,16 @@ class CreateServer extends CreateRecord
|
||||
Forms\Components\Select::make('node_id')
|
||||
->disabledOn('edit')
|
||||
->prefixIcon('tabler-server-2')
|
||||
->default(fn () => Node::query()->latest()->first()?->id)
|
||||
->default(fn () => ($this->node = Node::query()->latest()->first())?->id)
|
||||
->columnSpan(2)
|
||||
->live()
|
||||
->relationship('node', 'name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->afterStateUpdated(fn (Forms\Set $set) => $set('allocation_id', null))
|
||||
->afterStateUpdated(function (Forms\Set $set, $state) {
|
||||
$set('allocation_id', null);
|
||||
$this->node = Node::find($state);
|
||||
})
|
||||
->required(),
|
||||
|
||||
Forms\Components\Select::make('allocation_id')
|
||||
@@ -309,55 +314,6 @@ 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')
|
||||
@@ -393,7 +349,12 @@ class CreateServer extends CreateRecord
|
||||
]))
|
||||
->schema([
|
||||
Forms\Components\Placeholder::make('Select an egg first to show its variables!')
|
||||
->hidden(fn (Forms\Get $get) => !empty($get('server_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()
|
||||
),
|
||||
|
||||
Forms\Components\Repeater::make('server_variables')
|
||||
->relationship('serverVariables')
|
||||
@@ -410,19 +371,20 @@ class CreateServer extends CreateRecord
|
||||
$text = Forms\Components\TextInput::make('variable_value')
|
||||
->hidden($this->shouldHideComponent(...))
|
||||
->maxLength(191)
|
||||
->rules([
|
||||
->required(fn (Forms\Get $get) => in_array('required', explode('|', $get('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'));
|
||||
$message = str($validator->errors()->first())->replace('validatorkey', $get('name'))->toString();
|
||||
|
||||
$fail($message);
|
||||
}
|
||||
},
|
||||
]);
|
||||
);
|
||||
|
||||
$select = Forms\Components\Select::make('variable_value')
|
||||
->hidden($this->shouldHideComponent(...))
|
||||
@@ -452,7 +414,7 @@ class CreateServer extends CreateRecord
|
||||
->columnSpan(2),
|
||||
]),
|
||||
|
||||
Forms\Components\Section::make('Resource Management')
|
||||
Forms\Components\Section::make('Environment Management')
|
||||
->collapsed()
|
||||
->icon('tabler-server-cog')
|
||||
->iconColor('primary')
|
||||
@@ -464,175 +426,190 @@ class CreateServer extends CreateRecord
|
||||
])
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\Grid::make()
|
||||
->columns(4)
|
||||
->columnSpanFull()
|
||||
Forms\Components\Fieldset::make('Resource Limits')
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 4,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 3,
|
||||
'lg' => 3,
|
||||
])
|
||||
->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\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\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 logical thread'),
|
||||
]),
|
||||
|
||||
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('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\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\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\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('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\TextInput::make('oom_disabled_hidden')
|
||||
->hidden(),
|
||||
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\Fieldset::make('Application Feature Limits')
|
||||
Forms\Components\Fieldset::make('Feature Limits')
|
||||
->inlineLabel()
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
@@ -663,6 +640,70 @@ 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
@@ -27,13 +27,15 @@ 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\TextInputColumn::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',
|
||||
@@ -57,7 +59,11 @@ class AllocationsRelationManager extends RelationManager
|
||||
])
|
||||
->headerActions([
|
||||
//TODO Tables\Actions\CreateAction::make()->label('Create Allocation'),
|
||||
//TODO Tables\Actions\AssociateAction::make()->label('Add Allocation'),
|
||||
Tables\Actions\AssociateAction::make()
|
||||
->multiple()
|
||||
->preloadRecordSelect()
|
||||
->recordSelectOptionsQuery(fn ($query) => $query->whereBelongsTo($this->getOwnerRecord()->node))
|
||||
->label('Add Allocation'),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
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;
|
||||
@@ -20,8 +22,10 @@ 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;
|
||||
@@ -99,12 +103,26 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
|
||||
if ($this->getUser()->use_totp) {
|
||||
return [
|
||||
Placeholder::make('2FA already enabled!'),
|
||||
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'),
|
||||
];
|
||||
}
|
||||
$setupService = app(TwoFactorSetupService::class);
|
||||
|
||||
['image_url_data' => $url] = $setupService->handle($this->getUser());
|
||||
['image_url_data' => $url, 'secret' => $secret] = cache()->remember(
|
||||
"users.{$this->getUser()->id}.2fa.state",
|
||||
now()->addMinutes(5), fn () => $setupService->handle($this->getUser())
|
||||
);
|
||||
|
||||
$options = new QROptions([
|
||||
'svgLogo' => public_path('pelican.svg'),
|
||||
@@ -147,9 +165,19 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
Placeholder::make('qr')
|
||||
->label('Scan QR Code')
|
||||
->content(fn () => new HtmlString("
|
||||
<div style='width: 300px'>$image</div>
|
||||
<div style='width: 300px; background-color: rgb(24, 24, 27);'>$image</div>
|
||||
"))
|
||||
->default('asdfasdf'),
|
||||
->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.'),
|
||||
];
|
||||
}),
|
||||
|
||||
@@ -158,7 +186,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'),
|
||||
TextInput::make('description')->required(),
|
||||
TagsInput::make('allowed_ips')
|
||||
->splitKeys([',', ' ', 'Tab'])
|
||||
->placeholder('Example: 127.0.0.1 or 192.168.1.1')
|
||||
@@ -182,8 +210,9 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
$action->success();
|
||||
}),
|
||||
]),
|
||||
Section::make('API Keys')->columnSpan(2)->schema([
|
||||
Section::make('Keys')->columnSpan(2)->schema([
|
||||
Repeater::make('keys')
|
||||
->label('')
|
||||
->relationship('apiKeys')
|
||||
->addable(false)
|
||||
->itemLabel(fn ($state) => $state['identifier'])
|
||||
@@ -235,4 +264,43 @@ 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Filament\Resources\UserResource\Pages;
|
||||
|
||||
use App\Filament\Resources\UserResource;
|
||||
use App\Services\Exceptions\FilamentExceptionHandler;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use App\Models\User;
|
||||
@@ -66,7 +67,9 @@ class EditUser extends EditRecord
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make(),
|
||||
Actions\DeleteAction::make()
|
||||
->label(fn (User $user) => auth()->user()->id === $user->id ? 'Can\'t Delete Yourself' : ($user->servers()->count() > 0 ? 'User Has Servers' : 'Delete'))
|
||||
->disabled(fn (User $user) => auth()->user()->id === $user->id || $user->servers()->count() > 0),
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
];
|
||||
}
|
||||
@@ -75,4 +78,9 @@ class EditUser extends EditRecord
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function exception($exception, $stopPropagation): void
|
||||
{
|
||||
(new FilamentExceptionHandler())->handle($exception, $stopPropagation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ class EggShareController extends Controller
|
||||
*/
|
||||
public function import(EggImportFormRequest $request): RedirectResponse
|
||||
{
|
||||
$egg = $this->importerService->handle($request->file('import_file'));
|
||||
$egg = $this->importerService->fromFile($request->file('import_file'));
|
||||
$this->alert->success(trans('admin/eggs.notices.imported'))->flash();
|
||||
|
||||
return redirect()->route('admin.eggs.view', ['egg' => $egg->id]);
|
||||
@@ -61,7 +61,7 @@ class EggShareController extends Controller
|
||||
*/
|
||||
public function update(EggImportFormRequest $request, Egg $egg): RedirectResponse
|
||||
{
|
||||
$this->updateImporterService->handle($egg, $request->file('import_file'));
|
||||
$this->updateImporterService->fromFile($egg, $request->file('import_file'));
|
||||
$this->alert->success(trans('admin/eggs.notices.updated_via_import'))->flash();
|
||||
|
||||
return redirect()->route('admin.eggs.view', ['egg' => $egg]);
|
||||
|
||||
@@ -56,7 +56,7 @@ class NodeAutoDeployController extends Controller
|
||||
|
||||
return new JsonResponse([
|
||||
'node' => $node->id,
|
||||
'token' => $key->identifier . decrypt($key->token),
|
||||
'token' => $key->identifier . $key->token,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers\Admin\Nodes;
|
||||
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Node;
|
||||
use Spatie\QueryBuilder\QueryBuilder;
|
||||
use App\Http\Controllers\Controller;
|
||||
@@ -13,7 +12,7 @@ class NodeController extends Controller
|
||||
/**
|
||||
* Returns a listing of nodes on the system.
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
public function index(): View
|
||||
{
|
||||
$nodes = QueryBuilder::for(
|
||||
Node::query()->withCount('servers')
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers\Admin\Nodes;
|
||||
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Node;
|
||||
use Illuminate\Support\Collection;
|
||||
use App\Models\Allocation;
|
||||
@@ -29,16 +28,10 @@ class NodeViewController extends Controller
|
||||
/**
|
||||
* Returns index view for a specific node on the system.
|
||||
*/
|
||||
public function index(Request $request, Node $node): View
|
||||
public function index(Node $node): View
|
||||
{
|
||||
$node->loadCount('servers');
|
||||
|
||||
$stats = Node::query()
|
||||
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk')
|
||||
->join('servers', 'servers.node_id', '=', 'nodes.id')
|
||||
->where('node_id', '=', $node->id)
|
||||
->first();
|
||||
|
||||
return view('admin.nodes.view.index', [
|
||||
'node' => $node,
|
||||
'version' => $this->versionService,
|
||||
@@ -48,7 +41,7 @@ class NodeViewController extends Controller
|
||||
/**
|
||||
* Returns the settings page for a specific node.
|
||||
*/
|
||||
public function settings(Request $request, Node $node): View
|
||||
public function settings(Node $node): View
|
||||
{
|
||||
return view('admin.nodes.view.settings', [
|
||||
'node' => $node,
|
||||
@@ -58,7 +51,7 @@ class NodeViewController extends Controller
|
||||
/**
|
||||
* Return the node configuration page for a specific node.
|
||||
*/
|
||||
public function configuration(Request $request, Node $node): View
|
||||
public function configuration(Node $node): View
|
||||
{
|
||||
return view('admin.nodes.view.configuration', compact('node'));
|
||||
}
|
||||
@@ -66,7 +59,7 @@ class NodeViewController extends Controller
|
||||
/**
|
||||
* Return the node allocation management page.
|
||||
*/
|
||||
public function allocations(Request $request, Node $node): View
|
||||
public function allocations(Node $node): View
|
||||
{
|
||||
$node->setRelation(
|
||||
'allocations',
|
||||
@@ -92,7 +85,7 @@ class NodeViewController extends Controller
|
||||
/**
|
||||
* Return a listing of servers that exist for this specific node.
|
||||
*/
|
||||
public function servers(Request $request, Node $node): View
|
||||
public function servers(Node $node): View
|
||||
{
|
||||
$this->plainInject([
|
||||
'node' => Collection::wrap($node->makeVisible(['daemon_token_id', 'daemon_token']))
|
||||
|
||||
@@ -53,7 +53,6 @@ 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
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers\Admin\Servers;
|
||||
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Server;
|
||||
use Spatie\QueryBuilder\QueryBuilder;
|
||||
use Spatie\QueryBuilder\AllowedFilter;
|
||||
@@ -16,7 +15,7 @@ class ServerController extends Controller
|
||||
* Returns all the servers that exist on the system using a paginated result set. If
|
||||
* a query is passed along in the request it is also passed to the repository function.
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
public function index(): View
|
||||
{
|
||||
$servers = QueryBuilder::for(Server::query()->with('node', 'user', 'allocation'))
|
||||
->allowedFilters([
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Enums\ServerState;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Response;
|
||||
use App\Models\Mount;
|
||||
use App\Models\Server;
|
||||
use App\Models\Database;
|
||||
use App\Models\MountServer;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
use App\Exceptions\DisplayException;
|
||||
@@ -70,7 +70,7 @@ class ServersController extends Controller
|
||||
* @throws \App\Exceptions\DisplayException
|
||||
* @throws \App\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function toggleInstall(Server $server): RedirectResponse
|
||||
public function toggleInstall(Server $server)
|
||||
{
|
||||
if ($server->status === ServerState::InstallFailed) {
|
||||
throw new DisplayException(trans('admin/server.exceptions.marked_as_failed'));
|
||||
@@ -79,9 +79,13 @@ class ServersController extends Controller
|
||||
$server->status = $server->isInstalled() ? ServerState::Installing : null;
|
||||
$server->save();
|
||||
|
||||
$this->alert->success(trans('admin/server.alerts.install_toggled'))->flash();
|
||||
Notification::make()
|
||||
->title('Success!')
|
||||
->body(trans('admin/server.alerts.install_toggled'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return redirect()->route('admin.servers.view.manage', $server->id);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,12 +94,15 @@ class ServersController extends Controller
|
||||
* @throws \App\Exceptions\DisplayException
|
||||
* @throws \App\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function reinstallServer(Server $server): RedirectResponse
|
||||
public function reinstallServer(Server $server)
|
||||
{
|
||||
$this->reinstallService->handle($server);
|
||||
$this->alert->success(trans('admin/server.alerts.server_reinstalled'))->flash();
|
||||
|
||||
return redirect()->route('admin.servers.view.manage', $server->id);
|
||||
Notification::make()
|
||||
->title('Success!')
|
||||
->body(trans('admin/server.alerts.server_reinstalled'))
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -228,12 +235,7 @@ class ServersController extends Controller
|
||||
*/
|
||||
public function addMount(Request $request, Server $server): RedirectResponse
|
||||
{
|
||||
$mountServer = (new MountServer())->forceFill([
|
||||
'mount_id' => $request->input('mount_id'),
|
||||
'server_id' => $server->id,
|
||||
]);
|
||||
|
||||
$mountServer->saveOrFail();
|
||||
$server->mounts()->attach($request->input('mount_id'));
|
||||
|
||||
$this->alert->success('Mount was added successfully.')->flash();
|
||||
|
||||
@@ -245,7 +247,7 @@ class ServersController extends Controller
|
||||
*/
|
||||
public function deleteMount(Server $server, Mount $mount): RedirectResponse
|
||||
{
|
||||
MountServer::where('mount_id', $mount->id)->where('server_id', $server->id)->delete();
|
||||
$server->mounts()->detach($mount);
|
||||
|
||||
$this->alert->success('Mount was removed successfully.')->flash();
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ class UserController extends Controller
|
||||
/**
|
||||
* Display user index page.
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
public function index(): View
|
||||
{
|
||||
$users = QueryBuilder::for(
|
||||
User::query()->select('users.*')
|
||||
|
||||
165
app/Http/Controllers/Api/Application/Mounts/MountController.php
Normal file
165
app/Http/Controllers/Api/Application/Mounts/MountController.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -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'])
|
||||
->allowedSorts(['id', 'uuid', 'memory', 'disk', 'cpu'])
|
||||
->paginate($request->query('per_page') ?? 50);
|
||||
|
||||
return $this->fractal->collection($nodes)
|
||||
|
||||
@@ -9,9 +9,6 @@ use App\Http\Requests\Api\Application\Nodes\GetDeployableNodesRequest;
|
||||
|
||||
class NodeDeploymentController extends ApplicationApiController
|
||||
{
|
||||
/**
|
||||
* NodeDeploymentController constructor.
|
||||
*/
|
||||
public function __construct(private FindViableNodesService $viableNodesService)
|
||||
{
|
||||
parent::__construct();
|
||||
@@ -21,16 +18,17 @@ 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
|
||||
->setMemory($data['memory'])
|
||||
->setDisk($data['disk'])
|
||||
->handle((int) $request->query('per_page'), (int) $request->query('page'));
|
||||
|
||||
$nodes = $this->viableNodesService->handle(
|
||||
$data['memory'] ?? 0,
|
||||
$data['disk'] ?? 0,
|
||||
$data['cpu'] ?? 0,
|
||||
$data['tags'] ?? $data['location_ids'] ?? [],
|
||||
);
|
||||
|
||||
return $this->fractal->collection($nodes)
|
||||
->transformWith($this->getTransformer(NodeTransformer::class))
|
||||
|
||||
@@ -50,7 +50,6 @@ 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
|
||||
{
|
||||
|
||||
@@ -65,9 +65,7 @@ class LoginCheckpointController extends AbstractLoginController
|
||||
return $this->sendLoginResponse($user, $request);
|
||||
}
|
||||
} else {
|
||||
$decrypted = decrypt($user->totp_secret);
|
||||
|
||||
if ($this->google2FA->verifyKey($decrypted, (string) $request->input('authentication_code'), config('panel.auth.2fa.window'))) {
|
||||
if ($this->google2FA->verifyKey($user->totp_secret, (string) $request->input('authentication_code'), config('panel.auth.2fa.window'))) {
|
||||
Event::dispatch(new ProvidedAuthenticationToken($user));
|
||||
|
||||
return $this->sendLoginResponse($user, $request);
|
||||
|
||||
@@ -41,7 +41,7 @@ class DaemonAuthenticate
|
||||
/** @var Node $node */
|
||||
$node = Node::query()->where('daemon_token_id', $parts[0])->firstOrFail();
|
||||
|
||||
if (hash_equals((string) decrypt($node->daemon_token), $parts[1])) {
|
||||
if (hash_equals((string) $node->daemon_token, $parts[1])) {
|
||||
$request->attributes->set('node', $node);
|
||||
|
||||
return $next($request);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?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;
|
||||
}
|
||||
13
app/Http/Requests/Api/Application/Mounts/GetMountRequest.php
Normal file
13
app/Http/Requests/Api/Application/Mounts/GetMountRequest.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?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));
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,11 @@ 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',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,13 +28,14 @@ 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();
|
||||
}
|
||||
@@ -58,12 +59,8 @@ class StoreNodeRequest extends ApplicationApiRequest
|
||||
public function validated($key = null, $default = null): array
|
||||
{
|
||||
$response = parent::validated();
|
||||
$response['daemon_listen'] = $response['daemon_listen'];
|
||||
$response['daemon_sftp'] = $response['daemon_sftp'];
|
||||
$response['daemon_base'] = $response['daemon_base'] ?? (new Node())->getAttribute('daemon_base');
|
||||
|
||||
unset($response['daemon_base'], $response['daemon_listen'], $response['daemon_sftp']);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,11 +56,10 @@ class StoreServerRequest extends ApplicationApiRequest
|
||||
// Automatic deployment rules
|
||||
'deploy' => 'sometimes|required|array',
|
||||
'deploy.locations' => 'array',
|
||||
'deploy.locations.*' => 'integer|min:1',
|
||||
'deploy.locations.*' => 'required_with: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',
|
||||
];
|
||||
}
|
||||
@@ -123,6 +122,15 @@ 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;
|
||||
});
|
||||
@@ -139,6 +147,7 @@ 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;
|
||||
|
||||
@@ -22,7 +22,6 @@ 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()
|
||||
@@ -88,14 +87,6 @@ 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.
|
||||
*/
|
||||
|
||||
@@ -28,6 +28,7 @@ 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
|
||||
*
|
||||
@@ -83,7 +84,7 @@ class ApiKey extends Model
|
||||
*/
|
||||
public const KEY_LENGTH = 32;
|
||||
|
||||
public const RESOURCES = ['servers', 'nodes', 'allocations', 'users', 'eggs', 'database_hosts', 'server_databases'];
|
||||
public const RESOURCES = ['servers', 'nodes', 'allocations', 'users', 'eggs', 'database_hosts', 'server_databases', 'mounts'];
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
@@ -109,6 +110,7 @@ class ApiKey extends Model
|
||||
'r_' . AdminAcl::RESOURCE_EGGS,
|
||||
'r_' . AdminAcl::RESOURCE_NODES,
|
||||
'r_' . AdminAcl::RESOURCE_SERVERS,
|
||||
'r_' . AdminAcl::RESOURCE_MOUNTS,
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -137,6 +139,7 @@ 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
|
||||
@@ -146,6 +149,7 @@ 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',
|
||||
@@ -155,6 +159,7 @@ class ApiKey extends Model
|
||||
'r_' . AdminAcl::RESOURCE_EGGS => 'int',
|
||||
'r_' . AdminAcl::RESOURCE_NODES => 'int',
|
||||
'r_' . AdminAcl::RESOURCE_SERVERS => 'int',
|
||||
'r_' . AdminAcl::RESOURCE_MOUNTS => 'int',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -184,7 +189,7 @@ class ApiKey extends Model
|
||||
$identifier = substr($token, 0, self::IDENTIFIER_LENGTH);
|
||||
|
||||
$model = static::where('identifier', $identifier)->first();
|
||||
if (!is_null($model) && decrypt($model->token) === substr($token, strlen($identifier))) {
|
||||
if (!is_null($model) && $model->token === substr($token, strlen($identifier))) {
|
||||
return $model;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use App\Contracts\Extensions\HashidsInterface;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
@@ -64,6 +62,7 @@ class Database extends Model
|
||||
'server_id' => 'integer',
|
||||
'database_host_id' => 'integer',
|
||||
'max_connections' => 'integer',
|
||||
'password' => 'encrypted',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -72,26 +71,6 @@ 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.
|
||||
*/
|
||||
|
||||
@@ -60,6 +60,7 @@ class DatabaseHost extends Model
|
||||
'id' => 'integer',
|
||||
'max_databases' => 'integer',
|
||||
'node_id' => 'integer',
|
||||
'password' => 'encrypted',
|
||||
'created_at' => 'immutable_datetime',
|
||||
'updated_at' => 'immutable_datetime',
|
||||
];
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class MountServer extends Model
|
||||
{
|
||||
protected $table = 'mount_server';
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $primaryKey = null;
|
||||
|
||||
public $incrementing = false;
|
||||
}
|
||||
@@ -26,11 +26,14 @@ 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
|
||||
@@ -61,9 +64,6 @@ 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,8 +71,9 @@ class Node extends Model
|
||||
'public', 'name',
|
||||
'fqdn', 'scheme', 'behind_proxy',
|
||||
'memory', 'memory_overallocate', 'disk',
|
||||
'disk_overallocate', 'upload_size', 'daemon_base',
|
||||
'daemon_sftp', 'daemon_listen',
|
||||
'disk_overallocate', 'cpu', 'cpu_overallocate',
|
||||
'upload_size', 'daemon_base',
|
||||
'daemon_sftp', 'daemon_sftp_alias', 'daemon_listen',
|
||||
'description', 'maintenance_mode',
|
||||
];
|
||||
|
||||
@@ -87,8 +88,11 @@ 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',
|
||||
@@ -104,6 +108,8 @@ 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,
|
||||
@@ -116,8 +122,10 @@ 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',
|
||||
@@ -134,7 +142,7 @@ class Node extends Model
|
||||
{
|
||||
static::creating(function (self $node) {
|
||||
$node->uuid = Str::uuid();
|
||||
$node->daemon_token = encrypt(Str::random(self::DAEMON_TOKEN_LENGTH));
|
||||
$node->daemon_token = Str::random(self::DAEMON_TOKEN_LENGTH);
|
||||
$node->daemon_token_id = Str::random(self::DAEMON_TOKEN_ID_LENGTH);
|
||||
|
||||
return true;
|
||||
@@ -162,7 +170,7 @@ class Node extends Model
|
||||
'debug' => false,
|
||||
'uuid' => $this->uuid,
|
||||
'token_id' => $this->daemon_token_id,
|
||||
'token' => decrypt($this->daemon_token),
|
||||
'token' => $this->daemon_token,
|
||||
'api' => [
|
||||
'host' => '0.0.0.0',
|
||||
'port' => $this->daemon_listen,
|
||||
@@ -200,16 +208,6 @@ 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;
|
||||
@@ -239,12 +237,30 @@ 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): bool
|
||||
public function isViable(int $memory, int $disk, int $cpu): bool
|
||||
{
|
||||
$memoryLimit = $this->memory * (1 + ($this->memory_overallocate / 100));
|
||||
$diskLimit = $this->disk * (1 + ($this->disk_overallocate / 100));
|
||||
if ($this->memory_overallocate >= 0) {
|
||||
$memoryLimit = $this->memory * (1 + ($this->memory_overallocate / 100));
|
||||
if ($this->servers_sum_memory + $memory > $memoryLimit) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return ($this->sum_memory + $memory) <= $memoryLimit && ($this->sum_disk + $disk) <= $diskLimit;
|
||||
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;
|
||||
}
|
||||
|
||||
public static function getForServerCreation()
|
||||
|
||||
@@ -6,6 +6,8 @@ class DeploymentObject
|
||||
{
|
||||
private bool $dedicated = false;
|
||||
|
||||
private array $tags = [];
|
||||
|
||||
private array $ports = [];
|
||||
|
||||
public function isDedicated(): bool
|
||||
@@ -31,4 +33,17 @@ class DeploymentObject
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTags(): array
|
||||
{
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
public function setTags(array $tags): self
|
||||
{
|
||||
$this->tags = $tags;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,10 +4,8 @@ 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
|
||||
@@ -25,7 +23,6 @@ use App\Contracts\Extensions\HashidsInterface;
|
||||
* @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
|
||||
*/
|
||||
@@ -124,14 +121,6 @@ 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.
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Models;
|
||||
use App\Enums\ServerState;
|
||||
use App\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
@@ -13,7 +14,6 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||
use App\Exceptions\Http\Server\ServerStateConflictException;
|
||||
|
||||
/**
|
||||
@@ -184,6 +184,7 @@ class Server extends Model
|
||||
self::UPDATED_AT => 'datetime',
|
||||
'deleted_at' => 'datetime',
|
||||
'installed_at' => 'datetime',
|
||||
'docker_labels' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -310,12 +311,9 @@ class Server extends Model
|
||||
return $this->hasMany(Backup::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all mounts that have this server has mounted.
|
||||
*/
|
||||
public function mounts(): HasManyThrough
|
||||
public function mounts(): BelongsToMany
|
||||
{
|
||||
return $this->hasManyThrough(Mount::class, MountServer::class, 'server_id', 'id', 'id', 'mount_id');
|
||||
return $this->belongsToMany(Mount::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -52,14 +52,6 @@ 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.
|
||||
*/
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
|
||||
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
|
||||
@@ -18,7 +16,6 @@ use App\Contracts\Extensions\HashidsInterface;
|
||||
* @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
|
||||
*/
|
||||
@@ -96,14 +93,6 @@ 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.
|
||||
*/
|
||||
|
||||
@@ -31,7 +31,7 @@ trait HasAccessTokens
|
||||
'user_id' => $this->id,
|
||||
'key_type' => ApiKey::TYPE_ACCOUNT,
|
||||
'identifier' => ApiKey::generateTokenIdentifier(ApiKey::TYPE_ACCOUNT),
|
||||
'token' => encrypt($plain = Str::random(ApiKey::KEY_LENGTH)),
|
||||
'token' => $plain = Str::random(ApiKey::KEY_LENGTH),
|
||||
'memo' => $memo ?? '',
|
||||
'allowed_ips' => $ips ?? [],
|
||||
]);
|
||||
|
||||
@@ -171,6 +171,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
'use_totp' => 'boolean',
|
||||
'gravatar' => 'boolean',
|
||||
'totp_authenticated_at' => 'datetime',
|
||||
'totp_secret' => 'encrypted',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,14 @@ 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;
|
||||
@@ -29,8 +31,9 @@ class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
Schema::defaultStringLength(191);
|
||||
|
||||
View::share('appVersion', $this->versionData()['version'] ?? 'undefined');
|
||||
View::share('appIsGit', $this->versionData()['is_git'] ?? false);
|
||||
$versionData = app(SoftwareVersionService::class)->versionData();
|
||||
View::share('appVersion', $versionData['version'] ?? 'undefined');
|
||||
View::share('appIsGit', $versionData['is_git'] ?? false);
|
||||
|
||||
Paginator::useBootstrap();
|
||||
|
||||
@@ -59,7 +62,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
'daemon',
|
||||
fn (Node $node, array $headers = []) => Http::acceptJson()
|
||||
->asJson()
|
||||
->withToken($node->getDecryptedKey())
|
||||
->withToken($node->daemon_token)
|
||||
->withHeaders($headers)
|
||||
->withOptions(['verify' => (bool) app()->environment('production')])
|
||||
->timeout(config('panel.guzzle.timeout'))
|
||||
@@ -70,9 +73,11 @@ 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']]);
|
||||
Scramble::registerApi('remote', ['api_path' => 'api/remote', '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,34 +98,6 @@ class AppServiceProvider extends ServiceProvider
|
||||
Scramble::ignoreDefaultRoutes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return version information for the footer.
|
||||
*/
|
||||
protected function versionData(): array
|
||||
{
|
||||
return cache()->remember('git-version', 5, function () {
|
||||
if (file_exists(base_path('.git/HEAD'))) {
|
||||
$head = explode(' ', file_get_contents(base_path('.git/HEAD')));
|
||||
|
||||
if (array_key_exists(1, $head)) {
|
||||
$path = base_path('.git/' . trim($head[1]));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($path) && file_exists($path)) {
|
||||
return [
|
||||
'version' => substr(file_get_contents($path), 0, 8),
|
||||
'is_git' => true,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'version' => config('app.version'),
|
||||
'is_git' => false,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
public function bootAuth(): void
|
||||
{
|
||||
Sanctum::usePersonalAccessTokenModel(ApiKey::class);
|
||||
|
||||
@@ -35,11 +35,13 @@ class AdminPanelProvider extends PanelProvider
|
||||
->default()
|
||||
->id('admin')
|
||||
->path('admin')
|
||||
->topNavigation(config('panel.filament.top-navigation', false))
|
||||
->topNavigation(config('panel.filament.top-navigation', true))
|
||||
->login()
|
||||
->homeUrl('/')
|
||||
->favicon('/pelican.ico')
|
||||
->brandName('Pelican')
|
||||
->favicon(config('app.favicon', '/pelican.ico'))
|
||||
->brandName(config('app.name', 'Pelican'))
|
||||
->brandLogo(config('app.logo'))
|
||||
->brandLogoHeight('2rem')
|
||||
->profile(EditProfile::class, false)
|
||||
->colors([
|
||||
'danger' => Color::Red,
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
<?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');
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Database;
|
||||
use Illuminate\Foundation\Http\Middleware\TrimStrings;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
@@ -29,11 +28,6 @@ class RouteServiceProvider extends ServiceProvider
|
||||
return preg_match(self::FILE_PATH_REGEX, $request->getPathInfo()) === 1;
|
||||
});
|
||||
|
||||
// This is needed to make use of the "resolveRouteBinding" functionality in the
|
||||
// model. Without it you'll never trigger that logic flow thus resulting in a 404
|
||||
// error because we request databases with a HashID, and not with a normal ID.
|
||||
Route::model('database', Database::class);
|
||||
|
||||
$this->routes(function () {
|
||||
Route::middleware('web')->group(function () {
|
||||
Route::middleware(['auth.session', RequireTwoFactorAuthentication::class])
|
||||
|
||||
34
app/Rules/Port.php
Normal file
34
app/Rules/Port.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Rules;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
|
||||
class Port implements ValidationRule
|
||||
{
|
||||
/**
|
||||
* Run the validation rule.
|
||||
*
|
||||
* @param \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString $fail
|
||||
*/
|
||||
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||
{
|
||||
if (!is_numeric($value)) {
|
||||
$fail('The :attribute must be numeric.');
|
||||
}
|
||||
|
||||
$value = intval($value);
|
||||
if (floatval($value) !== (float) $value) {
|
||||
$fail('The :attribute must be an integer.');
|
||||
}
|
||||
|
||||
if ($value < 0) {
|
||||
$fail('The :attribute must be greater or equal to 0.');
|
||||
}
|
||||
|
||||
if ($value > 65535) {
|
||||
$fail('The :attribute must be less or equal to 65535.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ class AdminAcl
|
||||
public const RESOURCE_EGGS = 'eggs';
|
||||
public const RESOURCE_DATABASE_HOSTS = 'database_hosts';
|
||||
public const RESOURCE_SERVER_DATABASES = 'server_databases';
|
||||
public const RESOURCE_MOUNTS = 'mounts';
|
||||
|
||||
/**
|
||||
* Determine if an API key has permission to perform a specific read/write operation.
|
||||
|
||||
@@ -31,7 +31,7 @@ class KeyCreationService
|
||||
$data = array_merge($data, [
|
||||
'key_type' => $this->keyType,
|
||||
'identifier' => ApiKey::generateTokenIdentifier($this->keyType),
|
||||
'token' => encrypt(str_random(ApiKey::KEY_LENGTH)),
|
||||
'token' => str_random(ApiKey::KEY_LENGTH),
|
||||
]);
|
||||
|
||||
if ($this->keyType === ApiKey::TYPE_APPLICATION) {
|
||||
|
||||
@@ -86,9 +86,7 @@ class DatabaseManagementService
|
||||
$data = array_merge($data, [
|
||||
'server_id' => $server->id,
|
||||
'username' => sprintf('u%d_%s', $server->id, str_random(10)),
|
||||
'password' => encrypt(
|
||||
Utilities::randomStringWithSpecialCharacters(24)
|
||||
),
|
||||
'password' => Utilities::randomStringWithSpecialCharacters(24),
|
||||
]);
|
||||
|
||||
return $this->connection->transaction(function () use ($data, &$database) {
|
||||
@@ -100,7 +98,7 @@ class DatabaseManagementService
|
||||
$database->createUser(
|
||||
$database->username,
|
||||
$database->remote,
|
||||
decrypt($database->password),
|
||||
$database->password,
|
||||
$database->max_connections
|
||||
);
|
||||
$database->assignUserToDatabase($database->database, $database->username, $database->remote);
|
||||
|
||||
@@ -33,7 +33,7 @@ class DatabasePasswordService
|
||||
$this->dynamic->set('dynamic', $database->database_host_id);
|
||||
|
||||
$database->update([
|
||||
'password' => encrypt($password),
|
||||
'password' => $password,
|
||||
]);
|
||||
|
||||
$database->dropUser($database->username, $database->remote);
|
||||
|
||||
@@ -28,7 +28,7 @@ class HostCreationService
|
||||
{
|
||||
return $this->connection->transaction(function () use ($data) {
|
||||
$host = DatabaseHost::query()->create([
|
||||
'password' => encrypt(array_get($data, 'password')),
|
||||
'password' => array_get($data, 'password'),
|
||||
'name' => array_get($data, 'name'),
|
||||
'host' => array_get($data, 'host'),
|
||||
'port' => array_get($data, 'port'),
|
||||
|
||||
@@ -26,9 +26,7 @@ class HostUpdateService
|
||||
*/
|
||||
public function handle(int $hostId, array $data): DatabaseHost
|
||||
{
|
||||
if (!empty(array_get($data, 'password'))) {
|
||||
$data['password'] = encrypt($data['password']);
|
||||
} else {
|
||||
if (empty(array_get($data, 'password'))) {
|
||||
unset($data['password']);
|
||||
}
|
||||
|
||||
|
||||
@@ -90,11 +90,9 @@ class AllocationSelectionService
|
||||
*/
|
||||
private function getRandomAllocation(array $nodes = [], array $ports = [], bool $dedicated = false): ?Allocation
|
||||
{
|
||||
$query = Allocation::query()->whereNull('server_id');
|
||||
|
||||
if (!empty($nodes)) {
|
||||
$query->whereIn('node_id', $nodes);
|
||||
}
|
||||
$query = Allocation::query()
|
||||
->whereNull('server_id')
|
||||
->whereIn('node_id', $nodes);
|
||||
|
||||
if (!empty($ports)) {
|
||||
$query->where(function ($inner) use ($ports) {
|
||||
|
||||
@@ -3,81 +3,31 @@
|
||||
namespace App\Services\Deployment;
|
||||
|
||||
use App\Models\Node;
|
||||
use Webmozart\Assert\Assert;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use App\Exceptions\Service\Deployment\NoViableNodeException;
|
||||
|
||||
class FindViableNodesService
|
||||
{
|
||||
protected ?int $disk = null;
|
||||
protected ?int $memory = null;
|
||||
|
||||
/**
|
||||
* Set the amount of disk that will be used by the server being created. Nodes will be
|
||||
* filtered out if they do not have enough available free disk space for this server
|
||||
* to be placed on.
|
||||
*/
|
||||
public function setDisk(int $disk): self
|
||||
{
|
||||
$this->disk = $disk;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the amount of memory that this server will be using. As with disk space, nodes that
|
||||
* do not have enough free memory will be filtered out.
|
||||
*/
|
||||
public function setMemory(int $memory): self
|
||||
{
|
||||
$this->memory = $memory;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of nodes that meet the provided requirements and can then
|
||||
* Returns a collection of nodes that meet the provided requirements and can then
|
||||
* be passed to the AllocationSelectionService to return a single allocation.
|
||||
*
|
||||
* This functionality is used for automatic deployments of servers and will
|
||||
* attempt to find all nodes in the defined locations that meet the disk and
|
||||
* memory availability requirements. Any nodes not meeting those requirements
|
||||
* attempt to find all nodes in the defined locations that meet the memory, disk
|
||||
* and cpu availability requirements. Any nodes not meeting those requirements
|
||||
* are tossed out, as are any nodes marked as non-public, meaning automatic
|
||||
* deployments should not be done against them.
|
||||
*
|
||||
* @param int|null $page If provided the results will be paginated by returning
|
||||
* up to 50 nodes at a time starting at the provided page.
|
||||
* If "null" is provided as the value no pagination will
|
||||
* be used.
|
||||
*
|
||||
* @throws \App\Exceptions\Service\Deployment\NoViableNodeException
|
||||
*/
|
||||
public function handle(int $perPage = null, int $page = null): LengthAwarePaginator|Collection
|
||||
public function handle(int $memory = 0, int $disk = 0, int $cpu = 0, $tags = []): Collection
|
||||
{
|
||||
Assert::integer($this->disk, 'Disk space must be an int, got %s');
|
||||
Assert::integer($this->memory, 'Memory usage must be an int, got %s');
|
||||
$nodes = Node::query()
|
||||
->withSum('servers', 'memory')
|
||||
->withSum('servers', 'disk')
|
||||
->withSum('servers', 'cpu')
|
||||
->where('public', true)
|
||||
->get();
|
||||
|
||||
$query = Node::query()->select('nodes.*')
|
||||
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory')
|
||||
->selectRaw('IFNULL(SUM(servers.disk), 0) as sum_disk')
|
||||
->leftJoin('servers', 'servers.node_id', '=', 'nodes.id')
|
||||
->where('nodes.public', 1);
|
||||
|
||||
$results = $query->groupBy('nodes.id')
|
||||
->havingRaw('(IFNULL(SUM(servers.memory), 0) + ?) <= (nodes.memory * (1 + (nodes.memory_overallocate / 100)))', [$this->memory])
|
||||
->havingRaw('(IFNULL(SUM(servers.disk), 0) + ?) <= (nodes.disk * (1 + (nodes.disk_overallocate / 100)))', [$this->disk]);
|
||||
|
||||
if (!is_null($page)) {
|
||||
$results = $results->paginate($perPage ?? 50, ['*'], 'page', $page);
|
||||
} else {
|
||||
$results = $results->get()->toBase();
|
||||
}
|
||||
|
||||
if ($results->isEmpty()) {
|
||||
throw new NoViableNodeException(trans('exceptions.deployment.no_viable_nodes'));
|
||||
}
|
||||
|
||||
return $results;
|
||||
return $nodes
|
||||
->filter(fn (Node $node) => !$tags || collect($node->tags)->intersect($tags))
|
||||
->filter(fn (Node $node) => $node->isViable($memory, $disk, $cpu));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ class EggConfigurationService
|
||||
{
|
||||
// Get the legacy configuration structure for the server so that we
|
||||
// can property map the egg placeholders to values.
|
||||
$structure = $this->configurationStructureService->handle($server, [], true);
|
||||
$structure = $this->configurationStructureService->handle($server);
|
||||
|
||||
$response = [];
|
||||
// Normalize the output of the configuration for the new Daemon to more
|
||||
@@ -124,38 +124,6 @@ class EggConfigurationService
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the legacy modifies from eggs with their new counterpart. The legacy Daemon would
|
||||
* set SERVER_MEMORY, SERVER_IP, and SERVER_PORT with their respective values on the Daemon
|
||||
* side. Ensure that anything referencing those properly replaces them with the matching config
|
||||
* value.
|
||||
*/
|
||||
protected function replaceLegacyModifiers(string $key, string $value): string
|
||||
{
|
||||
switch ($key) {
|
||||
case 'config.docker.interface':
|
||||
$replace = 'config.docker.network.interface';
|
||||
break;
|
||||
case 'server.build.env.SERVER_MEMORY':
|
||||
case 'env.SERVER_MEMORY':
|
||||
$replace = 'server.build.memory';
|
||||
break;
|
||||
case 'server.build.env.SERVER_IP':
|
||||
case 'env.SERVER_IP':
|
||||
$replace = 'server.build.default.ip';
|
||||
break;
|
||||
case 'server.build.env.SERVER_PORT':
|
||||
case 'env.SERVER_PORT':
|
||||
$replace = 'server.build.default.port';
|
||||
break;
|
||||
default:
|
||||
// By default, we don't need to change anything, only if we ended up matching a specific legacy item.
|
||||
$replace = $key;
|
||||
}
|
||||
|
||||
return str_replace("{{{$key}}}", "{{{$replace}}}", $value);
|
||||
}
|
||||
|
||||
protected function matchAndReplaceKeys(mixed $value, array $structure): mixed
|
||||
{
|
||||
preg_match_all('/{{(?<key>[\w.-]*)}}/', $value, $matches);
|
||||
@@ -175,8 +143,6 @@ class EggConfigurationService
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $this->replaceLegacyModifiers($key, $value);
|
||||
|
||||
// We don't want to do anything with config keys since the Daemon will need to handle
|
||||
// that. For example, the Spigot egg uses "config.docker.interface" to identify the Docker
|
||||
// interface to proxy through, but the Panel would be unaware of that.
|
||||
@@ -198,7 +164,7 @@ class EggConfigurationService
|
||||
// variable from the server configuration.
|
||||
$plucked = Arr::get(
|
||||
$structure,
|
||||
preg_replace('/^env\./', 'build.env.', $key),
|
||||
preg_replace('/^env\./', 'build.environment.', $key),
|
||||
''
|
||||
);
|
||||
|
||||
|
||||
@@ -10,6 +10,16 @@ use App\Exceptions\Service\InvalidFileUploadException;
|
||||
|
||||
class EggParserService
|
||||
{
|
||||
public const UPGRADE_VARIABLES = [
|
||||
'server.build.env.SERVER_IP' => 'server.allocations.default.ip',
|
||||
'server.build.default.ip' => 'server.allocations.default.ip',
|
||||
'server.build.env.SERVER_PORT' => 'server.allocations.default.port',
|
||||
'server.build.default.port' => 'server.allocations.default.port',
|
||||
'server.build.env.SERVER_MEMORY' => 'server.build.memory_limit',
|
||||
'server.build.memory' => 'server.build.memory_limit',
|
||||
'server.build.env' => 'server.build.environment',
|
||||
];
|
||||
|
||||
/**
|
||||
* Takes an uploaded file and parses out the egg configuration from within.
|
||||
*
|
||||
@@ -26,11 +36,20 @@ class EggParserService
|
||||
|
||||
$version = $parsed['meta']['version'] ?? '';
|
||||
|
||||
return match ($version) {
|
||||
$parsed = match ($version) {
|
||||
'PTDL_v1' => $this->convertToV2($parsed),
|
||||
'PTDL_v2' => $parsed,
|
||||
default => throw new InvalidFileUploadException('The JSON file provided is not in a format that can be recognized.')
|
||||
};
|
||||
|
||||
// Make sure we only use recent variable format from now on
|
||||
$parsed['config']['files'] = str_replace(
|
||||
array_keys(self::UPGRADE_VARIABLES),
|
||||
array_values(self::UPGRADE_VARIABLES),
|
||||
$parsed['config']['files'] ?? '',
|
||||
);
|
||||
|
||||
return $parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,6 +25,7 @@ class EggExporterService
|
||||
'exported_at' => Carbon::now()->toAtomString(),
|
||||
'name' => $egg->name,
|
||||
'author' => $egg->author,
|
||||
'uuid' => $egg->uuid,
|
||||
'description' => $egg->description,
|
||||
'features' => $egg->features,
|
||||
'docker_images' => $egg->docker_images,
|
||||
|
||||
@@ -9,6 +9,7 @@ use Illuminate\Http\UploadedFile;
|
||||
use App\Models\EggVariable;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use App\Services\Eggs\EggParserService;
|
||||
use Spatie\TemporaryDirectory\TemporaryDirectory;
|
||||
|
||||
class EggImporterService
|
||||
{
|
||||
@@ -21,13 +22,16 @@ class EggImporterService
|
||||
*
|
||||
* @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable
|
||||
*/
|
||||
public function handle(UploadedFile $file): Egg
|
||||
public function fromFile(UploadedFile $file): Egg
|
||||
{
|
||||
$parsed = $this->parser->handle($file);
|
||||
|
||||
return $this->connection->transaction(function () use ($parsed) {
|
||||
$egg = (new Egg())->forceFill([
|
||||
'uuid' => Uuid::uuid4()->toString(),
|
||||
$uuid = $parsed['uuid'] ?? Uuid::uuid4()->toString();
|
||||
$egg = Egg::where('uuid', $uuid)->first() ?? new Egg();
|
||||
|
||||
$egg = $egg->forceFill([
|
||||
'uuid' => $uuid,
|
||||
'author' => Arr::get($parsed, 'author'),
|
||||
'copy_script_from' => null,
|
||||
]);
|
||||
@@ -42,4 +46,20 @@ class EggImporterService
|
||||
return $egg;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Take an url and parse it into a new egg.
|
||||
*
|
||||
* @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable
|
||||
*/
|
||||
public function fromUrl(string $url): Egg
|
||||
{
|
||||
$info = pathinfo($url);
|
||||
$tmpDir = TemporaryDirectory::make()->deleteWhenDestroyed();
|
||||
$tmpPath = $tmpDir->path($info['basename']);
|
||||
|
||||
file_put_contents($tmpPath, file_get_contents($url));
|
||||
|
||||
return $this->fromFile(new UploadedFile($tmpPath, $info['basename'], 'application/json'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use Illuminate\Support\Collection;
|
||||
use App\Models\EggVariable;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use App\Services\Eggs\EggParserService;
|
||||
use Spatie\TemporaryDirectory\TemporaryDirectory;
|
||||
|
||||
class EggUpdateImporterService
|
||||
{
|
||||
@@ -23,7 +24,7 @@ class EggUpdateImporterService
|
||||
*
|
||||
* @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable
|
||||
*/
|
||||
public function handle(Egg $egg, UploadedFile $file): Egg
|
||||
public function fromFile(Egg $egg, UploadedFile $file): Egg
|
||||
{
|
||||
$parsed = $this->parser->handle($file);
|
||||
|
||||
@@ -47,4 +48,20 @@ class EggUpdateImporterService
|
||||
return $egg->refresh();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing Egg using an url.
|
||||
*
|
||||
* @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable
|
||||
*/
|
||||
public function fromUrl(Egg $egg, string $url): Egg
|
||||
{
|
||||
$info = pathinfo($url);
|
||||
$tmpDir = TemporaryDirectory::make()->deleteWhenDestroyed();
|
||||
$tmpPath = $tmpDir->path($info['basename']);
|
||||
|
||||
file_put_contents($tmpPath, file_get_contents($url));
|
||||
|
||||
return $this->fromFile($egg, new UploadedFile($tmpPath, $info['basename'], 'application/json'));
|
||||
}
|
||||
}
|
||||
|
||||
24
app/Services/Exceptions/FilamentExceptionHandler.php
Normal file
24
app/Services/Exceptions/FilamentExceptionHandler.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Filament\Notifications\Notification;
|
||||
|
||||
class FilamentExceptionHandler
|
||||
{
|
||||
public function handle(Exception $exception, callable $stopPropagation): void
|
||||
{
|
||||
Notification::make()
|
||||
->title($exception->title ?? null)
|
||||
->body($exception->body ?? $exception->getMessage())
|
||||
->color($exception->color ?? 'danger')
|
||||
->icon($exception->icon ?? 'tabler-x')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
if ($this->stopPropagation ?? true) {
|
||||
$stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,14 @@ class SoftwareVersionService
|
||||
return Arr::get(self::$result, 'discord') ?? 'https://pelican.dev/discord';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the donation URL.
|
||||
*/
|
||||
public function getDonations(): string
|
||||
{
|
||||
return Arr::get(self::$result, 'donate') ?? 'https://pelican.dev/donate';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the current version of the panel is the latest.
|
||||
*/
|
||||
@@ -93,8 +101,28 @@ class SoftwareVersionService
|
||||
});
|
||||
}
|
||||
|
||||
public function getDonations(): string
|
||||
public function versionData(): array
|
||||
{
|
||||
return 'https://github.com';
|
||||
return cache()->remember('git-version', 5, function () {
|
||||
if (file_exists(base_path('.git/HEAD'))) {
|
||||
$head = explode(' ', file_get_contents(base_path('.git/HEAD')));
|
||||
|
||||
if (array_key_exists(1, $head)) {
|
||||
$path = base_path('.git/' . trim($head[1]));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($path) && file_exists($path)) {
|
||||
return [
|
||||
'version' => 'canary (' . substr(file_get_contents($path), 0, 8) . ')',
|
||||
'is_git' => true,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'version' => config('app.version'),
|
||||
'is_git' => false,
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ class NodeCreationService
|
||||
public function handle(array $data): Node
|
||||
{
|
||||
$data['uuid'] = Uuid::uuid4()->toString();
|
||||
$data['daemon_token'] = encrypt(Str::random(Node::DAEMON_TOKEN_LENGTH));
|
||||
$data['daemon_token'] = Str::random(Node::DAEMON_TOKEN_LENGTH);
|
||||
$data['daemon_token_id'] = Str::random(Node::DAEMON_TOKEN_ID_LENGTH);
|
||||
|
||||
return Node::query()->create($data);
|
||||
|
||||
@@ -63,7 +63,7 @@ class NodeJWTService
|
||||
public function handle(Node $node, ?string $identifiedBy, string $algo = 'md5'): Plain
|
||||
{
|
||||
$identifier = hash($algo, $identifiedBy);
|
||||
$config = Configuration::forSymmetricSigner(new Sha256(), InMemory::plainText($node->getDecryptedKey()));
|
||||
$config = Configuration::forSymmetricSigner(new Sha256(), InMemory::plainText($node->daemon_token));
|
||||
|
||||
$builder = $config->builder(new TimestampDates())
|
||||
->issuedBy(config('app.url'))
|
||||
|
||||
@@ -27,23 +27,19 @@ class NodeUpdateService
|
||||
*/
|
||||
public function handle(Node $node, array $data, bool $resetToken = false): Node
|
||||
{
|
||||
$data['id'] = $node->id;
|
||||
|
||||
if ($resetToken) {
|
||||
$data['daemon_token'] = encrypt(Str::random(Node::DAEMON_TOKEN_LENGTH));
|
||||
$data['daemon_token'] = Str::random(Node::DAEMON_TOKEN_LENGTH);
|
||||
$data['daemon_token_id'] = Str::random(Node::DAEMON_TOKEN_ID_LENGTH);
|
||||
}
|
||||
|
||||
[$updated, $exception] = $this->connection->transaction(function () use ($data, $node) {
|
||||
/** @var \App\Models\Node $updated */
|
||||
$updated = $node->replicate()->forceFill($data)->save();
|
||||
|
||||
$updated = $node->replicate();
|
||||
$updated->exists = true;
|
||||
$updated->forceFill($data)->save();
|
||||
try {
|
||||
// If we're changing the FQDN for the node, use the newly provided FQDN for the connection
|
||||
// address. This should alleviate issues where the node gets pointed to a "valid" FQDN that
|
||||
// isn't actually running the daemon software, and therefore you can't actually change it
|
||||
// back.
|
||||
//
|
||||
// This makes more sense anyways, because only the Panel uses the FQDN for connecting, the
|
||||
// node doesn't actually care about this.
|
||||
$node->fqdn = $updated->fqdn;
|
||||
|
||||
$this->configurationRepository->setNode($node)->update($updated);
|
||||
|
||||
@@ -20,7 +20,7 @@ class ServerConfigurationStructureService
|
||||
* DO NOT MODIFY THIS FUNCTION. This powers legacy code handling for the new daemon
|
||||
* daemon, if you modify the structure eggs will break unexpectedly.
|
||||
*/
|
||||
public function handle(Server $server, array $override = [], bool $legacy = false): array
|
||||
public function handle(Server $server, array $override = []): array
|
||||
{
|
||||
$clone = $server;
|
||||
// If any overrides have been set on this call make sure to update them on the
|
||||
@@ -32,17 +32,15 @@ class ServerConfigurationStructureService
|
||||
}
|
||||
}
|
||||
|
||||
return $legacy
|
||||
? $this->returnLegacyFormat($clone)
|
||||
: $this->returnCurrentFormat($clone);
|
||||
return $this->returnFormat($clone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the new data format used for the daemon.
|
||||
* Returns the data format used for the daemon.
|
||||
*/
|
||||
protected function returnCurrentFormat(Server $server): array
|
||||
protected function returnFormat(Server $server): array
|
||||
{
|
||||
return [
|
||||
$response = [
|
||||
'uuid' => $server->uuid,
|
||||
'meta' => [
|
||||
'name' => $server->name,
|
||||
@@ -59,8 +57,6 @@ class ServerConfigurationStructureService
|
||||
'cpu_limit' => $server->cpu,
|
||||
'threads' => $server->threads,
|
||||
'disk_space' => $server->disk,
|
||||
// This field is deprecated — use "oom_killer".
|
||||
'oom_disabled' => !$server->oom_killer,
|
||||
'oom_killer' => $server->oom_killer,
|
||||
],
|
||||
'container' => [
|
||||
@@ -75,54 +71,27 @@ class ServerConfigurationStructureService
|
||||
],
|
||||
'mappings' => $server->getAllocationMappings(),
|
||||
],
|
||||
'mounts' => $server->mounts->map(function (Mount $mount) {
|
||||
return [
|
||||
'source' => $mount->source,
|
||||
'target' => $mount->target,
|
||||
'read_only' => $mount->read_only,
|
||||
];
|
||||
}),
|
||||
'egg' => [
|
||||
'id' => $server->egg->uuid,
|
||||
'file_denylist' => $server->egg->inherit_file_denylist,
|
||||
],
|
||||
];
|
||||
|
||||
if (!empty($server->docker_labels)) {
|
||||
$response['labels'] = $server->docker_labels;
|
||||
}
|
||||
|
||||
if ($server->mounts->isNotEmpty()) {
|
||||
$response['mounts'] = $server->mounts->map(function (Mount $mount) {
|
||||
return [
|
||||
'source' => $mount->source,
|
||||
'target' => $mount->target,
|
||||
'read_only' => $mount->read_only,
|
||||
];
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the legacy server data format to continue support for old egg configurations
|
||||
* that have not yet been updated.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
protected function returnLegacyFormat(Server $server): array
|
||||
{
|
||||
return [
|
||||
'uuid' => $server->uuid,
|
||||
'build' => [
|
||||
'default' => [
|
||||
'ip' => $server->allocation->ip,
|
||||
'port' => $server->allocation->port,
|
||||
],
|
||||
'ports' => $server->allocations->groupBy('ip')->map(function ($item) {
|
||||
return $item->pluck('port');
|
||||
})->toArray(),
|
||||
'env' => $this->environment->handle($server),
|
||||
'oom_disabled' => !$server->oom_killer,
|
||||
'memory' => (int) $server->memory,
|
||||
'swap' => (int) $server->swap,
|
||||
'io' => (int) $server->io,
|
||||
'cpu' => (int) $server->cpu,
|
||||
'threads' => $server->threads,
|
||||
'disk' => (int) $server->disk,
|
||||
'image' => $server->image,
|
||||
],
|
||||
'service' => [
|
||||
'egg' => $server->egg->uuid,
|
||||
'skip_scripts' => $server->skip_scripts,
|
||||
],
|
||||
'rebuild' => false,
|
||||
'suspended' => $server->isSuspended() ? 1 : 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,6 @@ class ServerCreationService
|
||||
* @throws \Throwable
|
||||
* @throws \App\Exceptions\DisplayException
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws \App\Exceptions\Service\Deployment\NoViableNodeException
|
||||
* @throws \App\Exceptions\Service\Deployment\NoViableAllocationException
|
||||
*/
|
||||
public function handle(array $data, DeploymentObject $deployment = null): Server
|
||||
@@ -105,15 +104,16 @@ class ServerCreationService
|
||||
*
|
||||
* @throws \App\Exceptions\DisplayException
|
||||
* @throws \App\Exceptions\Service\Deployment\NoViableAllocationException
|
||||
* @throws \App\Exceptions\Service\Deployment\NoViableNodeException
|
||||
*/
|
||||
private function configureDeployment(array $data, DeploymentObject $deployment): Allocation
|
||||
{
|
||||
/** @var \Illuminate\Support\Collection $nodes */
|
||||
$nodes = $this->findViableNodesService
|
||||
->setDisk(Arr::get($data, 'disk'))
|
||||
->setMemory(Arr::get($data, 'memory'))
|
||||
->handle();
|
||||
/** @var Collection<\App\Models\Node> $nodes */
|
||||
$nodes = $this->findViableNodesService->handle(
|
||||
Arr::get($data, 'memory', 0),
|
||||
Arr::get($data, 'disk', 0),
|
||||
Arr::get($data, 'cpu', 0),
|
||||
Arr::get($data, 'tags', []),
|
||||
);
|
||||
|
||||
return $this->allocationSelectionService->setDedicated($deployment->isDedicated())
|
||||
->setNodes($nodes->pluck('id')->toArray())
|
||||
@@ -154,6 +154,7 @@ class ServerCreationService
|
||||
'database_limit' => Arr::get($data, 'database_limit') ?? 0,
|
||||
'allocation_limit' => Arr::get($data, 'allocation_limit') ?? 0,
|
||||
'backup_limit' => Arr::get($data, 'backup_limit') ?? 0,
|
||||
'docker_labels' => Arr::get($data, 'docker_labels'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Services\Servers;
|
||||
|
||||
use App\Enums\ServerState;
|
||||
use Filament\Notifications\Notification;
|
||||
use Webmozart\Assert\Assert;
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonServerRepository;
|
||||
@@ -26,7 +27,7 @@ class SuspensionService
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function toggle(Server $server, string $action = self::ACTION_SUSPEND): void
|
||||
public function toggle(Server $server, string $action = self::ACTION_SUSPEND)
|
||||
{
|
||||
Assert::oneOf($action, [self::ACTION_SUSPEND, self::ACTION_UNSUSPEND]);
|
||||
|
||||
@@ -35,11 +36,12 @@ class SuspensionService
|
||||
// suspended in the database. Additionally, nothing needs to happen if the server
|
||||
// is not suspended, and we try to un-suspend the instance.
|
||||
if ($isSuspending === $server->isSuspended()) {
|
||||
return;
|
||||
return Notification::make()->danger()->title('Failed!')->body('Server is already suspended!')->send();
|
||||
}
|
||||
|
||||
// Check if the server is currently being transferred.
|
||||
if (!is_null($server->transfer)) {
|
||||
Notification::make()->danger()->title('Failed!')->body('Server is currently being transferred.')->send();
|
||||
throw new ConflictHttpException('Cannot toggle suspension status on a server that is currently being transferred.');
|
||||
}
|
||||
|
||||
|
||||
@@ -53,17 +53,19 @@ class TransferServerService
|
||||
{
|
||||
$node_id = $data['node_id'];
|
||||
$allocation_id = intval($data['allocation_id']);
|
||||
$additional_allocations = array_map('intval', $data['allocation_additional'] ?? []);
|
||||
$additional_allocations = array_map(intval(...), $data['allocation_additional'] ?? []);
|
||||
|
||||
// Check if the node is viable for the transfer.
|
||||
$node = Node::query()
|
||||
->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')
|
||||
->select(['nodes.id', 'nodes.fqdn', 'nodes.scheme', 'nodes.daemon_token', 'nodes.daemon_listen', 'nodes.memory', 'nodes.disk', 'nodes.cpu', 'nodes.memory_overallocate', 'nodes.disk_overallocate', 'nodes.cpu_overallocate'])
|
||||
->withSum('servers', 'disk')
|
||||
->withSum('servers', 'memory')
|
||||
->withSum('servers', 'cpu')
|
||||
->leftJoin('servers', 'servers.node_id', '=', 'nodes.id')
|
||||
->where('nodes.id', $node_id)
|
||||
->first();
|
||||
|
||||
if (!$node->isViable($server->memory, $server->disk)) {
|
||||
if (!$node->isViable($server->memory, $server->disk, $server->cpu)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,9 +32,7 @@ class ToggleTwoFactorService
|
||||
*/
|
||||
public function handle(User $user, string $token, bool $toggleState = null): array
|
||||
{
|
||||
$secret = decrypt($user->totp_secret);
|
||||
|
||||
$isValidToken = $this->google2FA->verifyKey($secret, $token, config()->get('panel.auth.2fa.window'));
|
||||
$isValidToken = $this->google2FA->verifyKey($user->totp_secret, $token, config()->get('panel.auth.2fa.window'));
|
||||
|
||||
if (!$isValidToken) {
|
||||
throw new TwoFactorAuthenticationTokenInvalid();
|
||||
|
||||
@@ -26,7 +26,7 @@ class TwoFactorSetupService
|
||||
throw new \RuntimeException($exception->getMessage(), 0, $exception);
|
||||
}
|
||||
|
||||
$user->totp_secret = encrypt($secret);
|
||||
$user->totp_secret = $secret;
|
||||
$user->save();
|
||||
|
||||
$company = urlencode(preg_replace('/\s/', '', config('app.name')));
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace App\Transformers\Api\Application;
|
||||
use Illuminate\Support\Arr;
|
||||
use App\Models\Egg;
|
||||
use App\Models\Server;
|
||||
use League\Fractal\Resource\Item;
|
||||
use App\Models\EggVariable;
|
||||
use League\Fractal\Resource\Collection;
|
||||
use League\Fractal\Resource\NullResource;
|
||||
@@ -39,7 +38,11 @@ class EggTransformer extends BaseTransformer
|
||||
*/
|
||||
public function transform(Egg $model): array
|
||||
{
|
||||
$files = json_decode($model->config_files, true, 512, JSON_THROW_ON_ERROR);
|
||||
$model->loadMissing('configFrom');
|
||||
|
||||
$files = json_decode($model->inherit_config_files, true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
$model->loadMissing('scriptFrom');
|
||||
|
||||
return [
|
||||
'id' => $model->id,
|
||||
@@ -54,18 +57,18 @@ class EggTransformer extends BaseTransformer
|
||||
'docker_images' => $model->docker_images,
|
||||
'config' => [
|
||||
'files' => $files,
|
||||
'startup' => json_decode($model->config_startup, true),
|
||||
'stop' => $model->config_stop,
|
||||
'logs' => json_decode($model->config_logs, true),
|
||||
'file_denylist' => $model->file_denylist,
|
||||
'startup' => json_decode($model->inherit_config_startup, true),
|
||||
'stop' => $model->inherit_config_stop,
|
||||
'logs' => json_decode($model->inherit_config_logs, true),
|
||||
'file_denylist' => $model->inherit_file_denylist,
|
||||
'extends' => $model->config_from,
|
||||
],
|
||||
'startup' => $model->startup,
|
||||
'script' => [
|
||||
'privileged' => $model->script_is_privileged,
|
||||
'install' => $model->script_install,
|
||||
'entry' => $model->script_entry,
|
||||
'container' => $model->script_container,
|
||||
'install' => $model->copy_script_install,
|
||||
'entry' => $model->copy_script_entry,
|
||||
'container' => $model->copy_script_container,
|
||||
'extends' => $model->copy_script_from,
|
||||
],
|
||||
$model->getCreatedAtColumn() => $this->formatTimestamp($model->created_at),
|
||||
@@ -89,50 +92,6 @@ class EggTransformer extends BaseTransformer
|
||||
return $this->collection($model->getRelation('servers'), $this->makeTransformer(ServerTransformer::class), Server::RESOURCE_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Include more detailed information about the configuration if this Egg is
|
||||
* extending another.
|
||||
*/
|
||||
public function includeConfig(Egg $model): Item|NullResource
|
||||
{
|
||||
if (is_null($model->config_from)) {
|
||||
return $this->null();
|
||||
}
|
||||
|
||||
$model->loadMissing('configFrom');
|
||||
|
||||
return $this->item($model, function (Egg $model) {
|
||||
return [
|
||||
'files' => json_decode($model->inherit_config_files),
|
||||
'startup' => json_decode($model->inherit_config_startup),
|
||||
'stop' => $model->inherit_config_stop,
|
||||
'logs' => json_decode($model->inherit_config_logs),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Include more detailed information about the script configuration if the
|
||||
* Egg is extending another.
|
||||
*/
|
||||
public function includeScript(Egg $model): Item|NullResource
|
||||
{
|
||||
if (is_null($model->copy_script_from)) {
|
||||
return $this->null();
|
||||
}
|
||||
|
||||
$model->loadMissing('scriptFrom');
|
||||
|
||||
return $this->item($model, function (Egg $model) {
|
||||
return [
|
||||
'privileged' => $model->script_is_privileged,
|
||||
'install' => $model->copy_script_install,
|
||||
'entry' => $model->copy_script_entry,
|
||||
'container' => $model->copy_script_container,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Include the variables that are defined for this Egg.
|
||||
*
|
||||
|
||||
89
app/Transformers/Api/Application/MountTransformer.php
Normal file
89
app/Transformers/Api/Application/MountTransformer.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Transformers\Api\Application;
|
||||
|
||||
use App\Models\Mount;
|
||||
use League\Fractal\Resource\Collection;
|
||||
use League\Fractal\Resource\NullResource;
|
||||
use App\Services\Acl\Api\AdminAcl;
|
||||
|
||||
class MountTransformer extends BaseTransformer
|
||||
{
|
||||
/**
|
||||
* List of resources that can be included.
|
||||
*/
|
||||
protected array $availableIncludes = ['eggs', 'nodes', 'servers'];
|
||||
|
||||
/**
|
||||
* Return the resource name for the JSONAPI output.
|
||||
*/
|
||||
public function getResourceName(): string
|
||||
{
|
||||
return Mount::RESOURCE_NAME;
|
||||
}
|
||||
|
||||
public function transform(Mount $model)
|
||||
{
|
||||
return $model->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the eggs associated with this mount.
|
||||
*
|
||||
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
|
||||
*/
|
||||
public function includeEggs(Mount $mount): Collection|NullResource
|
||||
{
|
||||
if (!$this->authorize(AdminAcl::RESOURCE_EGGS)) {
|
||||
return $this->null();
|
||||
}
|
||||
|
||||
$mount->loadMissing('eggs');
|
||||
|
||||
return $this->collection(
|
||||
$mount->getRelation('eggs'),
|
||||
$this->makeTransformer(EggTransformer::class),
|
||||
'egg'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the nodes associated with this mount.
|
||||
*
|
||||
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
|
||||
*/
|
||||
public function includeNodes(Mount $mount): Collection|NullResource
|
||||
{
|
||||
if (!$this->authorize(AdminAcl::RESOURCE_NODES)) {
|
||||
return $this->null();
|
||||
}
|
||||
|
||||
$mount->loadMissing('nodes');
|
||||
|
||||
return $this->collection(
|
||||
$mount->getRelation('nodes'),
|
||||
$this->makeTransformer(NodeTransformer::class),
|
||||
'node'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the servers associated with this mount.
|
||||
*
|
||||
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
|
||||
*/
|
||||
public function includeServers(Mount $mount): Collection|NullResource
|
||||
{
|
||||
if (!$this->authorize(AdminAcl::RESOURCE_SERVERS)) {
|
||||
return $this->null();
|
||||
}
|
||||
|
||||
$mount->loadMissing('servers');
|
||||
|
||||
return $this->collection(
|
||||
$mount->getRelation('servers'),
|
||||
$this->makeTransformer(ServerTransformer::class),
|
||||
'server'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -23,27 +23,23 @@ class NodeTransformer extends BaseTransformer
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a node transformed into a format that can be consumed by the
|
||||
* external administrative API.
|
||||
* Return a node transformed into a format that can be consumed by the external administrative API.
|
||||
*/
|
||||
public function transform(Node $node): array
|
||||
{
|
||||
$response = collect($node->toArray())->mapWithKeys(function ($value, $key) {
|
||||
// I messed up early in 2016 when I named this column as poorly
|
||||
// as I did. This is the tragic result of my mistakes.
|
||||
$key = ($key === 'daemon_sftp') ? 'daemon_sftp' : $key;
|
||||
|
||||
return [snake_case($key) => $value];
|
||||
})->toArray();
|
||||
$response = collect($node->toArray())
|
||||
->mapWithKeys(fn ($value, $key) => [snake_case($key) => $value])
|
||||
->toArray();
|
||||
|
||||
$response[$node->getUpdatedAtColumn()] = $this->formatTimestamp($node->updated_at);
|
||||
$response[$node->getCreatedAtColumn()] = $this->formatTimestamp($node->created_at);
|
||||
|
||||
$resources = $node->servers()->select(['memory', 'disk'])->get();
|
||||
$resources = $node->servers()->select(['memory', 'disk', 'cpu'])->get();
|
||||
|
||||
$response['allocated_resources'] = [
|
||||
'memory' => $resources->sum('memory'),
|
||||
'disk' => $resources->sum('disk'),
|
||||
'cpu' => $resources->sum('cpu'),
|
||||
];
|
||||
|
||||
return $response;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user