Compare commits

...

99 Commits

Author SHA1 Message Date
notCharles
65a697d8f7 add variables to .env 2024-06-02 18:04:02 -04:00
Charles
9515a82a75 Merge pull request #280 from pelican-dev/charles/rework-server
Rework Server Pages
2024-06-02 17:54:00 -04:00
Lance Pioch
44f5ea567f Merge branch 'main' into charles/rework-server
# Conflicts:
#	app/Filament/Resources/ServerResource/Pages/EditServer.php
2024-06-02 17:46:45 -04:00
Charles
88f910f3e7 Merge pull request #307 from pelican-dev/issue/287
Allow Servers to have Mounts
2024-06-02 17:42:13 -04:00
Lance Pioch
020f028008 Add new component 2024-06-02 17:38:07 -04:00
Lance Pioch
0cb7f737b0 Set the node 2024-06-02 17:38:00 -04:00
notCharles
53aa52f519 Add migration to update stock egg uuids 2024-06-02 17:06:42 -04:00
notCharles
e884eda5a7 Update stock eggs to have UUIDs 2024-06-02 16:50:55 -04:00
notCharles
58d1fd3917 Add Mounts + Icons 2024-06-02 15:05:45 -04:00
notCharles
0b0952650e Remove labels/mounts if empty. 2024-06-02 13:43:25 -04:00
notCharles
aa55a7ed83 Update .gitignore, again... 2024-06-02 10:51:48 -04:00
notCharles
c7fa7a1bad Add to .gitignore 2024-06-02 10:48:45 -04:00
Lance Pioch
4a3bdd78ef Update readme 2024-06-02 00:38:54 -04:00
Lance Pioch
a1067fd4aa Allow mounts to be added to servers 2024-06-02 00:34:35 -04:00
Lance Pioch
110cc1248b Fix relationship 2024-06-02 00:33:58 -04:00
Lance Pioch
04a1ccc97e Merge branch 'main' of github.com:pelican-dev/panel 2024-06-01 19:26:25 -04:00
Lance Pioch
5e7f5c2a4c Allow adding new allocations to existing servers 2024-06-01 19:26:23 -04:00
kubi
b804878d7b Fix labels position in server config 2024-06-01 20:42:44 +00:00
notCharles
118977c8c5 Merge branch 'main' into charles/rework-server 2024-06-01 15:54:03 -04:00
notCharles
c31b7b8c6a Correctly save labels on create 2024-06-01 15:52:13 -04:00
Charles
eefe59b153 Merge pull request #300 from pelican-dev/issue/286
2FA Profile
2024-06-01 13:07:32 -04:00
Lance Pioch
cd4b7cbf9e Refresh this 2024-06-01 13:03:46 -04:00
Lance Pioch
67cb3d4816 Show backup tokens better 2024-06-01 12:49:36 -04:00
Lance Pioch
7762e68a6c Make qr code visible on light mode 2024-06-01 12:49:19 -04:00
Lance Pioch
7a327ea378 Remove clockwork 2024-06-01 12:36:11 -04:00
Lance Pioch
b3ca7b7ac9 Merge pull request #284 from Boy132/replace/encrypt-decrypt
Replace encrypt/decrypt with `encrypted` casting to resolve #4
2024-05-31 19:50:07 -04:00
Charles
abc99cd928 Merge pull request #303 from pelican-dev/issue/290
Allow updating of existing eggs via upload
2024-05-31 18:52:20 -04:00
Charles
cb638369cf Merge pull request #302 from pelican-dev/issue/294
Only show application api keys
2024-05-31 18:49:35 -04:00
notCharles
9174de2d8c Add Labels 2024-05-31 17:24:03 -04:00
Boy132
7cda358b66 add missing import 2024-05-31 23:07:50 +02:00
Boy132
33f6551b21 run pint 2024-05-31 23:06:46 +02:00
Boy132
b1928e89b4 Merge branch 'pelican-dev:main' into replace/encrypt-decrypt 2024-05-31 23:04:43 +02:00
Lance Pioch
c956cd0106 Update old keys 2024-05-31 17:03:14 -04:00
notCharles
5081cc3f63 Fix badge, update table 2024-05-31 16:39:23 -04:00
Lance Pioch
8eb2c23420 Fix creating new node 2024-05-31 16:03:12 -04:00
notCharles
cfe385f53a Add uuid to egg exproter
Add UUID to egg exporter.
2024-05-31 16:01:15 -04:00
Lance Pioch
264d3498a6 Allow mailgun to work #257 2024-05-31 15:44:21 -04:00
Lance Pioch
065f3f2468 Activity log fix #257 2024-05-31 15:18:04 -04:00
Lance Pioch
957638d4ac Fill previously existing egg 2024-05-31 15:14:22 -04:00
Lance Pioch
7d0ce1627b Remove unused imports 2024-05-31 02:00:38 -04:00
Lance Pioch
8cec7368ab Only show application api keys 2024-05-31 01:59:33 -04:00
Lance Pioch
5519931ee5 Pint 2024-05-31 01:42:02 -04:00
Charles
97ac0fe54b Add Reset Daemon Key Button (#298) closes #292 2024-05-31 01:41:21 -04:00
Lance Pioch
7657364208 Cache per user and show backup tokens temporarily 2024-05-31 01:38:32 -04:00
Lance Pioch
ef1a208b95 Add 2fa setup 2024-05-31 01:26:28 -04:00
Lance Pioch
aa82c6dd04 Update this 2024-05-31 01:20:25 -04:00
Lance Pioch
8ecabef6b5 Add customization 2024-05-29 19:24:02 -04:00
notCharles
a6d07ede5a Soon-TM 2024-05-29 19:18:09 -04:00
Boy132
f6325c07c4 Fix overallocation -1 and close #268 (#283) 2024-05-29 18:57:30 -04:00
Exotical
7674ee0e2b Make deploy.locations optional in the api (#295) 2024-05-29 18:54:07 -04:00
Senna
5760e72b8f Added 2 badges (#296) 2024-05-29 18:51:45 -04:00
Boy132
b6e46f758d Remove hashids (#282) and close #269 2024-05-29 18:41:44 -04:00
notCharles
e980877bbc Fix Node Creation
Add missing defaults
2024-05-29 18:28:21 -04:00
notCharles
dd223b47c0 WIP Server Transfer 2024-05-29 18:27:54 -04:00
Boy132
639fa3399d run pint 2024-05-28 15:27:33 +02:00
Boy132
82fd547484 replace encrypt/ decrypt with encrypted casting 2024-05-28 15:24:20 +02:00
notCharles
d461242f08 Improve Logic on buttons
If a server is suspended, disable transfer/toggle/reinstall as they will unsuspend the server due to the status change.

Also properly updates server state and container status.
2024-05-27 21:51:24 -04:00
notCharles
dec1cf8e74 Rework Edit Server Page
a WIP, Also functional
2024-05-27 20:02:13 -04:00
notCharles
15caac51fb fix auth redirect
closes https://github.com/pelican-dev/panel/issues/278
2024-05-27 13:41:46 -04:00
notCharles
183c274a0d Correct Tags to KeyVal 2024-05-27 13:37:59 -04:00
Lance Pioch
a8b2fb440f Merge branch 'main' of github.com:pelican-dev/panel 2024-05-25 20:59:54 -04:00
Lance Pioch
f8e4514998 Update filename 2024-05-25 20:51:52 -04:00
Lance Pioch
deeebf73d3 Update gitignores 2024-05-25 20:51:41 -04:00
Boy132
422fc102c9 Improve "no interaction" mode for queue worker service command (#270) 2024-05-25 20:48:02 -04:00
Boy132
e715e92f9d correctly transform eggs that use inheritance (application api) (#217) 2024-05-25 20:47:27 -04:00
Lance Pioch
73babfa2b3 Merge pull request #274 from pelican-dev/issue/267 and fix #267 2024-05-24 21:15:14 -04:00
Lance Pioch
e0a92d733b I swear I already did this 2024-05-24 20:58:19 -04:00
Jordan Adams
1e67cd9944 Fix mumble host to allow IPv6 (#264) 2024-05-24 19:36:18 -04:00
Lance Pioch
3946116dff Merge pull request #265 from pelican-dev/issue/222
Simplify node deployment service, add filtering with tags instead of locations
2024-05-24 19:35:06 -04:00
Lance Pioch
b77fd3d653 Fix #267 2024-05-24 19:34:23 -04:00
Lance Pioch
f4672c6cb1 Pint fix 2024-05-22 03:15:29 -04:00
Lance Pioch
5b9e4b1729 Always limit by nodes, was like this before anyways 2024-05-22 03:10:33 -04:00
Lance Pioch
48f715ae69 Fix directory 2024-05-22 02:52:49 -04:00
Lance Pioch
51460782cc Merge branch 'main' into issue/222
# Conflicts:
#	app/Http/Controllers/Api/Application/Nodes/NodeDeploymentController.php
#	app/Http/Requests/Api/Application/Nodes/GetDeployableNodesRequest.php
#	app/Services/Deployment/FindViableNodesService.php
#	app/Services/Servers/ServerCreationService.php
#	tests/Integration/Services/Deployment/FindViableNodesServiceTest.php
2024-05-22 02:47:37 -04:00
Lance Pioch
b007e63937 Pint fixes 2024-05-22 02:35:20 -04:00
Boy132
4dd833562b Add CPU limit to node (#239) to resolve #233
* add node cpu limit to backend

* update makenodecommand

* add node cpu limit to frontend

* add migration and update mysql schema

* run pint

* fix typo in mysql schema

* forgot this assert

* forgot to setCpu here

* run pint

* adjust migration

* Fix db migration

* make cpu optional

* set default value for cpu in node deployment

* update mysql schema

---------

Co-authored-by: notCharles <charles@pelican.dev>
2024-05-22 02:34:43 -04:00
Lance Pioch
b579f14f3f Backwards compatibility 2024-05-21 22:04:12 -04:00
Lance Pioch
eadaec1b30 Simplify now that keys are fixed 2024-05-21 21:48:16 -04:00
Lance Pioch
a9e58bb493 Fix database path for sqlite 2024-05-21 21:48:04 -04:00
Lance Pioch
5c33c7495a Ignore this for now 2024-05-21 21:45:26 -04:00
Lance Pioch
f9aa8cf218 Simplify viable nodes service 2024-05-21 21:44:49 -04:00
Lance Pioch
da698a3666 Remove exception 2024-05-21 21:02:11 -04:00
Lance Pioch
2808a3dd35 Simplify buttons 2024-05-20 14:38:48 -04:00
Lance Pioch
7ea365e8de Pint 2024-05-19 23:25:40 -04:00
Lance Pioch
ae399f9bad Add port validation rule for #68 2024-05-19 23:25:12 -04:00
Lance Pioch
53a5ff6e6d Update api docs 2024-05-19 23:24:21 -04:00
Lance Pioch
54ae4b3dc1 Merge pull request #261 from pelican-dev/charles/docker-tags
Add docker container labels
2024-05-19 21:36:26 -04:00
notCharles
859a721e17 mysql vs sqlite... 2024-05-19 21:30:25 -04:00
notCharles
03cbdd5bdd update edit/create pages 2024-05-19 21:15:43 -04:00
notCharles
4c43fd1683 Add docker_labels 2024-05-19 20:55:37 -04:00
notCharles
0c61a63191 Add id to allocation table 2024-05-19 20:23:59 -04:00
Boy132
b1f99ca8a3 Add api for mounts (#160)
* add application api endpoints for mounts

* run pint

* add mounts resource to api key

* add includes to mount transformer

* forgot delete route for mount itself

* add migration for "r_mounts" column

* add mounts to testcase api key
2024-05-19 08:50:15 -07:00
notCharles
0a5810358a Update jdbc string
should also update the password here.
2024-05-18 18:17:20 -04:00
notCharles
1bae239971 Fix db password rotation
updates the password textbox when password is rotated.
2024-05-18 18:13:06 -04:00
notCharles
597f74f105 reload form data after save
closes https://github.com/pelican-dev/panel/issues/251
2024-05-18 18:08:55 -04:00
notCharles
5344d99a40 Update Mobile View 2024-05-18 17:47:33 -04:00
Boy132
1db1a1a3e0 set default db username to "pelican" to match docs (#254) 2024-05-18 12:38:11 -07:00
Boy132
712b6a285b Add artisan command to create queue worker service (#253)
* add command to create queue worker service file

* remove comments from service file that are no longer needed

* only create queue worker service file if queue driver is not "sync"

* make "database" the recommended queue driver, again
2024-05-18 10:31:02 -07:00
notCharles
38b92ae21d Fix user->admin
closes https://github.com/pelican-dev/panel/issues/197
2024-05-17 22:58:17 -04:00
139 changed files with 2947 additions and 1539 deletions

View File

@@ -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=

View File

@@ -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
@@ -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

65
.gitignore vendored
View File

@@ -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

View File

@@ -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,20 @@ class AppSettingsCommand extends Command
Artisan::call('key:generate');
}
if ($this->variables['QUEUE_CONNECTION'] !== 'sync') {
Artisan::call('p:environment:queue-service', $redisUsed ? ['--use-redis'] : []);
}
$this->info($this->console->output());
return 0;
}
/**
* Check if redis is selected, if so, request connection details and verify them.
* Request 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',

View File

@@ -0,0 +1,72 @@
<?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('Service name', 'pelican-queue');
$path = '/etc/systemd/system/' . $serviceName . '.service';
if (file_exists($path) && !$this->option('overwrite') && !$this->confirm('The service file already exists. Do you want to overwrite it?')) {
$this->line('Creation of queue worker service file aborted.');
return;
}
$user = $this->option('user') ?? $this->ask('User', 'www-data');
$group = $this->option('group') ?? $this->ask('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 --queue=high,standard,low --tries=3
StartLimitInterval=180
StartLimitBurst=30
RestartSec=5s
[Install]
WantedBy=multi-user.target
");
if (!$success) {
$this->error('Error creating service file');
return;
}
$result = Process::run("systemctl enable --now $serviceName.service");
if ($result->failed()) {
$this->error('Error enabling service: ' . $result->errorOutput());
return;
}
$this->line('Queue worker service file created successfully.');
}
}

View File

@@ -20,6 +20,8 @@ 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.}
@@ -58,6 +60,8 @@ 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');

View File

@@ -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]);

View File

@@ -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;
}

View File

@@ -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())

View File

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

View File

@@ -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,
]);

View File

@@ -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);
}
}

View File

@@ -39,12 +39,7 @@ 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),
],
@@ -55,10 +50,6 @@ 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')
@@ -70,11 +61,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'),
],
];
}

View File

@@ -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 [

View File

@@ -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([

View File

@@ -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';
}
}

View File

@@ -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 [

View File

@@ -76,15 +76,6 @@ class EditDatabaseHost extends EditRecord
];
}
protected function mutateFormDataBeforeSave(array $data): array
{
if (isset($data['password'])) {
$data['password'] = encrypt($data['password']);
}
return $data;
}
protected function getFormActions(): array
{
return [];

View File

@@ -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);
}
}

View File

@@ -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')

View File

@@ -31,28 +31,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 +48,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(),

View File

@@ -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('%'),
]),
]),
]),
]);

View File

@@ -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,37 @@ 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\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 +227,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 +257,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 +274,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 +297,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 +313,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 +376,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 +423,9 @@ class EditNode extends EditRecord
NodeMemoryChart::class,
];
}
protected function afterSave(): void
{
$this->fillForm();
}
}

View File

@@ -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')

View File

@@ -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([
//

View File

@@ -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')
@@ -452,7 +413,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 +425,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 +639,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

View File

@@ -0,0 +1,588 @@
<?php
namespace App\Filament\Resources\ServerResource\Pages;
use App\Filament\Resources\ServerResource;
use App\Services\Servers\RandomWordService;
use Filament\Actions;
use Filament\Forms;
use App\Enums\ContainerStatus;
use App\Enums\ServerState;
use App\Models\Egg;
use App\Models\Server;
use App\Models\ServerVariable;
use App\Repositories\Daemon\DaemonServerRepository;
use App\Services\Servers\ServerDeletionService;
use Filament\Forms\Form;
use Filament\Resources\Pages\EditRecord;
use Illuminate\Support\Facades\Validator;
use Closure;
class EditServerOrg extends EditRecord
{
protected static string $resource = ServerResource::class;
public function form(Form $form): Form
{
return $form
->columns([
'default' => 2,
'sm' => 2,
'md' => 4,
'lg' => 6,
])
->schema([
Forms\Components\ToggleButtons::make('docker')
->label('Container Status')->inline()->inlineLabel()
->formatStateUsing(function ($state, Server $server) {
if ($server->node_id === null) {
return 'unknown';
}
/** @var DaemonServerRepository $service */
$service = resolve(DaemonServerRepository::class);
$details = $service->setServer($server)->getDetails();
return $details['state'] ?? 'unknown';
})
->options(fn ($state) => collect(ContainerStatus::cases())->filter(fn ($containerStatus) => $containerStatus->value === $state)->mapWithKeys(
fn (ContainerStatus $state) => [$state->value => str($state->value)->replace('_', ' ')->ucwords()]
))
->colors(collect(ContainerStatus::cases())->mapWithKeys(
fn (ContainerStatus $status) => [$status->value => $status->color()]
))
->icons(collect(ContainerStatus::cases())->mapWithKeys(
fn (ContainerStatus $status) => [$status->value => $status->icon()]
))
->columnSpan([
'default' => 1,
'sm' => 2,
'md' => 2,
'lg' => 3,
]),
Forms\Components\ToggleButtons::make('status')
->label('Server State')->inline()->inlineLabel()
->helperText('')
->formatStateUsing(fn ($state) => $state ?? ServerState::Normal)
->options(fn ($state) => collect(ServerState::cases())->filter(fn ($serverState) => $serverState->value === $state)->mapWithKeys(
fn (ServerState $state) => [$state->value => str($state->value)->replace('_', ' ')->ucwords()]
))
->colors(collect(ServerState::cases())->mapWithKeys(
fn (ServerState $state) => [$state->value => $state->color()]
))
->icons(collect(ServerState::cases())->mapWithKeys(
fn (ServerState $state) => [$state->value => $state->icon()]
))
->columnSpan([
'default' => 1,
'sm' => 2,
'md' => 2,
'lg' => 3,
]),
Forms\Components\TextInput::make('external_id')
->maxLength(191)
->hidden(),
Forms\Components\TextInput::make('name')
->prefixIcon('tabler-server')
->label('Display Name')
->suffixAction(Forms\Components\Actions\Action::make('random')
->icon('tabler-dice-' . random_int(1, 6))
->action(function (Forms\Set $set, Forms\Get $get) {
$egg = Egg::find($get('egg_id'));
$prefix = $egg ? str($egg->name)->lower()->kebab() . '-' : '';
$word = (new RandomWordService())->word();
$set('name', $prefix . $word);
}))
->columnSpan([
'default' => 2,
'sm' => 4,
'md' => 2,
'lg' => 3,
])
->required()
->maxLength(191),
Forms\Components\Select::make('owner_id')
->prefixIcon('tabler-user')
->label('Owner')
->columnSpan([
'default' => 2,
'sm' => 4,
'md' => 2,
'lg' => 3,
])
->relationship('user', 'username')
->searchable()
->preload()
->required(),
Forms\Components\Textarea::make('description')
->hidden()
->required()
->columnSpanFull(),
Forms\Components\Select::make('egg_id')
->disabledOn('edit')
->prefixIcon('tabler-egg')
->columnSpan([
'default' => 2,
'sm' => 2,
'md' => 2,
'lg' => 5,
])
->relationship('egg', 'name')
->searchable()
->preload()
->required(),
Forms\Components\ToggleButtons::make('skip_scripts')
->label('Run Egg Install Script?')->inline()
->options([
false => 'Yes',
true => 'Skip',
])
->colors([
false => 'primary',
true => 'danger',
])
->icons([
false => 'tabler-code',
true => 'tabler-code-off',
])
->required(),
Forms\Components\Textarea::make('startup')
->hintIcon('tabler-code')
->label('Startup Command')
->required()
->live()
->columnSpan([
'default' => 2,
'sm' => 4,
'md' => 4,
'lg' => 6,
])
->rows(function ($state) {
return str($state)->explode("\n")->reduce(
fn (int $carry, $line) => $carry + floor(strlen($line) / 125),
0
);
}),
Forms\Components\Hidden::make('start_on_completion'),
Forms\Components\Section::make('Egg Variables')
->icon('tabler-eggs')
->iconColor('primary')
->collapsible()
->collapsed()
->columnSpan(([
'default' => 2,
'sm' => 4,
'md' => 4,
'lg' => 6,
]))
->schema([
Forms\Components\Repeater::make('server_variables')
->relationship('serverVariables')
->grid()
->mutateRelationshipDataBeforeSaveUsing(function (array &$data): array {
foreach ($data as $key => $value) {
if (!isset($data['variable_value'])) {
$data['variable_value'] = '';
}
}
return $data;
})
->reorderable(false)->addable(false)->deletable(false)
->schema(function () {
$text = Forms\Components\TextInput::make('variable_value')
->hidden($this->shouldHideComponent(...))
->maxLength(191)
->rules([
fn (ServerVariable $serverVariable): Closure => function (string $attribute, $value, Closure $fail) use ($serverVariable) {
$validator = Validator::make(['validatorkey' => $value], [
'validatorkey' => $serverVariable->variable->rules,
]);
if ($validator->fails()) {
$message = str($validator->errors()->first())->replace('validatorkey', $serverVariable->variable->name);
$fail($message);
}
},
]);
$select = Forms\Components\Select::make('variable_value')
->hidden($this->shouldHideComponent(...))
->options($this->getSelectOptionsFromRules(...))
->selectablePlaceholder(false);
$components = [$text, $select];
/** @var Forms\Components\Component $component */
foreach ($components as &$component) {
$component = $component
->live(onBlur: true)
->hintIcon('tabler-code')
->label(fn (ServerVariable $serverVariable) => $serverVariable->variable->name)
->hintIconTooltip(fn (ServerVariable $serverVariable) => $serverVariable->variable->rules)
->prefix(fn (ServerVariable $serverVariable) => '{{' . $serverVariable->variable->env_variable . '}}')
->helperText(fn (ServerVariable $serverVariable) => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description);
}
return $components;
})
->columnSpan(2),
]),
Forms\Components\Section::make('Environment Management')
->collapsed()
->icon('tabler-server-cog')
->iconColor('primary')
->columns([
'default' => 2,
'sm' => 4,
'md' => 4,
'lg' => 4,
])
->columnSpanFull()
->schema([
Forms\Components\Fieldset::make('Resource Limits')
->columnSpan([
'default' => 2,
'sm' => 4,
'md' => 4,
'lg' => 6,
])
->columns([
'default' => 1,
'sm' => 2,
'md' => 3,
'lg' => 3,
])
->schema([
Forms\Components\Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_mem')
->label('Memory')->inlineLabel()->inline()
->afterStateUpdated(fn (Forms\Set $set) => $set('memory', 0))
->formatStateUsing(fn (Forms\Get $get) => $get('memory') == 0)
->live()
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
Forms\Components\TextInput::make('memory')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
->label('Memory Limit')->inlineLabel()
->suffix('MiB')
->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()
->live()
->afterStateUpdated(fn (Forms\Set $set) => $set('disk', 0))
->formatStateUsing(fn (Forms\Get $get) => $get('disk') == 0)
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
Forms\Components\TextInput::make('disk')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
->label('Disk Space Limit')->inlineLabel()
->suffix('MiB')
->required()
->columnSpan(2)
->numeric()
->minValue(0),
]),
Forms\Components\Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_cpu')
->label('CPU')->inlineLabel()->inline()
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu', 0))
->formatStateUsing(fn (Forms\Get $get) => $get('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('%')
->required()
->columnSpan(2)
->numeric()
->minValue(0),
]),
Forms\Components\Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('swap_support')
->live()
->label('Enable Swap Memory')->inlineLabel()->inline()
->columnSpan(2)
->afterStateUpdated(function ($state, Forms\Set $set) {
$value = match ($state) {
'unlimited' => -1,
'disabled' => 0,
'limited' => 128,
};
$set('swap', $value);
})
->formatStateUsing(function (Forms\Get $get) {
return match (true) {
$get('swap') > 0 => 'limited',
$get('swap') == 0 => 'disabled',
$get('swap') < 0 => 'unlimited',
};
})
->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 => true,
'limited', false => false,
})
->label('Swap Memory')->inlineLabel()
->suffix('MiB')
->minValue(-1)
->columnSpan(2)
->required()
->integer(),
]),
Forms\Components\Hidden::make('io')
->helperText('The IO performance relative to other running containers')
->label('Block IO Proportion'),
Forms\Components\Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('oom_killer')
->label('OOM Killer')->inlineLabel()->inline()
->columnSpan(2)
->options([
false => 'Disabled',
true => 'Enabled',
])
->colors([
false => 'success',
true => 'danger',
]),
Forms\Components\TextInput::make('oom_disabled_hidden')
->hidden(),
]),
]),
Forms\Components\Fieldset::make('Feature Limits')
->inlineLabel()
->columnSpan([
'default' => 2,
'sm' => 4,
'md' => 4,
'lg' => 6,
])
->columns([
'default' => 1,
'sm' => 2,
'md' => 3,
'lg' => 3,
])
->schema([
Forms\Components\TextInput::make('allocation_limit')
->suffixIcon('tabler-network')
->required()
->numeric(),
Forms\Components\TextInput::make('database_limit')
->suffixIcon('tabler-database')
->required()
->numeric(),
Forms\Components\TextInput::make('backup_limit')
->suffixIcon('tabler-copy-check')
->required()
->numeric(),
]),
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('Label Name')
->valueLabel('Label Description')
->columnSpanFull(),
]),
]),
]);
}
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make('Delete')
->successRedirectUrl(route('filament.admin.resources.servers.index'))
->color('danger')
->after(fn (Server $server) => resolve(ServerDeletionService::class)->handle($server))
->requiresConfirmation(),
Actions\Action::make('console')
->label('Console')
->icon('tabler-terminal')
->url(fn (Server $server) => "/server/$server->uuid_short"),
$this->getSaveFormAction()->formId('form'),
];
}
protected function getFormActions(): array
{
return [];
}
protected function mutateFormDataBeforeSave(array $data): array
{
unset($data['docker'], $data['status']);
return $data;
}
public function getRelationManagers(): array
{
return [
ServerResource\RelationManagers\AllocationsRelationManager::class,
];
}
private function shouldHideComponent(Forms\Get $get, Forms\Components\Component $component): bool
{
$containsRuleIn = str($get('rules'))->explode('|')->reduce(
fn ($result, $value) => $result === true && !str($value)->startsWith('in:'), true
);
if ($component instanceof Forms\Components\Select) {
return $containsRuleIn;
}
if ($component instanceof Forms\Components\TextInput) {
return !$containsRuleIn;
}
throw new \Exception('Component type not supported: ' . $component::class);
}
private function getSelectOptionsFromRules(Forms\Get $get): array
{
$inRule = str($get('rules'))->explode('|')->reduce(
fn ($result, $value) => str($value)->startsWith('in:') ? $value : $result, ''
);
return str($inRule)
->after('in:')
->explode(',')
->each(fn ($value) => str($value)->trim())
->mapWithKeys(fn ($value) => [$value => $value])
->all();
}
}

