Compare commits

..

157 Commits

Author SHA1 Message Date
Charles
dd7a01aa04 Merge pull request #345 from Boy132/show-git-version
Show update info on dashboard & show git commit (when using git)
2024-06-07 20:00:04 -04:00
Boy132
55badb5644 update colors 2024-06-08 00:43:25 +02:00
Boy132
93f059025c show update info on dashboard, show git commit (when using git) 2024-06-08 00:38:46 +02:00
Charles
7be0cd6928 Merge pull request #323 from Boy132/feature/node-sftp-alias
Add alias for node sftp address
2024-06-07 18:04:44 -04:00
Boy132
0156456919 Merge branch 'pelican-dev:main' into feature/node-sftp-alias 2024-06-07 23:49:38 +02:00
Charles
b9d1ce4438 Merge pull request #334 from pelican-dev/issue/297
Better exception handling
2024-06-07 17:46:33 -04:00
Charles
9ce262bf56 Merge pull request #316 from pelican-dev/issue/node-update
Fix Node Updating
2024-06-07 17:44:10 -04:00
notCharles
7ee52affb2 Update token rotation 2024-06-07 17:38:58 -04:00
Charles
93bfe925b9 Merge pull request #333 from pelican-dev/issue/2
Remove unused parameters
2024-06-07 17:32:40 -04:00
Boy132
cc1ac1eba1 Allow importing eggs via url (#344)
* allow importing eggs via url

* refactor

* run pint

* turn back into one button

* fix empty check

* small cleanup

* removed container for tabs

* Update URL function

* Use sys temp

---------

Co-authored-by: notCharles <charles@pelican.dev>
2024-06-07 17:31:34 -04:00
Charles
02d24b8a36 Fix the egg variable disaster... (#331)
* Migrations to update existing eggs in db

* Update stock eggs

* Update Eggs on import

* Also update updated versions of eggs that are uploaded

* Redo this..

Tests passed locally.

* Pint & Update replace

* Squash Migrations, simplify logic

* Maybe this way...

* Swap them over to single call

---------

Co-authored-by: Lance Pioch <git@lance.sh>
2024-06-07 16:23:25 -04:00
Charles
16fac3b5c6 Merge pull request #337 from Boy132/fix/schedules-run-every-minute
Fix schedules running every minute
2024-06-07 05:43:56 -04:00
Lance Pioch
eb99f53d87 Reset this for now 2024-06-07 00:08:41 -04:00
Lance Pioch
643e4168b9 Add required rule separately 2024-06-06 19:39:46 -04:00
Lance Pioch
51cd7a8e81 Remove unused route files 2024-06-06 16:15:35 -04:00
Boy132
91bf38b63d fix schedules running every minute 2024-06-06 15:53:29 +02:00
Charles
e3699f34d8 Merge pull request #336 from Boy132/fix/default-database-path
Use env value instead of config value for database path
2024-06-06 06:09:51 -04:00
Charles
dc3da2dc98 Merge pull request #335 from Boy132/add/mounts-helper-text
Add helper text to mounts on EditServer page
2024-06-06 06:05:27 -04:00
Boy132
d245751c97 use env value instead of config value 2024-06-06 11:59:24 +02:00
Boy132
e0d7a094ab add helper text to mounts 2024-06-06 10:18:05 +02:00
Lance Pioch
3010e3d61e Better default 2024-06-05 23:37:12 -04:00
Lance Pioch
d68e7218a8 Reformat as table 2024-06-05 23:37:09 -04:00
Lance Pioch
a4435a7454 Pint fix 2024-06-05 22:12:53 -04:00
Lance Pioch
df26c4f9f5 Better exception handling 2024-06-05 21:49:09 -04:00
Lance Pioch
6f1de67523 Remove extraneous parameters 2024-06-05 16:03:04 -04:00
Charles
6f009ee126 Remove cli from php
Every workflow run hangs at attempting to add the cli package and adds ~1 min to the workflow.
2024-06-05 14:15:33 -04:00
Boy132
328e159c6b Merge branch 'pelican-dev:main' into feature/node-sftp-alias 2024-06-05 08:47:20 +02:00
Boy132
f9fd426aca change column type to string
Co-authored-by: Lance Pioch <lancepioch@gmail.com>
2024-06-05 08:47:11 +02:00
Lance Pioch
6166fac929 Merge pull request #322 from Boy132/fix/make-user-db-test
Replace DB check in MakeUserCommand
2024-06-04 17:33:47 -04:00
Lance Pioch
4bd1070025 Merge pull request #324 from Boy132/patch-1
Remove maxLength from `variable_value` input
2024-06-04 17:30:16 -04:00
Lance Pioch
2d6e30b646 Merge pull request #326 from Boy132/fix/artisan-queue-again
Another call fix in AppSettingsCommand
2024-06-04 17:29:48 -04:00
Lance Pioch
f61c6b9dc2 Merge pull request #327 from Boy132/patch-2
Fix default sqlite database path in setup command
2024-06-04 17:28:45 -04:00
Lance Pioch
5e29737dc5 Merge pull request #328 from Boy132/fix/pelicanignore
Replace `panelignore` with `pelicanignore`
2024-06-04 15:43:09 -04:00
Boy132
d996019204 fix eslint 2024-06-04 17:49:04 +02:00
Boy132
91d8dbd084 replace panelignore with pelicanignore 2024-06-04 17:48:02 +02:00
Boy132
bb03ddda50 listen on all queues 2024-06-04 17:26:19 +02:00
Boy132
1c66681c0e make default sqlite database path relative 2024-06-04 13:26:05 +02:00
Boy132
0728266826 restart queue service if service already exists 2024-06-04 13:14:54 +02:00
Boy132
d81c9faac6 improve prompts 2024-06-04 13:01:52 +02:00
Boy132
cff54f1969 show output when running p:environment:queue-service 2024-06-04 13:01:24 +02:00
Boy132
201563a13b remove maxLength from variable_value input 2024-06-04 11:20:40 +02:00
Boy132
8f2261f6cd add alias for node sftp address 2024-06-04 09:17:36 +02:00
Boy132
29cc92f0dc replace db check in MakeUserCommand 2024-06-04 08:33:54 +02:00
Lance Pioch
33f10cbcb9 Merge pull request #312 from RMartinOscar/patch-1
Update EditUser.php
2024-06-03 10:35:31 -04:00
Lance Pioch
b538532e34 Merge pull request #314 from RMartinOscar/patch-2
Update EditDatabaseHost.php
2024-06-03 10:35:07 -04:00
Lance Pioch
a892821b4f Merge pull request #319 from RMartinOscar/patch-3
Update AllocationsRelationManager to allow big endian
2024-06-03 10:34:27 -04:00
Lance Pioch
5a3b50b31f Apply suggestions from code review 2024-06-03 10:34:08 -04:00
Lance Pioch
51b217571b Merge pull request #320 from Boy132/fix/artisan-call
Fix artisan call in AppSettingsCommand
2024-06-03 10:33:00 -04:00
Boy132
6e75c76c60 cleanup 2024-06-03 13:46:48 +02:00
Boy132
e22c5c3e0a fix artisan call in app settings command 2024-06-03 13:43:11 +02:00
MartinOscar
f3171939a4 Update AllocationsRelationManager.php
Remove useless range order
2024-06-03 07:11:09 +02:00
MartinOscar
189d564f87 Update AllocationsRelationManager.php 2024-06-03 06:30:05 +02:00
MartinOscar
7926f97c8e Update EditDatabaseHost.php 2024-06-03 04:09:36 +02:00
MartinOscar
f4d39c1c68 Update EditDatabaseHost.php 2024-06-03 04:02:31 +02:00
Lance Pioch
6c2d0a2d50 Remove shenanigans 2024-06-02 21:59:12 -04:00
MartinOscar
f6899301fd Update EditDatabaseHost.php 2024-06-03 03:54:33 +02:00
MartinOscar
cbb4ef1da2 Update EditUser.php 2024-06-03 03:52:39 +02:00
notCharles
f6ef76d98e Disable delete for own user. 2024-06-02 21:00:11 -04:00
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
162 changed files with 2855 additions and 1729 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
@@ -60,7 +59,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: bcmath, cli, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none
@@ -97,9 +96,8 @@ jobs:
MAIL_MAILER: array
SESSION_DRIVER: array
QUEUE_CONNECTION: sync
HASHIDS_SALT: alittlebitofsalt1234
DB_CONNECTION: sqlite
DB_DATABASE: ${{ github.workspace }}/database/testing.sqlite
DB_DATABASE: testing.sqlite
steps:
- name: Code Checkout
uses: actions/checkout@v4
@@ -121,7 +119,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: bcmath, cli, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none

65
.gitignore vendored
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,22 @@ class AppSettingsCommand extends Command
Artisan::call('key:generate');
}
if ($this->variables['QUEUE_CONNECTION'] !== 'sync') {
$this->call('p:environment:queue-service', [
'--use-redis' => $redisUsed,
]);
}
$this->info($this->console->output());
return 0;
}
/**
* Check if redis is selected, if so, request connection details and verify them.
* Request redis connection details and verify them.
*/
private function checkForRedis()
private function requestRedisSettings(): void
{
$items = collect($this->variables)->filter(function ($item) {
return $item === 'redis';
});
// Redis was not selected, no need to continue.
if (count($items) === 0) {
return;
}
$this->output->note(__('commands.appsettings.redis.note'));
$this->variables['REDIS_HOST'] = $this->option('redis-host') ?? $this->ask(
'Redis Host',

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

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

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

@@ -3,6 +3,7 @@
namespace App\Filament\Resources\DatabaseHostResource\Pages;
use App\Filament\Resources\DatabaseHostResource;
use App\Models\DatabaseHost;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
use Filament\Forms;
@@ -71,20 +72,13 @@ class EditDatabaseHost extends EditRecord
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
Actions\DeleteAction::make()
->label(fn (DatabaseHost $databaseHost) => $databaseHost->databases()->count() > 0 ? 'Database Host Has Databases' : 'Delete')
->disabled(fn (DatabaseHost $databaseHost) => $databaseHost->databases()->count() > 0),
$this->getSaveFormAction()->formId('form'),
];
}
protected function mutateFormDataBeforeSave(array $data): array
{
if (isset($data['password'])) {
$data['password'] = encrypt($data['password']);
}
return $data;
}
protected function getFormActions(): array
{
return [];

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

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

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,49 @@ class EditNode extends EditRecord
])
->default(fn () => request()->isSecure() ? 'https' : 'http'), ]),
Tabs\Tab::make('Advanced Settings')
->columns(['default' => 1, 'sm' => 1, 'md' => 4, 'lg' => 6])
->icon('tabler-server-cog')
->schema([
Forms\Components\TextInput::make('id')
->label('Node ID')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 1])
->disabled(),
Forms\Components\TextInput::make('uuid')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->label('Node UUID')
->hintAction(CopyAction::make())
->columnSpan(2)
->disabled(),
Forms\Components\TagsInput::make('tags')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->label('Tags')
->disabled()
->placeholder('Not Implemented')
->hintIcon('tabler-question-mark')
->hintIconTooltip('Not Implemented')
->columnSpan(1),
->hintIconTooltip('Not Implemented'),
Forms\Components\TextInput::make('upload_size')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 1])
->label('Upload Limit')
->hintIcon('tabler-question-mark')
->hintIconTooltip('Enter the maximum size of files that can be uploaded through the web-based file manager.')
->numeric()->required()
->minValue(1)
->maxValue(1024)
->suffix('MiB'),
Forms\Components\TextInput::make('daemon_sftp')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
->label('SFTP Port')
->minValue(0)
->maxValue(65536)
->default(2022)
->required()
->integer(),
Forms\Components\TextInput::make('daemon_sftp_alias')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
->label('SFTP Alias')
->helperText('Display alias for the SFTP address. Leave empty to use the Node FQDN.'),
Forms\Components\ToggleButtons::make('public')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
->label('Automatic Allocation')->inline()
->columnSpan(1)
->options([
true => 'Yes',
false => 'No',
@@ -214,29 +239,20 @@ class EditNode extends EditRecord
false => 'danger',
]),
Forms\Components\ToggleButtons::make('maintenance_mode')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
->label('Maintenance Mode')->inline()
->columnSpan(1)
->hinticon('tabler-question-mark')
->hintIconTooltip("If the node is marked 'Under Maintenance' users won't be able to access servers that are on this node.")
->options([
true => 'Enable',
false => 'Disable',
true => 'Enable',
])
->colors([
true => 'danger',
false => 'success',
true => 'danger',
]),
Forms\Components\TextInput::make('upload_size')
->label('Upload Limit')
->hintIcon('tabler-question-mark')
->hintIconTooltip('Enter the maximum size of files that can be uploaded through the web-based file manager.')
->columnStart(4)->columnSpan(1)
->numeric()->required()
->minValue(1)
->maxValue(1024)
->suffix('MiB'),
Forms\Components\Grid::make()
->columns(6)
->columns(['default' => 1, 'sm' => 1, 'md' => 3, 'lg' => 6])
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_mem')
@@ -253,14 +269,14 @@ class EditNode extends EditRecord
true => 'primary',
false => 'warning',
])
->columnSpan(2),
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2]),
Forms\Components\TextInput::make('memory')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
->label('Memory Limit')->inlineLabel()
->suffix('MiB')
->required()
->columnSpan(2)
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->numeric()
->minValue(0),
Forms\Components\TextInput::make('memory_overallocate')
@@ -270,15 +286,14 @@ class EditNode extends EditRecord
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
->hintIcon('tabler-question-mark')
->hintIconTooltip('The % allowable to go over the set limit.')
->columnSpan(2)
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->numeric()
->minValue(-1)
->maxValue(100)
->suffix('%'),
]),
Forms\Components\Grid::make()
->columns(6)
->columnSpanFull()
->columns(['default' => 1, 'sm' => 1, 'md' => 3, 'lg' => 6])
->schema([
Forms\Components\ToggleButtons::make('unlimited_disk')
->label('Disk')->inlineLabel()->inline()
@@ -294,14 +309,14 @@ class EditNode extends EditRecord
true => 'primary',
false => 'warning',
])
->columnSpan(2),
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2]),
Forms\Components\TextInput::make('disk')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
->label('Disk Limit')->inlineLabel()
->suffix('MiB')
->required()
->columnSpan(2)
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->numeric()
->minValue(0),
Forms\Components\TextInput::make('disk_overallocate')
@@ -310,6 +325,47 @@ class EditNode extends EditRecord
->label('Overallocate')->inlineLabel()
->hintIcon('tabler-question-mark')
->hintIconTooltip('The % allowable to go over the set limit.')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->required()
->numeric()
->minValue(-1)
->maxValue(100)
->suffix('%'),
]),
Forms\Components\Grid::make()
->columns(6)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_cpu')
->label('CPU')->inlineLabel()->inline()
->live()
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu', 0))
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu_overallocate', 0))
->formatStateUsing(fn (Forms\Get $get) => $get('cpu') == 0)
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
Forms\Components\TextInput::make('cpu')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_cpu'))
->label('CPU Limit')->inlineLabel()
->suffix('%')
->required()
->columnSpan(2)
->numeric()
->minValue(0),
Forms\Components\TextInput::make('cpu_overallocate')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_cpu'))
->label('Overallocate')->inlineLabel()
->hintIcon('tabler-question-mark')
->hintIconTooltip('The % allowable to go over the set limit.')
->columnSpan(2)
->required()
->numeric()
@@ -332,6 +388,18 @@ class EditNode extends EditRecord
->rows(19)
->hintAction(CopyAction::make())
->columnSpanFull(),
Forms\Components\Actions::make([
Forms\Components\Actions\Action::make('resetKey')
->label('Reset Daemon Token')
->color('danger')
->requiresConfirmation()
->modalHeading('Reset Daemon Token?')
->modalDescription('Resetting the daemon token will void any request coming from the old token. This token is used for all sensitive operations on the daemon including server creation and deletion. We suggest changing this token regularly for security.')
->action(fn (NodeUpdateService $nodeUpdateService, Node $node) => $nodeUpdateService->handle($node, [], true)
&& Notification::make()->success()->title('Daemon Key Reset')->send()
&& $this->fillForm()
),
]),
]),
]),
]);
@@ -367,4 +435,9 @@ class EditNode extends EditRecord
NodeMemoryChart::class,
];
}
protected function afterSave(): void
{
$this->fillForm();
}
}

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([
//
@@ -112,7 +113,7 @@ class AllocationsRelationManager extends RelationManager
$start = max((int) $start, 0);
$end = min((int) $end, 2 ** 16 - 1);
for ($i = $start; $i <= $end; $i++) {
foreach (range($start, $end) as $i) {
$ports->push($i);
}
}

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')
@@ -410,19 +371,20 @@ class CreateServer extends CreateRecord
$text = Forms\Components\TextInput::make('variable_value')
->hidden($this->shouldHideComponent(...))
->maxLength(191)
->rules([
->required(fn (Forms\Get $get) => in_array('required', explode('|', $get('rules'))))
->rules(
fn (Forms\Get $get): Closure => function (string $attribute, $value, Closure $fail) use ($get) {
$validator = Validator::make(['validatorkey' => $value], [
'validatorkey' => $get('rules'),
]);
if ($validator->fails()) {
$message = str($validator->errors()->first())->replace('validatorkey', $get('name'));
$message = str($validator->errors()->first())->replace('validatorkey', $get('name'))->toString();
$fail($message);
}
},
]);
);
$select = Forms\Components\Select::make('variable_value')
->hidden($this->shouldHideComponent(...))
@@ -452,7 +414,7 @@ class CreateServer extends CreateRecord
->columnSpan(2),
]),
Forms\Components\Section::make('Resource Management')
Forms\Components\Section::make('Environment Management')
->collapsed()
->icon('tabler-server-cog')
->iconColor('primary')
@@ -464,175 +426,190 @@ class CreateServer extends CreateRecord
])
->columnSpanFull()
->schema([
Forms\Components\Grid::make()
->columns(4)
->columnSpanFull()
Forms\Components\Fieldset::make('Resource Limits')
->columnSpan([
'default' => 2,
'sm' => 4,
'md' => 4,
'lg' => 6,
])
->columns([
'default' => 1,
'sm' => 2,
'md' => 3,
'lg' => 3,
])
->schema([
Forms\Components\ToggleButtons::make('unlimited_mem')
->label('Memory')->inlineLabel()->inline()
->default(true)
->afterStateUpdated(fn (Forms\Set $set) => $set('memory', 0))
->live()
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
Forms\Components\Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_mem')
->label('Memory')->inlineLabel()->inline()
->default(true)
->afterStateUpdated(fn (Forms\Set $set) => $set('memory', 0))
->live()
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
Forms\Components\TextInput::make('memory')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
->label('Memory Limit')->inlineLabel()
->suffix('MiB')
->default(0)
->required()
->columnSpan(2)
->numeric()
->minValue(0),
]),
Forms\Components\Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_disk')
->label('Disk Space')->inlineLabel()->inline()
->default(true)
->live()
->afterStateUpdated(fn (Forms\Set $set) => $set('disk', 0))
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
Forms\Components\TextInput::make('disk')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
->label('Disk Space Limit')->inlineLabel()
->suffix('MiB')
->default(0)
->required()
->columnSpan(2)
->numeric()
->minValue(0),
]),
Forms\Components\Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_cpu')
->label('CPU')->inlineLabel()->inline()
->default(true)
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu', 0))
->live()
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
Forms\Components\TextInput::make('cpu')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_cpu'))
->label('CPU Limit')->inlineLabel()
->suffix('%')
->default(0)
->required()
->columnSpan(2)
->numeric()
->minValue(0)
->helperText('100% equals one logical thread'),
]),
Forms\Components\Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('swap_support')
->live()
->label('Enable Swap Memory')
->inlineLabel()
->inline()
->columnSpan(2)
->default('disabled')
->afterStateUpdated(function ($state, Forms\Set $set) {
$value = match ($state) {
'unlimited' => -1,
'disabled' => 0,
'limited' => 128,
};
$set('swap', $value);
})
->options([
'unlimited' => 'Unlimited',
'limited' => 'Limited',
'disabled' => 'Disabled',
])
->colors([
'unlimited' => 'primary',
'limited' => 'warning',
'disabled' => 'danger',
Forms\Components\TextInput::make('memory')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
->label('Memory Limit')->inlineLabel()
->suffix('MiB')
->default(0)
->required()
->columnSpan(2)
->numeric()
->minValue(0),
]),
Forms\Components\TextInput::make('swap')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => match ($get('swap_support')) {
'disabled', 'unlimited' => true,
'limited' => false,
})
->label('Swap Memory')
->default(0)
->suffix('MiB')
->minValue(-1)
->columnSpan(2)
->inlineLabel()
->required()
->integer(),
]),
Forms\Components\Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_disk')
->label('Disk Space')->inlineLabel()->inline()
->default(true)
->live()
->afterStateUpdated(fn (Forms\Set $set) => $set('disk', 0))
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
Forms\Components\Hidden::make('io')
->helperText('The IO performance relative to other running containers')
->label('Block IO Proportion')
->default(500),
Forms\Components\Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('oom_killer')
->label('OOM Killer')
->inlineLabel()->inline()
->default(false)
->columnSpan(2)
->options([
false => 'Disabled',
true => 'Enabled',
])
->colors([
false => 'success',
true => 'danger',
Forms\Components\TextInput::make('disk')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
->label('Disk Space Limit')->inlineLabel()
->suffix('MiB')
->default(0)
->required()
->columnSpan(2)
->numeric()
->minValue(0),
]),
Forms\Components\TextInput::make('oom_disabled_hidden')
->hidden(),
Forms\Components\Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_cpu')
->label('CPU')->inlineLabel()->inline()
->default(true)
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu', 0))
->live()
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
Forms\Components\TextInput::make('cpu')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_cpu'))
->label('CPU Limit')->inlineLabel()
->suffix('%')
->default(0)
->required()
->columnSpan(2)
->numeric()
->minValue(0)
->helperText('100% equals one CPU core.'),
]),
Forms\Components\Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('swap_support')
->live()
->label('Enable Swap Memory')
->inlineLabel()
->inline()
->columnSpan(2)
->default('disabled')
->afterStateUpdated(function ($state, Forms\Set $set) {
$value = match ($state) {
'unlimited' => -1,
'disabled' => 0,
'limited' => 128,
};
$set('swap', $value);
})
->options([
'unlimited' => 'Unlimited',
'limited' => 'Limited',
'disabled' => 'Disabled',
])
->colors([
'unlimited' => 'primary',
'limited' => 'warning',
'disabled' => 'danger',
]),
Forms\Components\TextInput::make('swap')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => match ($get('swap_support')) {
'disabled', 'unlimited' => true,
'limited' => false,
})
->label('Swap Memory')
->default(0)
->suffix('MiB')
->minValue(-1)
->columnSpan(2)
->inlineLabel()
->required()
->integer(),
]),
Forms\Components\Hidden::make('io')
->helperText('The IO performance relative to other running containers')
->label('Block IO Proportion')
->default(500),
Forms\Components\Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('oom_killer')
->label('OOM Killer')
->inlineLabel()->inline()
->default(false)
->columnSpan(2)
->options([
false => 'Disabled',
true => 'Enabled',
])
->colors([
false => 'success',
true => 'danger',
]),
Forms\Components\TextInput::make('oom_disabled_hidden')
->hidden(),
]),
]),
Forms\Components\Fieldset::make('Application Feature Limits')
Forms\Components\Fieldset::make('Feature Limits')
->inlineLabel()
->columnSpan([
'default' => 2,
@@ -663,6 +640,70 @@ class CreateServer extends CreateRecord
->numeric()
->default(0),
]),
Forms\Components\Fieldset::make('Docker Settings')
->columnSpan([
'default' => 2,
'sm' => 4,
'md' => 4,
'lg' => 6,
])
->columns([
'default' => 1,
'sm' => 2,
'md' => 3,
'lg' => 3,
])
->schema([
Forms\Components\Select::make('select_image')
->label('Image Name')
->afterStateUpdated(fn (Forms\Set $set, $state) => $set('image', $state))
->options(function ($state, Forms\Get $get, Forms\Set $set) {
$egg = Egg::query()->find($get('egg_id'));
$images = $egg->docker_images ?? [];
$currentImage = $get('image');
if (!$currentImage && $images) {
$defaultImage = collect($images)->first();
$set('image', $defaultImage);
$set('select_image', $defaultImage);
}
return array_flip($images) + ['ghcr.io/custom-image' => 'Custom Image'];
})
->selectablePlaceholder(false)
->columnSpan(1),
Forms\Components\TextInput::make('image')
->label('Image')
->debounce(500)
->afterStateUpdated(function ($state, Forms\Get $get, Forms\Set $set) {
$egg = Egg::query()->find($get('egg_id'));
$images = $egg->docker_images ?? [];
if (in_array($state, $images)) {
$set('select_image', $state);
} else {
$set('select_image', 'ghcr.io/custom-image');
}
})
->placeholder('Enter a custom Image')
->columnSpan(1),
Forms\Components\KeyValue::make('docker_labels')
->label('Container Labels')
->keyLabel('Title')
->valueLabel('Description')
->columnSpan(3),
Forms\Components\CheckboxList::make('mounts')
->live()
->relationship('mounts')
->options(fn () => $this->node?->mounts->mapWithKeys(fn ($mount) => [$mount->id => $mount->name]) ?? [])
->descriptions(fn () => $this->node?->mounts->mapWithKeys(fn ($mount) => [$mount->id => "$mount->source -> $mount->target"]) ?? [])
->label('Mounts')
->helperText(fn () => $this->node?->mounts->isNotEmpty() ? '' : 'No Mounts exist for this Node')
->columnSpanFull(),
]),
]),
]);
}