View File

@@ -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([

View File

@@ -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();
}
}
}

View File

@@ -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,
]);
}
}

View File

@@ -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

View File

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

View File

@@ -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);
}
}

View File

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

View File

@@ -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))

View File

@@ -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
{

View File

@@ -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);

View File

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

View 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 DeleteMountRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_MOUNTS;
protected int $permission = AdminAcl::WRITE;
}

View 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;
}

View 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 StoreMountRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_MOUNTS;
protected int $permission = AdminAcl::WRITE;
}

View File

@@ -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));
}
}

View File

@@ -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',
];
}
}

View File

@@ -28,6 +28,8 @@ class StoreNodeRequest extends ApplicationApiRequest
'memory_overallocate',
'disk',
'disk_overallocate',
'cpu',
'cpu_overallocate',
'upload_size',
'daemon_listen',
'daemon_sftp',

View File

@@ -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;

View File

@@ -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.
*/

View File

@@ -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;
}

View File

@@ -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.
*/

View File

@@ -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',
];

View File

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

View File

@@ -26,6 +26,8 @@ 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
@@ -61,9 +63,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,7 +70,8 @@ class Node extends Model
'public', 'name',
'fqdn', 'scheme', 'behind_proxy',
'memory', 'memory_overallocate', 'disk',
'disk_overallocate', 'upload_size', 'daemon_base',
'disk_overallocate', 'cpu', 'cpu_overallocate',
'upload_size', 'daemon_base',
'daemon_sftp', 'daemon_listen',
'description', 'maintenance_mode',
];
@@ -87,6 +87,8 @@ class Node extends Model
'memory_overallocate' => 'required|numeric|min:-1',
'disk' => 'required|numeric|min:0',
'disk_overallocate' => 'required|numeric|min:-1',
'cpu' => 'required|numeric|min:0',
'cpu_overallocate' => 'required|numeric|min:-1',
'daemon_base' => 'sometimes|required|regex:/^([\/][\d\w.\-\/]+)$/',
'daemon_sftp' => 'required|numeric|between:1,65535',
'daemon_listen' => 'required|numeric|between:1,65535',
@@ -104,6 +106,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 +120,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 +140,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 +168,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 +206,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 +235,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()

View File

@@ -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;
}
}

View File

@@ -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.
*/

View File

@@ -5,6 +5,7 @@ namespace App\Models;
use App\Enums\ServerState;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Facades\Http;
@@ -13,7 +14,6 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use App\Exceptions\Http\Server\ServerStateConflictException;
/**
@@ -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);
}
/**

View File

@@ -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.
*/

View File

@@ -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.
*/

View File

@@ -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 ?? [],
]);

View File

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

View File

@@ -12,6 +12,7 @@ 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;
@@ -59,7 +60,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 +71,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);
}
/**

View File

@@ -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,

View File

@@ -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');
}
}

View File

@@ -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
View 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.');
}
}
}

View File

@@ -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.

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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);

View File

@@ -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'),

View File

@@ -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']);
}

View File

@@ -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) {

View File

@@ -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));
}
}

View File

@@ -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

View File

@@ -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,

View File

@@ -26,8 +26,11 @@ class EggImporterService
$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,
]);

View File

@@ -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);

View File

@@ -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'))

View File

@@ -28,14 +28,14 @@ class NodeUpdateService
public function handle(Node $node, array $data, bool $resetToken = false): Node
{
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->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

View File

@@ -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,
];
}
}

View File

@@ -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'),
]);
}

View File

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

View File

@@ -53,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;
}

View File

@@ -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();

View File

@@ -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')));

View File

@@ -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.
*

View 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'
);
}
}

View File

@@ -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;

View File

@@ -45,7 +45,7 @@ class ServerDatabaseTransformer extends BaseTransformer
{
return $this->item($model, function (Database $model) {
return [
'password' => decrypt($model->password),
'password' => $model->password,
];
}, 'database_password');
}

View File

@@ -55,7 +55,7 @@ class ActivityLogTransformer extends BaseClientTransformer
$properties = $model->properties
->mapWithKeys(function ($value, $key) use ($model) {
if ($key === 'ip' && !$model->actor->is($this->request->user())) {
if ($key === 'ip' && $model->actor && !$model->actor->is($this->request->user())) {
return [$key => '[hidden]'];
}

View File

@@ -6,22 +6,11 @@ use App\Models\Database;
use League\Fractal\Resource\Item;
use App\Models\Permission;
use League\Fractal\Resource\NullResource;
use App\Contracts\Extensions\HashidsInterface;
class DatabaseTransformer extends BaseClientTransformer
{
protected array $availableIncludes = ['password'];
private HashidsInterface $hashids;
/**
* Handle dependency injection.
*/
public function handle(HashidsInterface $hashids)
{
$this->hashids = $hashids;
}
public function getResourceName(): string
{
return Database::RESOURCE_NAME;
@@ -32,7 +21,7 @@ class DatabaseTransformer extends BaseClientTransformer
$model->loadMissing('host');
return [
'id' => $this->hashids->encode($model->id),
'id' => $model->id,
'host' => [
'address' => $model->getRelation('host')->host,
'port' => $model->getRelation('host')->port,
@@ -55,7 +44,7 @@ class DatabaseTransformer extends BaseClientTransformer
return $this->item($database, function (Database $model) {
return [
'password' => decrypt($model->password),
'password' => $model->password,
];
}, 'database_password');
}

View File

@@ -16,7 +16,7 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
$middleware->redirectGuestsTo(fn () => route('login'));
$middleware->redirectGuestsTo(fn () => route('auth.login'));
$middleware->web(\App\Http\Middleware\LanguageMiddleware::class);

View File

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

View File

@@ -16,7 +16,6 @@
"doctrine/dbal": "~3.6.0",
"filament/filament": "^3.2",
"guzzlehttp/guzzle": "^7.8.1",
"hashids/hashids": "~5.0.0",
"laracasts/utilities": "~3.2.2",
"laravel/framework": "^11.7",
"laravel/helpers": "^1.7",
@@ -34,7 +33,8 @@
"s1lentium/iptools": "~1.2.0",
"spatie/laravel-fractal": "^6.2",
"spatie/laravel-query-builder": "^5.8.1",
"symfony/mailgun-mailer": "^7.0.7",
"symfony/http-client": "^7.1",
"symfony/mailgun-mailer": "^7.1",
"symfony/postmark-mailer": "^7.0.7",
"symfony/yaml": "^7.0.7",
"webbingbrasil/filament-copyactions": "^3.0.1",
@@ -43,7 +43,6 @@
"require-dev": {
"barryvdh/laravel-ide-helper": "^3.0",
"fakerphp/faker": "^1.23.1",
"itsgoingd/clockwork": "~5.1.12",
"larastan/larastan": "^2.9.6",
"laravel/pint": "^1.15.3",
"laravel/sail": "^1.29.1",

325
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "dc1c1e5ee766f2e31e84c50670fa0c98",
"content-hash": "8feeafbeb16044bd6716510a73393fc0",
"packages": [
{
"name": "abdelhamiderrahmouni/filament-monaco-editor",
@@ -2613,75 +2613,6 @@
],
"time": "2023-12-03T19:50:20+00:00"
},
{
"name": "hashids/hashids",
"version": "5.0.2",
"source": {
"type": "git",
"url": "https://github.com/vinkla/hashids.git",
"reference": "197171016b77ddf14e259e186559152eb3f8cf33"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/vinkla/hashids/zipball/197171016b77ddf14e259e186559152eb3f8cf33",
"reference": "197171016b77ddf14e259e186559152eb3f8cf33",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "^8.1"
},
"require-dev": {
"phpunit/phpunit": "^10.0"
},
"suggest": {
"ext-bcmath": "Required to use BC Math arbitrary precision mathematics (*).",
"ext-gmp": "Required to use GNU multiple precision mathematics (*)."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.0-dev"
}
},
"autoload": {
"psr-4": {
"Hashids\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ivan Akimov",
"email": "ivan@barreleye.com"
},
{
"name": "Vincent Klaiber",
"email": "hello@doubledip.se"
}
],
"description": "Generate short, unique, non-sequential ids (like YouTube and Bitly) from numbers",
"homepage": "https://hashids.org/php",
"keywords": [
"bitly",
"decode",
"encode",
"hash",
"hashid",
"hashids",
"ids",
"obfuscate",
"youtube"
],
"support": {
"issues": "https://github.com/vinkla/hashids/issues",
"source": "https://github.com/vinkla/hashids/tree/5.0.2"
},
"time": "2023-02-23T15:00:54+00:00"
},
{
"name": "kirschbaum-development/eloquent-power-joins",
"version": "3.5.6",
@@ -7722,6 +7653,178 @@
],
"time": "2024-04-18T09:29:19+00:00"
},
{
"name": "symfony/http-client",
"version": "v7.1.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
"reference": "2266f9813ed7d8c84e04627edead7b7fd249d6e9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/2266f9813ed7d8c84e04627edead7b7fd249d6e9",
"reference": "2266f9813ed7d8c84e04627edead7b7fd249d6e9",
"shasum": ""
},
"require": {
"php": ">=8.2",
"psr/log": "^1|^2|^3",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/http-client-contracts": "^3.4.1",
"symfony/service-contracts": "^2.5|^3"
},
"conflict": {
"php-http/discovery": "<1.15",
"symfony/http-foundation": "<6.4"
},
"provide": {
"php-http/async-client-implementation": "*",
"php-http/client-implementation": "*",
"psr/http-client-implementation": "1.0",
"symfony/http-client-implementation": "3.0"
},
"require-dev": {
"amphp/amp": "^2.5",
"amphp/http-client": "^4.2.1",
"amphp/http-tunnel": "^1.0",
"amphp/socket": "^1.1",
"guzzlehttp/promises": "^1.4|^2.0",
"nyholm/psr7": "^1.0",
"php-http/httplug": "^1.0|^2.0",
"psr/http-client": "^1.0",
"symfony/dependency-injection": "^6.4|^7.0",
"symfony/http-kernel": "^6.4|^7.0",
"symfony/messenger": "^6.4|^7.0",
"symfony/process": "^6.4|^7.0",
"symfony/rate-limiter": "^6.4|^7.0",
"symfony/stopwatch": "^6.4|^7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\HttpClient\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously",
"homepage": "https://symfony.com",
"keywords": [
"http"
],
"support": {
"source": "https://github.com/symfony/http-client/tree/v7.1.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-05-13T15:35:37+00:00"
},
{
"name": "symfony/http-client-contracts",
"version": "v3.5.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client-contracts.git",
"reference": "20414d96f391677bf80078aa55baece78b82647d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/20414d96f391677bf80078aa55baece78b82647d",
"reference": "20414d96f391677bf80078aa55baece78b82647d",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.5-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
},
"autoload": {
"psr-4": {
"Symfony\\Contracts\\HttpClient\\": ""
},
"exclude-from-classmap": [
"/Test/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Generic abstractions related to HTTP clients",
"homepage": "https://symfony.com",
"keywords": [
"abstractions",
"contracts",
"decoupling",
"interfaces",
"interoperability",
"standards"
],
"support": {
"source": "https://github.com/symfony/http-client-contracts/tree/v3.5.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-04-18T09:32:20+00:00"
},
{
"name": "symfony/http-foundation",
"version": "v7.0.7",
@@ -7994,16 +8097,16 @@
},
{
"name": "symfony/mailgun-mailer",
"version": "v7.0.7",
"version": "v7.1.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/mailgun-mailer.git",
"reference": "e9bb8fdbdd79334a8a88bdd233204315abd992c5"
"reference": "aa5afbe846bbc8bde6afe2602f0427834b872f55"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mailgun-mailer/zipball/e9bb8fdbdd79334a8a88bdd233204315abd992c5",
"reference": "e9bb8fdbdd79334a8a88bdd233204315abd992c5",
"url": "https://api.github.com/repos/symfony/mailgun-mailer/zipball/aa5afbe846bbc8bde6afe2602f0427834b872f55",
"reference": "aa5afbe846bbc8bde6afe2602f0427834b872f55",
"shasum": ""
},
"require": {
@@ -8043,7 +8146,7 @@
"description": "Symfony Mailgun Mailer Bridge",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/mailgun-mailer/tree/v7.0.7"
"source": "https://github.com/symfony/mailgun-mailer/tree/v7.1.0"
},
"funding": [
{
@@ -8059,7 +8162,7 @@
"type": "tidelift"
}
],
"time": "2024-04-18T09:29:19+00:00"
"time": "2024-04-18T09:32:20+00:00"
},
{
"name": "symfony/mime",
@@ -10437,74 +10540,6 @@
},
"time": "2020-07-09T08:09:16+00:00"
},
{
"name": "itsgoingd/clockwork",
"version": "v5.1.12",
"source": {
"type": "git",
"url": "https://github.com/itsgoingd/clockwork.git",
"reference": "c9dbdbb1f0efd19bb80f1080ef63f1b9b1bc3b1b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/itsgoingd/clockwork/zipball/c9dbdbb1f0efd19bb80f1080ef63f1b9b1bc3b1b",
"reference": "c9dbdbb1f0efd19bb80f1080ef63f1b9b1bc3b1b",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": ">=5.6"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Clockwork\\Support\\Laravel\\ClockworkServiceProvider"
],
"aliases": {
"Clockwork": "Clockwork\\Support\\Laravel\\Facade"
}
}
},
"autoload": {
"psr-4": {
"Clockwork\\": "Clockwork/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "itsgoingd",
"email": "itsgoingd@luzer.sk",
"homepage": "https://twitter.com/itsgoingd"
}
],
"description": "php dev tools in your browser",
"homepage": "https://underground.works/clockwork",
"keywords": [
"Devtools",
"debugging",
"laravel",
"logging",
"lumen",
"profiling",
"slim"
],
"support": {
"issues": "https://github.com/itsgoingd/clockwork/issues",
"source": "https://github.com/itsgoingd/clockwork/tree/v5.1.12"
},
"funding": [
{
"url": "https://github.com/itsgoingd",
"type": "github"
}
],
"time": "2022-12-13T00:04:12+00:00"
},
{
"name": "larastan/larastan",
"version": "v2.9.6",
@@ -13061,5 +13096,5 @@
"ext-zip": "*"
},
"platform-dev": [],
"plugin-api-version": "2.3.0"
"plugin-api-version": "2.6.0"
}