File diff suppressed because it is too large Load Diff

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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
@@ -124,38 +124,6 @@ class EggConfigurationService
return $response;
}
/**
* Replaces the legacy modifies from eggs with their new counterpart. The legacy Daemon would
* set SERVER_MEMORY, SERVER_IP, and SERVER_PORT with their respective values on the Daemon
* side. Ensure that anything referencing those properly replaces them with the matching config
* value.
*/
protected function replaceLegacyModifiers(string $key, string $value): string
{
switch ($key) {
case 'config.docker.interface':
$replace = 'config.docker.network.interface';
break;
case 'server.build.env.SERVER_MEMORY':
case 'env.SERVER_MEMORY':
$replace = 'server.build.memory';
break;
case 'server.build.env.SERVER_IP':
case 'env.SERVER_IP':
$replace = 'server.build.default.ip';
break;
case 'server.build.env.SERVER_PORT':
case 'env.SERVER_PORT':
$replace = 'server.build.default.port';
break;
default:
// By default, we don't need to change anything, only if we ended up matching a specific legacy item.
$replace = $key;
}
return str_replace("{{{$key}}}", "{{{$replace}}}", $value);
}
protected function matchAndReplaceKeys(mixed $value, array $structure): mixed
{
preg_match_all('/{{(?<key>[\w.-]*)}}/', $value, $matches);
@@ -175,8 +143,6 @@ class EggConfigurationService
continue;
}
$value = $this->replaceLegacyModifiers($key, $value);
// We don't want to do anything with config keys since the Daemon will need to handle
// that. For example, the Spigot egg uses "config.docker.interface" to identify the Docker
// interface to proxy through, but the Panel would be unaware of that.
@@ -198,7 +164,7 @@ class EggConfigurationService
// variable from the server configuration.
$plucked = Arr::get(
$structure,
preg_replace('/^env\./', 'build.env.', $key),
preg_replace('/^env\./', 'build.environment.', $key),
''
);

View File

@@ -10,6 +10,16 @@ use App\Exceptions\Service\InvalidFileUploadException;
class EggParserService
{
public const UPGRADE_VARIABLES = [
'server.build.env.SERVER_IP' => 'server.allocations.default.ip',
'server.build.default.ip' => 'server.allocations.default.ip',
'server.build.env.SERVER_PORT' => 'server.allocations.default.port',
'server.build.default.port' => 'server.allocations.default.port',
'server.build.env.SERVER_MEMORY' => 'server.build.memory_limit',
'server.build.memory' => 'server.build.memory_limit',
'server.build.env' => 'server.build.environment',
];
/**
* Takes an uploaded file and parses out the egg configuration from within.
*
@@ -26,11 +36,20 @@ class EggParserService
$version = $parsed['meta']['version'] ?? '';
return match ($version) {
$parsed = match ($version) {
'PTDL_v1' => $this->convertToV2($parsed),
'PTDL_v2' => $parsed,
default => throw new InvalidFileUploadException('The JSON file provided is not in a format that can be recognized.')
};
// Make sure we only use recent variable format from now on
$parsed['config']['files'] = str_replace(
array_keys(self::UPGRADE_VARIABLES),
array_values(self::UPGRADE_VARIABLES),
$parsed['config']['files'] ?? '',
);
return $parsed;
}
/**

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

@@ -9,6 +9,7 @@ use Illuminate\Http\UploadedFile;
use App\Models\EggVariable;
use Illuminate\Database\ConnectionInterface;
use App\Services\Eggs\EggParserService;
use Spatie\TemporaryDirectory\TemporaryDirectory;
class EggImporterService
{
@@ -21,13 +22,16 @@ class EggImporterService
*
* @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable
*/
public function handle(UploadedFile $file): Egg
public function fromFile(UploadedFile $file): Egg
{
$parsed = $this->parser->handle($file);
return $this->connection->transaction(function () use ($parsed) {
$egg = (new Egg())->forceFill([
'uuid' => Uuid::uuid4()->toString(),
$uuid = $parsed['uuid'] ?? Uuid::uuid4()->toString();
$egg = Egg::where('uuid', $uuid)->first() ?? new Egg();
$egg = $egg->forceFill([
'uuid' => $uuid,
'author' => Arr::get($parsed, 'author'),
'copy_script_from' => null,
]);
@@ -42,4 +46,20 @@ class EggImporterService
return $egg;
});
}
/**
* Take an url and parse it into a new egg.
*
* @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable
*/
public function fromUrl(string $url): Egg
{
$info = pathinfo($url);
$tmpDir = TemporaryDirectory::make()->deleteWhenDestroyed();
$tmpPath = $tmpDir->path($info['basename']);
file_put_contents($tmpPath, file_get_contents($url));
return $this->fromFile(new UploadedFile($tmpPath, $info['basename'], 'application/json'));
}
}

View File

@@ -8,6 +8,7 @@ use Illuminate\Support\Collection;
use App\Models\EggVariable;
use Illuminate\Database\ConnectionInterface;
use App\Services\Eggs\EggParserService;
use Spatie\TemporaryDirectory\TemporaryDirectory;
class EggUpdateImporterService
{
@@ -23,7 +24,7 @@ class EggUpdateImporterService
*
* @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable
*/
public function handle(Egg $egg, UploadedFile $file): Egg
public function fromFile(Egg $egg, UploadedFile $file): Egg
{
$parsed = $this->parser->handle($file);
@@ -47,4 +48,20 @@ class EggUpdateImporterService
return $egg->refresh();
});
}
/**
* Update an existing Egg using an url.
*
* @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable
*/
public function fromUrl(Egg $egg, string $url): Egg
{
$info = pathinfo($url);
$tmpDir = TemporaryDirectory::make()->deleteWhenDestroyed();
$tmpPath = $tmpDir->path($info['basename']);
file_put_contents($tmpPath, file_get_contents($url));
return $this->fromFile($egg, new UploadedFile($tmpPath, $info['basename'], 'application/json'));
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Services\Exceptions;
use Exception;
use Filament\Notifications\Notification;
class FilamentExceptionHandler
{
public function handle(Exception $exception, callable $stopPropagation): void
{
Notification::make()
->title($exception->title ?? null)
->body($exception->body ?? $exception->getMessage())
->color($exception->color ?? 'danger')
->icon($exception->icon ?? 'tabler-x')
->danger()
->send();
if ($this->stopPropagation ?? true) {
$stopPropagation();
}
}
}

View File

@@ -49,6 +49,14 @@ class SoftwareVersionService
return Arr::get(self::$result, 'discord') ?? 'https://pelican.dev/discord';
}
/**
* Get the donation URL.
*/
public function getDonations(): string
{
return Arr::get(self::$result, 'donate') ?? 'https://pelican.dev/donate';
}
/**
* Determine if the current version of the panel is the latest.
*/
@@ -93,8 +101,28 @@ class SoftwareVersionService
});
}
public function getDonations(): string
public function versionData(): array
{
return 'https://github.com';
return cache()->remember('git-version', 5, function () {
if (file_exists(base_path('.git/HEAD'))) {
$head = explode(' ', file_get_contents(base_path('.git/HEAD')));
if (array_key_exists(1, $head)) {
$path = base_path('.git/' . trim($head[1]));
}
}
if (isset($path) && file_exists($path)) {
return [
'version' => 'canary (' . substr(file_get_contents($path), 0, 8) . ')',
'is_git' => true,
];
}
return [
'version' => config('app.version'),
'is_git' => false,
];
});
}
}

View File

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

@@ -27,23 +27,19 @@ class NodeUpdateService
*/
public function handle(Node $node, array $data, bool $resetToken = false): Node
{
$data['id'] = $node->id;
if ($resetToken) {
$data['daemon_token'] = encrypt(Str::random(Node::DAEMON_TOKEN_LENGTH));
$data['daemon_token'] = Str::random(Node::DAEMON_TOKEN_LENGTH);
$data['daemon_token_id'] = Str::random(Node::DAEMON_TOKEN_ID_LENGTH);
}
[$updated, $exception] = $this->connection->transaction(function () use ($data, $node) {
/** @var \App\Models\Node $updated */
$updated = $node->replicate()->forceFill($data)->save();
$updated = $node->replicate();
$updated->exists = true;
$updated->forceFill($data)->save();
try {
// If we're changing the FQDN for the node, use the newly provided FQDN for the connection
// address. This should alleviate issues where the node gets pointed to a "valid" FQDN that
// isn't actually running the daemon software, and therefore you can't actually change it
// back.
//
// This makes more sense anyways, because only the Panel uses the FQDN for connecting, the
// node doesn't actually care about this.
$node->fqdn = $updated->fqdn;
$this->configurationRepository->setNode($node)->update($updated);

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;

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