View File

@@ -10,7 +10,7 @@ return [
'sqlite' => [
'driver' => 'sqlite',
'url' => env('DB_URL'),
'database' => env('DB_DATABASE', database_path('database.sqlite')),
'database' => database_path(env('DB_DATABASE', 'database.sqlite')),
'prefix' => '',
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
],
@@ -21,7 +21,7 @@ return [
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'panel'),
'username' => env('DB_USERNAME', 'panel'),
'username' => env('DB_USERNAME', 'pelican'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => 'utf8mb4',

View File

@@ -1,15 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Hashids Configuration
|--------------------------------------------------------------------------
|
| Here are the settings that control the Hashids setup and usage in the panel.
|
*/
'salt' => env('HASHIDS_SALT'),
'length' => env('HASHIDS_LENGTH', 8),
'alphabet' => env('HASHIDS_ALPHABET', 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'),
];

View File

@@ -26,7 +26,7 @@ class ApiKeyFactory extends Factory
return [
'key_type' => ApiKey::TYPE_APPLICATION,
'identifier' => ApiKey::generateTokenIdentifier(ApiKey::TYPE_APPLICATION),
'token' => $token ?: $token = encrypt(Str::random(ApiKey::KEY_LENGTH)),
'token' => $token ?: $token = Str::random(ApiKey::KEY_LENGTH),
'allowed_ips' => null,
'memo' => 'Test Function Key',
'created_at' => Carbon::now(),

View File

@@ -27,7 +27,7 @@ class DatabaseFactory extends Factory
'database' => Str::random(10),
'username' => Str::random(10),
'remote' => '%',
'password' => $password ?: encrypt('test123'),
'password' => $password ?: 'test123',
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
];

View File

@@ -3,7 +3,6 @@
namespace Database\Factories;
use App\Models\DatabaseHost;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Database\Eloquent\Factories\Factory;
class DatabaseHostFactory extends Factory
@@ -25,7 +24,7 @@ class DatabaseHostFactory extends Factory
'host' => $this->faker->unique()->ipv4(),
'port' => 3306,
'username' => $this->faker->colorName(),
'password' => Crypt::encrypt($this->faker->word()),
'password' => $this->faker->word(),
];
}
}

View File

@@ -5,7 +5,6 @@ namespace Database\Factories;
use Ramsey\Uuid\Uuid;
use Illuminate\Support\Str;
use App\Models\Node;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Database\Eloquent\Factories\Factory;
class NodeFactory extends Factory
@@ -33,9 +32,11 @@ class NodeFactory extends Factory
'memory_overallocate' => 0,
'disk' => 10240,
'disk_overallocate' => 0,
'cpu' => 100,
'cpu_overallocate' => 0,
'upload_size' => 100,
'daemon_token_id' => Str::random(Node::DAEMON_TOKEN_ID_LENGTH),
'daemon_token' => Crypt::encrypt(Str::random(Node::DAEMON_TOKEN_LENGTH)),
'daemon_token' => Str::random(Node::DAEMON_TOKEN_LENGTH),
'daemon_listen' => 8080,
'daemon_sftp' => 2022,
'daemon_base' => '/var/lib/panel/volumes',

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