mirror of
https://github.com/pelican-dev/panel.git
synced 2026-05-04 18:00:48 +03:00
Compare commits
225 Commits
release/v1
...
v1.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40819cf171 | ||
|
|
133b94ab08 | ||
|
|
82c0568129 | ||
|
|
75d35e6ee8 | ||
|
|
2a740b43e6 | ||
|
|
818a8a42ad | ||
|
|
67dbf772d5 | ||
|
|
efb834c8f7 | ||
|
|
cf37994c3b | ||
|
|
fc92a87993 | ||
|
|
f459987458 | ||
|
|
5290b8f8bb | ||
|
|
e08cbdecd4 | ||
|
|
70c31eef8f | ||
|
|
5409532ca1 | ||
|
|
a1190c12e0 | ||
|
|
42ca4e7fba | ||
|
|
d6b71885ec | ||
|
|
7b0a15e746 | ||
|
|
7813b6060c | ||
|
|
c431775b7e | ||
|
|
6692942f6f | ||
|
|
276b51f477 | ||
|
|
d4eecdd53d | ||
|
|
d7316c4dfe | ||
|
|
011579451d | ||
|
|
6b5b480902 | ||
|
|
87dc8066c9 | ||
|
|
aa08e774a1 | ||
|
|
482e8ed6b2 | ||
|
|
59bbb63739 | ||
|
|
f4c3c89c17 | ||
|
|
fe4e6271fb | ||
|
|
8ee5d6aabd | ||
|
|
42ecd2951d | ||
|
|
7a6edab79a | ||
|
|
4f43e9171a | ||
|
|
5a3c606627 | ||
|
|
6916b89638 | ||
|
|
0da184c56e | ||
|
|
ce1163d387 | ||
|
|
cd4fc1a95d | ||
|
|
0c0b468525 | ||
|
|
12518bc5d6 | ||
|
|
7c829fb9cf | ||
|
|
61f3e965ba | ||
|
|
10796f8916 | ||
|
|
1d66d4c320 | ||
|
|
e95cd0cd98 | ||
|
|
46a24a087b | ||
|
|
f216376265 | ||
|
|
6d6b50c27d | ||
|
|
58bfa12280 | ||
|
|
8e5660a1b9 | ||
|
|
beac4cd3f6 | ||
|
|
9184441763 | ||
|
|
3ac23d1514 | ||
|
|
6295ea34de | ||
|
|
3cadbbc60c | ||
|
|
60c5f826d6 | ||
|
|
1047e8f948 | ||
|
|
f3501d8b14 | ||
|
|
9114685680 | ||
|
|
8080435eca | ||
|
|
c5824ff26c | ||
|
|
dd7a01aa04 | ||
|
|
55badb5644 | ||
|
|
93f059025c | ||
|
|
7be0cd6928 | ||
|
|
0156456919 | ||
|
|
b9d1ce4438 | ||
|
|
9ce262bf56 | ||
|
|
7ee52affb2 | ||
|
|
93bfe925b9 | ||
|
|
cc1ac1eba1 | ||
|
|
02d24b8a36 | ||
|
|
16fac3b5c6 | ||
|
|
6b249b9545 | ||
|
|
70fc84309f | ||
|
|
f43fb985a2 | ||
|
|
eb99f53d87 | ||
|
|
643e4168b9 | ||
|
|
51cd7a8e81 | ||
|
|
91bf38b63d | ||
|
|
e3699f34d8 | ||
|
|
dc3da2dc98 | ||
|
|
d245751c97 | ||
|
|
e0d7a094ab | ||
|
|
3010e3d61e | ||
|
|
d68e7218a8 | ||
|
|
a4435a7454 | ||
|
|
df26c4f9f5 | ||
|
|
6f1de67523 | ||
|
|
6f009ee126 | ||
|
|
328e159c6b | ||
|
|
f9fd426aca | ||
|
|
6166fac929 | ||
|
|
4bd1070025 | ||
|
|
2d6e30b646 | ||
|
|
f61c6b9dc2 | ||
|
|
5e29737dc5 | ||
|
|
d996019204 | ||
|
|
91d8dbd084 | ||
|
|
bb03ddda50 | ||
|
|
1c66681c0e | ||
|
|
0728266826 | ||
|
|
d81c9faac6 | ||
|
|
cff54f1969 | ||
|
|
201563a13b | ||
|
|
8f2261f6cd | ||
|
|
29cc92f0dc | ||
|
|
33f10cbcb9 | ||
|
|
b538532e34 | ||
|
|
a892821b4f | ||
|
|
5a3b50b31f | ||
|
|
51b217571b | ||
|
|
6e75c76c60 | ||
|
|
e22c5c3e0a | ||
|
|
f3171939a4 | ||
|
|
189d564f87 | ||
|
|
7926f97c8e | ||
|
|
f4d39c1c68 | ||
|
|
6c2d0a2d50 | ||
|
|
f6899301fd | ||
|
|
cbb4ef1da2 | ||
|
|
f6ef76d98e | ||
|
|
65a697d8f7 | ||
|
|
9515a82a75 | ||
|
|
44f5ea567f | ||
|
|
88f910f3e7 | ||
|
|
020f028008 | ||
|
|
0cb7f737b0 | ||
|
|
53aa52f519 | ||
|
|
e884eda5a7 | ||
|
|
58d1fd3917 | ||
|
|
0b0952650e | ||
|
|
aa55a7ed83 | ||
|
|
c7fa7a1bad | ||
|
|
4a3bdd78ef | ||
|
|
a1067fd4aa | ||
|
|
110cc1248b | ||
|
|
04a1ccc97e | ||
|
|
5e7f5c2a4c | ||
|
|
b804878d7b | ||
|
|
118977c8c5 | ||
|
|
c31b7b8c6a | ||
|
|
eefe59b153 | ||
|
|
cd4b7cbf9e | ||
|
|
67cb3d4816 | ||
|
|
7762e68a6c | ||
|
|
7a327ea378 | ||
|
|
b3ca7b7ac9 | ||
|
|
abc99cd928 | ||
|
|
cb638369cf | ||
|
|
9174de2d8c | ||
|
|
7cda358b66 | ||
|
|
33f6551b21 | ||
|
|
b1928e89b4 | ||
|
|
c956cd0106 | ||
|
|
5081cc3f63 | ||
|
|
8eb2c23420 | ||
|
|
cfe385f53a | ||
|
|
264d3498a6 | ||
|
|
065f3f2468 | ||
|
|
957638d4ac | ||
|
|
7d0ce1627b | ||
|
|
8cec7368ab | ||
|
|
5519931ee5 | ||
|
|
97ac0fe54b | ||
|
|
7657364208 | ||
|
|
ef1a208b95 | ||
|
|
aa82c6dd04 | ||
|
|
8ecabef6b5 | ||
|
|
a6d07ede5a | ||
|
|
f6325c07c4 | ||
|
|
7674ee0e2b | ||
|
|
5760e72b8f | ||
|
|
b6e46f758d | ||
|
|
e980877bbc | ||
|
|
dd223b47c0 | ||
|
|
639fa3399d | ||
|
|
82fd547484 | ||
|
|
d461242f08 | ||
|
|
dec1cf8e74 | ||
|
|
15caac51fb | ||
|
|
183c274a0d | ||
|
|
a8b2fb440f | ||
|
|
f8e4514998 | ||
|
|
deeebf73d3 | ||
|
|
422fc102c9 | ||
|
|
e715e92f9d | ||
|
|
73babfa2b3 | ||
|
|
e0a92d733b | ||
|
|
1e67cd9944 | ||
|
|
3946116dff | ||
|
|
b77fd3d653 | ||
|
|
f4672c6cb1 | ||
|
|
5b9e4b1729 | ||
|
|
48f715ae69 | ||
|
|
51460782cc | ||
|
|
b007e63937 | ||
|
|
4dd833562b | ||
|
|
b579f14f3f | ||
|
|
eadaec1b30 | ||
|
|
a9e58bb493 | ||
|
|
5c33c7495a | ||
|
|
f9aa8cf218 | ||
|
|
da698a3666 | ||
|
|
2808a3dd35 | ||
|
|
7ea365e8de | ||
|
|
ae399f9bad | ||
|
|
53a5ff6e6d | ||
|
|
54ae4b3dc1 | ||
|
|
859a721e17 | ||
|
|
03cbdd5bdd | ||
|
|
4c43fd1683 | ||
|
|
0c61a63191 | ||
|
|
b1f99ca8a3 | ||
|
|
0a5810358a | ||
|
|
1bae239971 | ||
|
|
597f74f105 | ||
|
|
5344d99a40 | ||
|
|
1db1a1a3e0 | ||
|
|
712b6a285b | ||
|
|
38b92ae21d |
@@ -17,9 +17,6 @@ CACHE_STORE=file
|
||||
QUEUE_CONNECTION=database
|
||||
SESSION_DRIVER=file
|
||||
|
||||
HASHIDS_SALT=
|
||||
HASHIDS_LENGTH=8
|
||||
|
||||
MAIL_MAILER=log
|
||||
MAIL_HOST=smtp.example.com
|
||||
MAIL_PORT=25
|
||||
@@ -33,3 +30,8 @@ MAIL_FROM_NAME="Pelican Admin"
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
# Set this to true, and set start & end ports to auto create allocations.
|
||||
PANEL_CLIENT_ALLOCATIONS_ENABLED=false
|
||||
PANEL_CLIENT_ALLOCATIONS_RANGE_START=
|
||||
PANEL_CLIENT_ALLOCATIONS_RANGE_END=
|
||||
|
||||
3
.github/workflows/build.yaml
vendored
3
.github/workflows/build.yaml
vendored
@@ -1,6 +1,9 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
|
||||
85
.github/workflows/ci.yaml
vendored
85
.github/workflows/ci.yaml
vendored
@@ -1,6 +1,9 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
@@ -13,7 +16,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php: [8.2, 8.3]
|
||||
database: ["mariadb:10.2", "mysql:8"]
|
||||
database: ["mysql:8"]
|
||||
services:
|
||||
database:
|
||||
image: ${{ matrix.database }}
|
||||
@@ -34,7 +37,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 +62,79 @@ 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
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install --no-interaction --no-suggest --prefer-dist
|
||||
|
||||
- name: Unit tests
|
||||
run: vendor/bin/phpunit tests/Unit
|
||||
env:
|
||||
DB_HOST: UNIT_NO_DB
|
||||
SKIP_MIGRATIONS: true
|
||||
|
||||
- name: Integration tests
|
||||
run: vendor/bin/phpunit tests/Integration
|
||||
env:
|
||||
DB_PORT: ${{ job.services.database.ports[3306] }}
|
||||
DB_USERNAME: root
|
||||
|
||||
mariadb:
|
||||
name: MariaDB
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php: [8.2, 8.3]
|
||||
database: ["mariadb:10.3", "mariadb:10.11", "mariadb:11.4"]
|
||||
services:
|
||||
database:
|
||||
image: ${{ matrix.database }}
|
||||
env:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: yes
|
||||
MYSQL_DATABASE: testing
|
||||
ports:
|
||||
- 3306
|
||||
options: --health-cmd="mariadb-admin ping || mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
||||
env:
|
||||
APP_ENV: testing
|
||||
APP_DEBUG: "false"
|
||||
APP_KEY: ThisIsARandomStringForTests12345
|
||||
APP_TIMEZONE: UTC
|
||||
APP_URL: http://localhost/
|
||||
APP_ENVIRONMENT_ONLY: "true"
|
||||
CACHE_DRIVER: array
|
||||
MAIL_MAILER: array
|
||||
SESSION_DRIVER: array
|
||||
QUEUE_CONNECTION: sync
|
||||
DB_CONNECTION: mariadb
|
||||
DB_HOST: 127.0.0.1
|
||||
DB_DATABASE: testing
|
||||
DB_USERNAME: root
|
||||
steps:
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get cache directory
|
||||
id: composer-cache
|
||||
run: |
|
||||
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-composer-${{ matrix.php }}-
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
|
||||
@@ -97,9 +171,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 +194,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
|
||||
|
||||
|
||||
29
.github/workflows/lint.yaml
vendored
29
.github/workflows/lint.yaml
vendored
@@ -6,8 +6,8 @@ on:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
pint:
|
||||
name: Pint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Code Checkout
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: "8.2"
|
||||
php-version: "8.3"
|
||||
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
@@ -29,3 +29,26 @@ jobs:
|
||||
|
||||
- name: Pint
|
||||
run: vendor/bin/pint --test
|
||||
phpstan:
|
||||
name: PHPStan
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: "8.3"
|
||||
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
|
||||
- name: Setup .env
|
||||
run: cp .env.example .env
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install --no-interaction --no-progress --prefer-dist
|
||||
|
||||
- name: PHPStan
|
||||
run: vendor/bin/phpstan --memory-limit=-1
|
||||
|
||||
27
.github/workflows/release.yaml
vendored
27
.github/workflows/release.yaml
vendored
@@ -54,31 +54,12 @@ jobs:
|
||||
|
||||
- name: Create release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ contains(github.ref, 'rc') || contains(github.ref, 'beta') || contains(github.ref, 'alpha') }}
|
||||
|
||||
- name: Upload release archive
|
||||
id: upload-release-archive
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: panel.tar.gz
|
||||
asset_name: panel.tar.gz
|
||||
asset_content_type: application/gzip
|
||||
|
||||
- name: Upload release checksum
|
||||
id: upload-release-checksum
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./checksum.txt
|
||||
asset_name: checksum.txt
|
||||
asset_content_type: text/plain
|
||||
files: |
|
||||
panel.tar.gz
|
||||
checksum.txt
|
||||
|
||||
65
.gitignore
vendored
65
.gitignore
vendored
@@ -1,41 +1,28 @@
|
||||
/vendor
|
||||
*.DS_Store*
|
||||
!.env.ci
|
||||
!.env.example
|
||||
.env*
|
||||
.vagrant/*
|
||||
.vscode/*
|
||||
storage/framework/*
|
||||
/.idea
|
||||
/nbproject
|
||||
/.direnv
|
||||
|
||||
node_modules
|
||||
*.log
|
||||
_ide_helper.php
|
||||
_ide_helper_models.php
|
||||
.phpstorm.meta.php
|
||||
.yarn
|
||||
public/assets/manifest.json
|
||||
*.sqlite
|
||||
|
||||
# For local development with docker
|
||||
# Remove if we ever put the Dockerfile in the repo
|
||||
.dockerignore
|
||||
docker-compose.yml
|
||||
|
||||
# for image related files
|
||||
misc
|
||||
.php-cs-fixer.cache
|
||||
coverage.xml
|
||||
resources/lang/locales.js
|
||||
.phpunit.result.cache
|
||||
|
||||
/.phpunit.cache
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/hot
|
||||
result
|
||||
docker-compose.yaml
|
||||
|
||||
public/css/filament-monaco-editor/
|
||||
|
||||
public/js/filament-monaco-editor/
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/storage/clockwork/*
|
||||
/vendor
|
||||
*.DS_Store*
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpactor.json
|
||||
.phpunit.result.cache
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
auth.json
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
/.fleet
|
||||
/.idea
|
||||
/.vscode
|
||||
|
||||
public/assets/manifest.json
|
||||
/database/*.sqlite
|
||||
filament-monaco-editor/
|
||||
_ide_helper*
|
||||
/.phpstorm.meta.php
|
||||
|
||||
@@ -24,15 +24,14 @@ class AppSettingsCommand extends Command
|
||||
];
|
||||
|
||||
public const QUEUE_DRIVERS = [
|
||||
'sync' => 'Synchronous (recommended)',
|
||||
'database' => 'Database',
|
||||
'database' => 'Database (recommended)',
|
||||
'redis' => 'Redis',
|
||||
'sync' => 'Synchronous',
|
||||
];
|
||||
|
||||
protected $description = 'Configure basic environment settings for the Panel.';
|
||||
|
||||
protected $signature = 'p:environment:setup
|
||||
{--new-salt : Whether or not to generate a new salt for Hashids.}
|
||||
{--url= : The URL that this Panel is running on.}
|
||||
{--cache= : The cache driver backend to use.}
|
||||
{--session= : The session driver backend to use.}
|
||||
@@ -61,10 +60,6 @@ class AppSettingsCommand extends Command
|
||||
{
|
||||
$this->variables['APP_TIMEZONE'] = 'UTC';
|
||||
|
||||
if (empty(config('hashids.salt')) || $this->option('new-salt')) {
|
||||
$this->variables['HASHIDS_SALT'] = str_random(20);
|
||||
}
|
||||
|
||||
$this->output->comment(__('commands.appsettings.comment.url'));
|
||||
$this->variables['APP_URL'] = $this->option('url') ?? $this->ask(
|
||||
'Application URL',
|
||||
@@ -103,7 +98,13 @@ class AppSettingsCommand extends Command
|
||||
$this->variables['SESSION_SECURE_COOKIE'] = 'true';
|
||||
}
|
||||
|
||||
$this->checkForRedis();
|
||||
$redisUsed = count(collect($this->variables)->filter(function ($item) {
|
||||
return $item === 'redis';
|
||||
})) !== 0;
|
||||
|
||||
if ($redisUsed) {
|
||||
$this->requestRedisSettings();
|
||||
}
|
||||
|
||||
$path = base_path('.env');
|
||||
if (!file_exists($path)) {
|
||||
@@ -116,25 +117,22 @@ class AppSettingsCommand extends Command
|
||||
Artisan::call('key:generate');
|
||||
}
|
||||
|
||||
if ($this->variables['QUEUE_CONNECTION'] !== 'sync') {
|
||||
$this->call('p:environment:queue-service', [
|
||||
'--use-redis' => $redisUsed,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->info($this->console->output());
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if redis is selected, if so, request connection details and verify them.
|
||||
* Request redis connection details and verify them.
|
||||
*/
|
||||
private function checkForRedis()
|
||||
private function requestRedisSettings(): void
|
||||
{
|
||||
$items = collect($this->variables)->filter(function ($item) {
|
||||
return $item === 'redis';
|
||||
});
|
||||
|
||||
// Redis was not selected, no need to continue.
|
||||
if (count($items) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->output->note(__('commands.appsettings.redis.note'));
|
||||
$this->variables['REDIS_HOST'] = $this->option('redis-host') ?? $this->ask(
|
||||
'Redis Host',
|
||||
|
||||
@@ -13,6 +13,7 @@ class DatabaseSettingsCommand extends Command
|
||||
|
||||
public const DATABASE_DRIVERS = [
|
||||
'sqlite' => 'SQLite (recommended)',
|
||||
'mariadb' => 'MariaDB',
|
||||
'mysql' => 'MySQL',
|
||||
];
|
||||
|
||||
@@ -21,10 +22,10 @@ class DatabaseSettingsCommand extends Command
|
||||
protected $signature = 'p:environment:database
|
||||
{--driver= : The database driver backend to use.}
|
||||
{--database= : The database to use.}
|
||||
{--host= : The connection address for the MySQL server.}
|
||||
{--port= : The connection port for the MySQL server.}
|
||||
{--username= : Username to use when connecting to the MySQL server.}
|
||||
{--password= : Password to use for the MySQL database.}';
|
||||
{--host= : The connection address for the MySQL/ MariaDB server.}
|
||||
{--port= : The connection port for the MySQL/ MariaDB server.}
|
||||
{--username= : Username to use when connecting to the MySQL/ MariaDB server.}
|
||||
{--password= : Password to use for the MySQL/ MariaDB database.}';
|
||||
|
||||
protected array $variables = [];
|
||||
|
||||
@@ -82,7 +83,20 @@ class DatabaseSettingsCommand extends Command
|
||||
}
|
||||
|
||||
try {
|
||||
$this->testMySQLConnection();
|
||||
// Test connection
|
||||
config()->set('database.connections._panel_command_test', [
|
||||
'driver' => 'mysql',
|
||||
'host' => $this->variables['DB_HOST'],
|
||||
'port' => $this->variables['DB_PORT'],
|
||||
'database' => $this->variables['DB_DATABASE'],
|
||||
'username' => $this->variables['DB_USERNAME'],
|
||||
'password' => $this->variables['DB_PASSWORD'],
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'strict' => true,
|
||||
]);
|
||||
|
||||
$this->database->connection('_panel_command_test')->getPdo();
|
||||
} catch (\PDOException $exception) {
|
||||
$this->output->error(sprintf('Unable to connect to the MySQL server using the provided credentials. The error returned was "%s".', $exception->getMessage()));
|
||||
$this->output->error(__('commands.database_settings.DB_error_2'));
|
||||
@@ -93,12 +107,72 @@ class DatabaseSettingsCommand extends Command
|
||||
return $this->handle();
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
} elseif ($this->variables['DB_CONNECTION'] === 'mariadb') {
|
||||
$this->output->note(__('commands.database_settings.DB_HOST_note'));
|
||||
$this->variables['DB_HOST'] = $this->option('host') ?? $this->ask(
|
||||
'Database Host',
|
||||
config('database.connections.mariadb.host', '127.0.0.1')
|
||||
);
|
||||
|
||||
$this->variables['DB_PORT'] = $this->option('port') ?? $this->ask(
|
||||
'Database Port',
|
||||
config('database.connections.mariadb.port', 3306)
|
||||
);
|
||||
|
||||
$this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask(
|
||||
'Database Name',
|
||||
config('database.connections.mariadb.database', 'panel')
|
||||
);
|
||||
|
||||
$this->output->note(__('commands.database_settings.DB_USERNAME_note'));
|
||||
$this->variables['DB_USERNAME'] = $this->option('username') ?? $this->ask(
|
||||
'Database Username',
|
||||
config('database.connections.mariadb.username', 'pelican')
|
||||
);
|
||||
|
||||
$askForMariaDBPassword = true;
|
||||
if (!empty(config('database.connections.mariadb.password')) && $this->input->isInteractive()) {
|
||||
$this->variables['DB_PASSWORD'] = config('database.connections.mariadb.password');
|
||||
$askForMariaDBPassword = $this->confirm(__('commands.database_settings.DB_PASSWORD_note'));
|
||||
}
|
||||
|
||||
if ($askForMariaDBPassword) {
|
||||
$this->variables['DB_PASSWORD'] = $this->option('password') ?? $this->secret('Database Password');
|
||||
}
|
||||
|
||||
try {
|
||||
// Test connection
|
||||
config()->set('database.connections._panel_command_test', [
|
||||
'driver' => 'mariadb',
|
||||
'host' => $this->variables['DB_HOST'],
|
||||
'port' => $this->variables['DB_PORT'],
|
||||
'database' => $this->variables['DB_DATABASE'],
|
||||
'username' => $this->variables['DB_USERNAME'],
|
||||
'password' => $this->variables['DB_PASSWORD'],
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'strict' => true,
|
||||
]);
|
||||
|
||||
$this->database->connection('_panel_command_test')->getPdo();
|
||||
} catch (\PDOException $exception) {
|
||||
$this->output->error(sprintf('Unable to connect to the MariaDB server using the provided credentials. The error returned was "%s".', $exception->getMessage()));
|
||||
$this->output->error(__('commands.database_settings.DB_error_2'));
|
||||
|
||||
if ($this->confirm(__('commands.database_settings.go_back'))) {
|
||||
$this->database->disconnect('_panel_command_test');
|
||||
|
||||
return $this->handle();
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
} 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')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,24 +182,4 @@ class DatabaseSettingsCommand extends Command
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that we can connect to the provided MySQL instance and perform a selection.
|
||||
*/
|
||||
private function testMySQLConnection()
|
||||
{
|
||||
config()->set('database.connections._panel_command_test', [
|
||||
'driver' => 'mysql',
|
||||
'host' => $this->variables['DB_HOST'],
|
||||
'port' => $this->variables['DB_PORT'],
|
||||
'database' => $this->variables['DB_DATABASE'],
|
||||
'username' => $this->variables['DB_USERNAME'],
|
||||
'password' => $this->variables['DB_PASSWORD'],
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'strict' => true,
|
||||
]);
|
||||
|
||||
$this->database->connection('_panel_command_test')->getPdo();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<?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 service 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') ? '
|
||||
After=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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,12 @@ use App\Services\Helpers\SoftwareVersionService;
|
||||
|
||||
class InfoCommand extends Command
|
||||
{
|
||||
protected $description = 'Displays the application, database, and email configurations along with the panel version.';
|
||||
protected $description = 'Displays the application, database, email and backup configurations along with the panel version.';
|
||||
|
||||
protected $signature = 'p:info';
|
||||
|
||||
/**
|
||||
* VersionCommand constructor.
|
||||
* InfoCommand constructor.
|
||||
*/
|
||||
public function __construct(private SoftwareVersionService $versionService)
|
||||
{
|
||||
@@ -26,45 +26,76 @@ 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');
|
||||
|
||||
$this->output->title('Application Configuration');
|
||||
$this->table([], [
|
||||
['Environment', $this->formatText(config('app.env'), config('app.env') === 'production' ?: 'bg=red')],
|
||||
['Debug Mode', $this->formatText(config('app.debug') ? 'Yes' : 'No', !config('app.debug') ?: 'bg=red')],
|
||||
['Installation URL', config('app.url')],
|
||||
['Environment', config('app.env') === 'production' ? config('app.env') : $this->formatText(config('app.env'), 'bg=red')],
|
||||
['Debug Mode', config('app.debug') ? $this->formatText('Yes', 'bg=red') : 'No'],
|
||||
['Application Name', config('app.name')],
|
||||
['Application URL', config('app.url')],
|
||||
['Installation Directory', base_path()],
|
||||
['Cache Driver', config('cache.default')],
|
||||
['Queue Driver', config('queue.default')],
|
||||
['Queue Driver', config('queue.default') === 'sync' ? $this->formatText(config('queue.default'), 'bg=red') : config('queue.default')],
|
||||
['Session Driver', config('session.driver')],
|
||||
['Filesystem Driver', config('filesystems.default')],
|
||||
['Default Theme', config('themes.active')],
|
||||
], 'compact');
|
||||
|
||||
$this->output->title('Database Configuration');
|
||||
$driver = config('database.default');
|
||||
$this->table([], [
|
||||
['Driver', $driver],
|
||||
['Host', config("database.connections.$driver.host")],
|
||||
['Port', config("database.connections.$driver.port")],
|
||||
['Database', config("database.connections.$driver.database")],
|
||||
['Username', config("database.connections.$driver.username")],
|
||||
], 'compact');
|
||||
if ($driver === 'sqlite') {
|
||||
$this->table([], [
|
||||
['Driver', $driver],
|
||||
['Database', config("database.connections.$driver.database")],
|
||||
], 'compact');
|
||||
} else {
|
||||
$this->table([], [
|
||||
['Driver', $driver],
|
||||
['Host', config("database.connections.$driver.host")],
|
||||
['Port', config("database.connections.$driver.port")],
|
||||
['Database', config("database.connections.$driver.database")],
|
||||
['Username', config("database.connections.$driver.username")],
|
||||
], 'compact');
|
||||
}
|
||||
|
||||
// TODO: Update this to handle other mail drivers
|
||||
$this->output->title('Email Configuration');
|
||||
$this->table([], [
|
||||
['Driver', config('mail.default')],
|
||||
['Host', config('mail.mailers.smtp.host')],
|
||||
['Port', config('mail.mailers.smtp.port')],
|
||||
['Username', config('mail.mailers.smtp.username')],
|
||||
['From Address', config('mail.from.address')],
|
||||
['From Name', config('mail.from.name')],
|
||||
['Encryption', config('mail.mailers.smtp.encryption')],
|
||||
], 'compact');
|
||||
$driver = config('mail.default');
|
||||
if ($driver === 'smtp') {
|
||||
$this->table([], [
|
||||
['Driver', $driver],
|
||||
['Host', config("mail.mailers.$driver.host")],
|
||||
['Port', config("mail.mailers.$driver.port")],
|
||||
['Username', config("mail.mailers.$driver.username")],
|
||||
['Encryption', config("mail.mailers.$driver.encryption")],
|
||||
['From Address', config('mail.from.address')],
|
||||
['From Name', config('mail.from.name')],
|
||||
], 'compact');
|
||||
} else {
|
||||
$this->table([], [
|
||||
['Driver', $driver],
|
||||
['From Address', config('mail.from.address')],
|
||||
['From Name', config('mail.from.name')],
|
||||
], 'compact');
|
||||
}
|
||||
|
||||
$this->output->title('Backup Configuration');
|
||||
$driver = config('backups.default');
|
||||
if ($driver === 's3') {
|
||||
$this->table([], [
|
||||
['Driver', $driver],
|
||||
['Region', config("backups.disks.$driver.region")],
|
||||
['Bucket', config("backups.disks.$driver.bucket")],
|
||||
['Endpoint', config("backups.disks.$driver.endpoint")],
|
||||
['Use path style endpoint', config("backups.disks.$driver.use_path_style_endpoint") ? 'Yes' : 'No'],
|
||||
], 'compact');
|
||||
} else {
|
||||
$this->table([], [
|
||||
['Driver', $driver],
|
||||
], 'compact');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,9 +20,12 @@ class MakeNodeCommand extends Command
|
||||
{--overallocateMemory= : Enter the amount of ram to overallocate (% or -1 to overallocate the maximum).}
|
||||
{--maxDisk= : Set the max disk amount.}
|
||||
{--overallocateDisk= : Enter the amount of disk to overallocate (% or -1 to overallocate the maximum).}
|
||||
{--maxCpu= : Set the max cpu amount.}
|
||||
{--overallocateCpu= : Enter the amount of cpu to overallocate (% or -1 to overallocate the maximum).}
|
||||
{--uploadSize= : Enter the maximum upload filesize.}
|
||||
{--daemonListeningPort= : Enter the daemon listening port.}
|
||||
{--daemonSFTPPort= : Enter the daemon SFTP listening port.}
|
||||
{--daemonSFTPAlias= : Enter the daemon SFTP alias.}
|
||||
{--daemonBase= : Enter the base folder.}';
|
||||
|
||||
protected $description = 'Creates a new node on the system via the CLI.';
|
||||
@@ -54,16 +57,19 @@ class MakeNodeCommand extends Command
|
||||
$data['public'] = $this->option('public') ?? $this->confirm(__('commands.make_node.public'), true);
|
||||
$data['behind_proxy'] = $this->option('proxy') ?? $this->confirm(__('commands.make_node.behind_proxy'));
|
||||
$data['maintenance_mode'] = $this->option('maintenance') ?? $this->confirm(__('commands.make_node.maintenance_mode'));
|
||||
$data['memory'] = $this->option('maxMemory') ?? $this->ask(__('commands.make_node.memory'));
|
||||
$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['upload_size'] = $this->option('uploadSize') ?? $this->ask(__('commands.make_node.upload_size'), '100');
|
||||
$data['memory'] = $this->option('maxMemory') ?? $this->ask(__('commands.make_node.memory'), '0');
|
||||
$data['memory_overallocate'] = $this->option('overallocateMemory') ?? $this->ask(__('commands.make_node.memory_overallocate'), '-1');
|
||||
$data['disk'] = $this->option('maxDisk') ?? $this->ask(__('commands.make_node.disk'), '0');
|
||||
$data['disk_overallocate'] = $this->option('overallocateDisk') ?? $this->ask(__('commands.make_node.disk_overallocate'), '-1');
|
||||
$data['cpu'] = $this->option('maxCpu') ?? $this->ask(__('commands.make_node.cpu'), '0');
|
||||
$data['cpu_overallocate'] = $this->option('overallocateCpu') ?? $this->ask(__('commands.make_node.cpu_overallocate'), '-1');
|
||||
$data['upload_size'] = $this->option('uploadSize') ?? $this->ask(__('commands.make_node.upload_size'), '256');
|
||||
$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);
|
||||
$this->line(__('commands.make_node.succes1') . $data['name'] . __('commands.make_node.succes2') . $node->id . '.');
|
||||
$this->line(__('commands.make_node.success', ['name' => $data['name'], 'id' => $node->id]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
->where('next_run_at', '<=', Carbon::now()->toDateTimeString())
|
||||
->get();
|
||||
|
||||
if ($schedules->count() < 1) {
|
||||
@@ -62,7 +62,7 @@ class ProcessRunnableCommand extends Command
|
||||
|
||||
$this->line(trans('command/messages.schedule.output_line', [
|
||||
'schedule' => $schedule->name,
|
||||
'hash' => $schedule->hashid,
|
||||
'id' => $schedule->id,
|
||||
]));
|
||||
} catch (\Throwable|\Exception $exception) {
|
||||
logger()->error($exception, ['schedule_id' => $schedule->id]);
|
||||
|
||||
@@ -30,7 +30,7 @@ class MakeUserCommand extends Command
|
||||
public function handle(): int
|
||||
{
|
||||
try {
|
||||
DB::select('select 1 where 1');
|
||||
DB::connection()->getPdo();
|
||||
} catch (Exception $exception) {
|
||||
$this->error($exception->getMessage());
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Contracts\Extensions;
|
||||
|
||||
use Hashids\HashidsInterface as VendorHashidsInterface;
|
||||
|
||||
interface HashidsInterface extends VendorHashidsInterface
|
||||
{
|
||||
/**
|
||||
* Decode an encoded hashid and return the first result.
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function decodeFirst(string $encoded, string $default = null): mixed;
|
||||
}
|
||||
@@ -12,6 +12,7 @@ enum ContainerStatus: string
|
||||
case Paused = 'paused';
|
||||
case Dead = 'dead';
|
||||
case Removing = 'removing';
|
||||
case Offline = 'offline';
|
||||
|
||||
// HTTP Based
|
||||
case Missing = 'missing';
|
||||
@@ -27,6 +28,7 @@ enum ContainerStatus: string
|
||||
self::Dead => 'tabler-heart-x',
|
||||
self::Removing => 'tabler-heart-down',
|
||||
self::Missing => 'tabler-heart-question',
|
||||
self::Offline => 'tabler-heart-bolt',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -41,6 +43,7 @@ enum ContainerStatus: string
|
||||
self::Dead => 'danger',
|
||||
self::Removing => 'warning',
|
||||
self::Missing => 'danger',
|
||||
self::Offline => 'gray',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -215,7 +215,7 @@ class Handler extends ExceptionHandler
|
||||
->map(fn ($trace) => Arr::except($trace, ['args']))
|
||||
->all(),
|
||||
'previous' => Collection::make($this->extractPrevious($e))
|
||||
->map(fn ($exception) => $e->getTrace())
|
||||
->map(fn ($exception) => $exception->getTrace())
|
||||
->map(fn ($trace) => Arr::except($trace, ['args']))
|
||||
->all(),
|
||||
],
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Deployment;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class NoViableNodeException extends DisplayException
|
||||
{
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Helper;
|
||||
|
||||
class CdnVersionFetchingException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -6,9 +6,9 @@ use App\Exceptions\DisplayException;
|
||||
|
||||
class TwoFactorAuthenticationTokenInvalid extends DisplayException
|
||||
{
|
||||
/**
|
||||
* TwoFactorAuthenticationTokenInvalid constructor.
|
||||
*/
|
||||
public string $title = 'Invalid 2FA Code';
|
||||
public string $icon = 'tabler-2fa';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('The provided two-factor authentication token was not valid.');
|
||||
|
||||
@@ -25,7 +25,7 @@ class DynamicDatabaseConnection
|
||||
'port' => $host->port,
|
||||
'database' => $database,
|
||||
'username' => $host->username,
|
||||
'password' => decrypt($host->password),
|
||||
'password' => $host->password,
|
||||
'charset' => self::DB_CHARSET,
|
||||
'collation' => self::DB_COLLATION,
|
||||
]);
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions;
|
||||
|
||||
use Hashids\Hashids as VendorHashids;
|
||||
use App\Contracts\Extensions\HashidsInterface;
|
||||
|
||||
class Hashids extends VendorHashids implements HashidsInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function decodeFirst(string $encoded, string $default = null): mixed
|
||||
{
|
||||
$result = $this->decode($encoded);
|
||||
if (!is_array($result)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return array_first($result, null, $default);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use App\Models\Egg;
|
||||
use App\Models\Node;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use App\Services\Helpers\SoftwareVersionService;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Pages\Page;
|
||||
|
||||
@@ -29,8 +30,14 @@ class Dashboard extends Page
|
||||
|
||||
public function getViewData(): array
|
||||
{
|
||||
/** @var SoftwareVersionService $softwareVersionService */
|
||||
$softwareVersionService = app(SoftwareVersionService::class);
|
||||
|
||||
return [
|
||||
'inDevelopment' => config('app.version') === 'canary',
|
||||
'version' => $softwareVersionService->versionData()['version'],
|
||||
'latestVersion' => $softwareVersionService->getPanel(),
|
||||
'isLatest' => $softwareVersionService->isLatestPanel(),
|
||||
'eggsCount' => Egg::query()->count(),
|
||||
'nodesList' => ListNodes::getUrl(),
|
||||
'nodesCount' => Node::query()->count(),
|
||||
@@ -39,15 +46,17 @@ class Dashboard extends Page
|
||||
|
||||
'devActions' => [
|
||||
CreateAction::make()
|
||||
->label(trans('dashboard/index.sections.intro-developers.button_issues'))
|
||||
->icon('tabler-brand-github')
|
||||
->url('https://github.com/pelican-dev/panel/issues/new/choose', true)
|
||||
->color('warning'),
|
||||
CreateAction::make()
|
||||
->label(trans('dashboard/index.sections.intro-developers.button_features'))
|
||||
->label('Bugs & Features')
|
||||
->icon('tabler-brand-github')
|
||||
->url('https://github.com/pelican-dev/panel/discussions', true),
|
||||
],
|
||||
'updateActions' => [
|
||||
CreateAction::make()
|
||||
->label('Read Documentation')
|
||||
->icon('tabler-clipboard-text')
|
||||
->url('https://pelican.dev/docs/panel/update', true)
|
||||
->color('warning'),
|
||||
],
|
||||
'nodeActions' => [
|
||||
CreateAction::make()
|
||||
->label(trans('dashboard/index.sections.intro-first-node.button_label'))
|
||||
@@ -55,14 +64,10 @@ class Dashboard extends Page
|
||||
->url(route('filament.admin.resources.nodes.create')),
|
||||
],
|
||||
'supportActions' => [
|
||||
CreateAction::make()
|
||||
->label(trans('dashboard/index.sections.intro-support.button_translate'))
|
||||
->icon('tabler-language')
|
||||
->url('https://crowdin.com/project/pelican-dev', true),
|
||||
CreateAction::make()
|
||||
->label(trans('dashboard/index.sections.intro-support.button_donate'))
|
||||
->icon('tabler-cash')
|
||||
->url('https://pelican.dev/donate', true)
|
||||
->url($softwareVersionService->getDonations(), true)
|
||||
->color('success'),
|
||||
],
|
||||
'helpActions' => [
|
||||
@@ -70,11 +75,6 @@ class Dashboard extends Page
|
||||
->label(trans('dashboard/index.sections.intro-help.button_docs'))
|
||||
->icon('tabler-speedboat')
|
||||
->url('https://pelican.dev/docs', true),
|
||||
CreateAction::make()
|
||||
->label(trans('dashboard/index.sections.intro-help.button_discord'))
|
||||
->icon('tabler-brand-discord')
|
||||
->url('https://discord.gg/pelican-panel', true)
|
||||
->color('blurple'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,19 +4,18 @@ namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\ApiKeyResource\Pages;
|
||||
use App\Models\ApiKey;
|
||||
use Filament\Resources\Components\Tab;
|
||||
use Filament\Resources\Resource;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ApiKeyResource extends Resource
|
||||
{
|
||||
protected static ?string $model = ApiKey::class;
|
||||
protected static ?string $label = 'API Key';
|
||||
protected static ?string $navigationIcon = 'tabler-key';
|
||||
protected static ?string $navigationGroup = 'Advanced';
|
||||
|
||||
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 +23,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 [
|
||||
|
||||
@@ -4,9 +4,13 @@ namespace App\Filament\Resources\ApiKeyResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ApiKeyResource;
|
||||
use App\Models\ApiKey;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Filament\Forms;
|
||||
|
||||
class CreateApiKey extends CreateRecord
|
||||
{
|
||||
@@ -18,40 +22,26 @@ 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))),
|
||||
Hidden::make('identifier')->default(ApiKey::generateTokenIdentifier(ApiKey::TYPE_APPLICATION)),
|
||||
Hidden::make('token')->default(str_random(ApiKey::KEY_LENGTH)),
|
||||
|
||||
Forms\Components\Hidden::make('user_id')
|
||||
Hidden::make('user_id')
|
||||
->default(auth()->user()->id)
|
||||
->required(),
|
||||
|
||||
Forms\Components\Select::make('key_type')
|
||||
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',
|
||||
];
|
||||
->default(ApiKey::TYPE_APPLICATION)
|
||||
->required(),
|
||||
|
||||
return collect($originalOptions)
|
||||
->filter(fn ($value, $key) => $key <= ApiKey::TYPE_APPLICATION || $apiKey->key_type === $key)
|
||||
->all();
|
||||
})
|
||||
->selectablePlaceholder(false)
|
||||
->required()
|
||||
->default(ApiKey::TYPE_APPLICATION),
|
||||
|
||||
Forms\Components\Fieldset::make('Permissions')
|
||||
Fieldset::make('Permissions')
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 2,
|
||||
])
|
||||
->schema(
|
||||
collect(ApiKey::RESOURCES)->map(fn ($resource) => Forms\Components\ToggleButtons::make("r_$resource")
|
||||
collect(ApiKey::RESOURCES)->map(fn ($resource) => ToggleButtons::make("r_$resource")
|
||||
->label(str($resource)->replace('_', ' ')->title())->inline()
|
||||
->options([
|
||||
0 => 'None',
|
||||
@@ -81,15 +71,13 @@ class CreateApiKey extends CreateRecord
|
||||
)->all(),
|
||||
),
|
||||
|
||||
Forms\Components\TagsInput::make('allowed_ips')
|
||||
TagsInput::make('allowed_ips')
|
||||
->placeholder('Example: 127.0.0.1 or 192.168.1.1')
|
||||
->label('Whitelisted IPv4 Addresses')
|
||||
->helperText('Press enter to add a new IP address or leave blank to allow any IP address')
|
||||
->columnSpanFull()
|
||||
->hidden()
|
||||
->default(null),
|
||||
->columnSpanFull(),
|
||||
|
||||
Forms\Components\Textarea::make('memo')
|
||||
Textarea::make('memo')
|
||||
->required()
|
||||
->label('Description')
|
||||
->helperText('
|
||||
|
||||
@@ -5,11 +5,10 @@ namespace App\Filament\Resources\ApiKeyResource\Pages;
|
||||
use App\Filament\Resources\ApiKeyResource;
|
||||
use App\Models\ApiKey;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Components\Tab;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Filament\Tables;
|
||||
|
||||
class ListApiKeys extends ListRecords
|
||||
{
|
||||
@@ -19,42 +18,39 @@ 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')
|
||||
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')
|
||||
TextColumn::make('memo')
|
||||
->label('Description')
|
||||
->wrap()
|
||||
->limit(50),
|
||||
|
||||
Tables\Columns\TextColumn::make('identifier')
|
||||
TextColumn::make('identifier')
|
||||
->hidden()
|
||||
->searchable(),
|
||||
|
||||
Tables\Columns\TextColumn::make('last_used_at')
|
||||
TextColumn::make('last_used_at')
|
||||
->label('Last Used')
|
||||
->placeholder('Not Used')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
TextColumn::make('created_at')
|
||||
->label('Created')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
|
||||
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()
|
||||
DeleteAction::make(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -64,22 +60,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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ class DatabaseHostResource extends Resource
|
||||
protected static ?string $label = 'Databases';
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-database';
|
||||
protected static ?string $navigationGroup = 'Advanced';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
|
||||
@@ -3,10 +3,16 @@
|
||||
namespace App\Filament\Resources\DatabaseHostResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DatabaseHostResource;
|
||||
use App\Services\Databases\Hosts\HostCreationService;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use PDOException;
|
||||
|
||||
class CreateDatabaseHost extends CreateRecord
|
||||
{
|
||||
@@ -30,14 +36,14 @@ class CreateDatabaseHost extends CreateRecord
|
||||
'lg' => 4,
|
||||
])
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('host')
|
||||
TextInput::make('host')
|
||||
->columnSpan(2)
|
||||
->helperText('The IP address or Domain name that should be used when attempting to connect to this MySQL host from this Panel to create new databases.')
|
||||
->required()
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state))
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('port')
|
||||
->maxLength(255),
|
||||
TextInput::make('port')
|
||||
->columnSpan(1)
|
||||
->helperText('The port that MySQL is running on for this host.')
|
||||
->required()
|
||||
@@ -45,26 +51,26 @@ class CreateDatabaseHost extends CreateRecord
|
||||
->default(3306)
|
||||
->minValue(0)
|
||||
->maxValue(65535),
|
||||
Forms\Components\TextInput::make('max_databases')
|
||||
TextInput::make('max_databases')
|
||||
->label('Max databases')
|
||||
->helpertext('Blank is unlimited.')
|
||||
->numeric(),
|
||||
Forms\Components\TextInput::make('name')
|
||||
TextInput::make('name')
|
||||
->label('Display Name')
|
||||
->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.')
|
||||
->required()
|
||||
->maxLength(60),
|
||||
Forms\Components\TextInput::make('username')
|
||||
TextInput::make('username')
|
||||
->helperText('The username of an account that has enough permissions to create new users and databases on the system.')
|
||||
->required()
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('password')
|
||||
->maxLength(255),
|
||||
TextInput::make('password')
|
||||
->helperText('The password for the database user.')
|
||||
->password()
|
||||
->revealable()
|
||||
->maxLength(191)
|
||||
->maxLength(255)
|
||||
->required(),
|
||||
Forms\Components\Select::make('node_id')
|
||||
Select::make('node_id')
|
||||
->searchable()
|
||||
->preload()
|
||||
->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.')
|
||||
@@ -74,25 +80,35 @@ 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 [
|
||||
$this->getCreateFormAction()->formId('form'),
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function handleRecordCreation(array $data): Model
|
||||
{
|
||||
return resolve(HostCreationService::class)->handle($data);
|
||||
}
|
||||
|
||||
public function exception($e, $stopPropagation): void
|
||||
{
|
||||
if ($e instanceof PDOException) {
|
||||
Notification::make()
|
||||
->title('Error connecting to database host')
|
||||
->body($e->getMessage())
|
||||
->color('danger')
|
||||
->icon('tabler-database')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
$stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,19 @@
|
||||
namespace App\Filament\Resources\DatabaseHostResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DatabaseHostResource;
|
||||
use App\Filament\Resources\DatabaseHostResource\RelationManagers\DatabasesRelationManager;
|
||||
use App\Models\DatabaseHost;
|
||||
use App\Services\Databases\Hosts\HostUpdateService;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use PDOException;
|
||||
|
||||
class EditDatabaseHost extends EditRecord
|
||||
{
|
||||
@@ -25,40 +33,40 @@ class EditDatabaseHost extends EditRecord
|
||||
'lg' => 4,
|
||||
])
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('host')
|
||||
TextInput::make('host')
|
||||
->columnSpan(2)
|
||||
->helperText('The IP address or Domain name that should be used when attempting to connect to this MySQL host from this Panel to create new databases.')
|
||||
->required()
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state))
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('port')
|
||||
->maxLength(255),
|
||||
TextInput::make('port')
|
||||
->columnSpan(1)
|
||||
->helperText('The port that MySQL is running on for this host.')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->maxValue(65535),
|
||||
Forms\Components\TextInput::make('max_databases')
|
||||
TextInput::make('max_databases')
|
||||
->label('Max databases')
|
||||
->helpertext('Blank is unlimited.')
|
||||
->numeric(),
|
||||
Forms\Components\TextInput::make('name')
|
||||
TextInput::make('name')
|
||||
->label('Display Name')
|
||||
->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.')
|
||||
->required()
|
||||
->maxLength(60),
|
||||
Forms\Components\TextInput::make('username')
|
||||
TextInput::make('username')
|
||||
->helperText('The username of an account that has enough permissions to create new users and databases on the system.')
|
||||
->required()
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('password')
|
||||
->maxLength(255),
|
||||
TextInput::make('password')
|
||||
->helperText('The password for the database user.')
|
||||
->password()
|
||||
->revealable()
|
||||
->maxLength(191)
|
||||
->maxLength(255)
|
||||
->required(),
|
||||
Forms\Components\Select::make('node_id')
|
||||
Select::make('node_id')
|
||||
->searchable()
|
||||
->preload()
|
||||
->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.')
|
||||
@@ -71,20 +79,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 [];
|
||||
@@ -93,7 +94,27 @@ class EditDatabaseHost extends EditRecord
|
||||
public function getRelationManagers(): array
|
||||
{
|
||||
return [
|
||||
DatabaseHostResource\RelationManagers\DatabasesRelationManager::class,
|
||||
DatabasesRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
protected function handleRecordUpdate($record, array $data): Model
|
||||
{
|
||||
return resolve(HostUpdateService::class)->handle($record->id, $data);
|
||||
}
|
||||
|
||||
public function exception($e, $stopPropagation): void
|
||||
{
|
||||
if ($e instanceof PDOException) {
|
||||
Notification::make()
|
||||
->title('Error connecting to database host')
|
||||
->body($e->getMessage())
|
||||
->color('danger')
|
||||
->icon('tabler-database')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
$stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@ namespace App\Filament\Resources\DatabaseHostResource\Pages;
|
||||
use App\Filament\Resources\DatabaseHostResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ListDatabaseHosts extends ListRecords
|
||||
@@ -19,30 +22,27 @@ class ListDatabaseHosts extends ListRecords
|
||||
return $table
|
||||
->searchable(false)
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
TextColumn::make('name')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('host')
|
||||
TextColumn::make('host')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('port')
|
||||
TextColumn::make('port')
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('username')
|
||||
TextColumn::make('username')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('max_databases')
|
||||
TextColumn::make('max_databases')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('node.name')
|
||||
TextColumn::make('node.name')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -4,39 +4,40 @@ namespace App\Filament\Resources\DatabaseHostResource\RelationManagers;
|
||||
|
||||
use App\Models\Database;
|
||||
use App\Services\Databases\DatabasePasswordService;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class DatabasesRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'databases';
|
||||
|
||||
protected $listeners = ['refresh' => 'refreshForm'];
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('database')->columnSpanFull(),
|
||||
Forms\Components\TextInput::make('username'),
|
||||
Forms\Components\TextInput::make('password')
|
||||
TextInput::make('database')->columnSpanFull(),
|
||||
TextInput::make('username'),
|
||||
TextInput::make('password')
|
||||
->hintAction(
|
||||
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)),
|
||||
Forms\Components\TextInput::make('remote')->label('Connections From'),
|
||||
Forms\Components\TextInput::make('max_connections'),
|
||||
Forms\Components\TextInput::make('JDBC')
|
||||
->formatStateUsing(fn (Database $database) => $database->password),
|
||||
TextInput::make('remote')->label('Connections From'),
|
||||
TextInput::make('max_connections'),
|
||||
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 (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
|
||||
@@ -44,20 +45,27 @@ class DatabasesRelationManager extends RelationManager
|
||||
return $table
|
||||
->recordTitleAttribute('servers')
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('database')->icon('tabler-database'),
|
||||
Tables\Columns\TextColumn::make('username')->icon('tabler-user'),
|
||||
//Tables\Columns\TextColumn::make('password'),
|
||||
Tables\Columns\TextColumn::make('remote'),
|
||||
Tables\Columns\TextColumn::make('server.name')
|
||||
TextColumn::make('database')->icon('tabler-database'),
|
||||
TextColumn::make('username')->icon('tabler-user'),
|
||||
TextColumn::make('remote'),
|
||||
TextColumn::make('server.name')
|
||||
->icon('tabler-brand-docker')
|
||||
->url(fn (Database $database) => route('filament.admin.resources.servers.edit', ['record' => $database->server_id])),
|
||||
Tables\Columns\TextColumn::make('max_connections'),
|
||||
Tables\Columns\TextColumn::make('created_at')->dateTime(),
|
||||
TextColumn::make('max_connections'),
|
||||
TextColumn::make('created_at')->dateTime(),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\DeleteAction::make(),
|
||||
Tables\Actions\ViewAction::make()->color('primary'),
|
||||
//Tables\Actions\EditAction::make(),
|
||||
DeleteAction::make(),
|
||||
ViewAction::make()->color('primary'),
|
||||
]);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ class DatabaseResource extends Resource
|
||||
protected static ?string $navigationIcon = 'tabler-database';
|
||||
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
protected static ?string $navigationGroup = 'Advanced';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
namespace App\Filament\Resources\DatabaseResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DatabaseResource;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Filament\Forms;
|
||||
|
||||
class CreateDatabase extends CreateRecord
|
||||
{
|
||||
@@ -15,29 +16,29 @@ class CreateDatabase extends CreateRecord
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\Select::make('server_id')
|
||||
Select::make('server_id')
|
||||
->relationship('server', 'name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('database_host_id')
|
||||
TextInput::make('database_host_id')
|
||||
->required()
|
||||
->numeric(),
|
||||
Forms\Components\TextInput::make('database')
|
||||
TextInput::make('database')
|
||||
->required()
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('remote')
|
||||
->maxLength(255),
|
||||
TextInput::make('remote')
|
||||
->required()
|
||||
->maxLength(191)
|
||||
->maxLength(255)
|
||||
->default('%'),
|
||||
Forms\Components\TextInput::make('username')
|
||||
TextInput::make('username')
|
||||
->required()
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('password')
|
||||
->maxLength(255),
|
||||
TextInput::make('password')
|
||||
->password()
|
||||
->revealable()
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('max_connections')
|
||||
TextInput::make('max_connections')
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->default(0),
|
||||
|
||||
@@ -4,9 +4,10 @@ namespace App\Filament\Resources\DatabaseResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DatabaseResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Forms;
|
||||
|
||||
class EditDatabase extends EditRecord
|
||||
{
|
||||
@@ -16,29 +17,29 @@ class EditDatabase extends EditRecord
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\Select::make('server_id')
|
||||
Select::make('server_id')
|
||||
->relationship('server', 'name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('database_host_id')
|
||||
TextInput::make('database_host_id')
|
||||
->required()
|
||||
->numeric(),
|
||||
Forms\Components\TextInput::make('database')
|
||||
TextInput::make('database')
|
||||
->required()
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('remote')
|
||||
->maxLength(255),
|
||||
TextInput::make('remote')
|
||||
->required()
|
||||
->maxLength(191)
|
||||
->maxLength(255)
|
||||
->default('%'),
|
||||
Forms\Components\TextInput::make('username')
|
||||
TextInput::make('username')
|
||||
->required()
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('password')
|
||||
->maxLength(255),
|
||||
TextInput::make('password')
|
||||
->password()
|
||||
->revealable()
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('max_connections')
|
||||
TextInput::make('max_connections')
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->default(0),
|
||||
|
||||
@@ -4,9 +4,12 @@ namespace App\Filament\Resources\DatabaseResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DatabaseResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Tables;
|
||||
|
||||
class ListDatabases extends ListRecords
|
||||
{
|
||||
@@ -16,39 +19,36 @@ class ListDatabases extends ListRecords
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('server.name')
|
||||
TextColumn::make('server.name')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('database_host_id')
|
||||
TextColumn::make('database_host_id')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('database')
|
||||
TextColumn::make('database')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('username')
|
||||
TextColumn::make('username')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('remote')
|
||||
TextColumn::make('remote')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('max_connections')
|
||||
TextColumn::make('max_connections')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
Tables\Columns\TextColumn::make('updated_at')
|
||||
TextColumn::make('updated_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
namespace App\Filament\Resources\EggResource\Pages;
|
||||
|
||||
use App\Filament\Resources\EggResource;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\KeyValue;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\Tabs\Tab;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
|
||||
use Filament\Forms;
|
||||
@@ -19,26 +31,26 @@ class CreateEgg extends CreateRecord
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\Tabs::make()->tabs([
|
||||
Forms\Components\Tabs\Tab::make('Configuration')
|
||||
Tabs::make()->tabs([
|
||||
Tab::make('Configuration')
|
||||
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(191)
|
||||
->maxLength(255)
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->helperText('A simple, human-readable name to use as an identifier for this Egg.'),
|
||||
Forms\Components\TextInput::make('author')
|
||||
->maxLength(191)
|
||||
TextInput::make('author')
|
||||
->maxLength(255)
|
||||
->required()
|
||||
->email()
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->helperText('The author of this version of the Egg.'),
|
||||
Forms\Components\Textarea::make('description')
|
||||
Textarea::make('description')
|
||||
->rows(3)
|
||||
->columnSpanFull()
|
||||
->helperText('A description of this Egg that will be displayed throughout the Panel as needed.'),
|
||||
Forms\Components\Textarea::make('startup')
|
||||
Textarea::make('startup')
|
||||
->rows(3)
|
||||
->columnSpanFull()
|
||||
->required()
|
||||
@@ -46,26 +58,26 @@ class CreateEgg extends CreateRecord
|
||||
'java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}',
|
||||
]))
|
||||
->helperText('The default startup command that should be used for new servers using this Egg.'),
|
||||
Forms\Components\TagsInput::make('features')
|
||||
TagsInput::make('features')
|
||||
->placeholder('Add Feature')
|
||||
->helperText('')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
Forms\Components\Toggle::make('force_outgoing_ip')
|
||||
Toggle::make('force_outgoing_ip')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's primary allocation IP.
|
||||
Required for certain games to work properly when the Node has multiple public IP addresses.
|
||||
Enabling this option will disable internal networking for any servers using this egg, causing them to be unable to internally access other servers on the same node."),
|
||||
Forms\Components\Hidden::make('script_is_privileged')
|
||||
Hidden::make('script_is_privileged')
|
||||
->default(1),
|
||||
Forms\Components\TagsInput::make('tags')
|
||||
TagsInput::make('tags')
|
||||
->placeholder('Add Tags')
|
||||
->helperText('')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
Forms\Components\TextInput::make('update_url')
|
||||
TextInput::make('update_url')
|
||||
->disabled()
|
||||
->helperText('Not implemented.')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
Forms\Components\KeyValue::make('docker_images')
|
||||
KeyValue::make('docker_images')
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->required()
|
||||
@@ -77,37 +89,37 @@ class CreateEgg extends CreateRecord
|
||||
->helperText('The docker images available to servers using this egg.'),
|
||||
]),
|
||||
|
||||
Forms\Components\Tabs\Tab::make('Process Management')
|
||||
Tab::make('Process Management')
|
||||
->columns()
|
||||
->schema([
|
||||
Forms\Components\Hidden::make('config_from')
|
||||
Hidden::make('config_from')
|
||||
->default(null)
|
||||
->label('Copy Settings From')
|
||||
// ->placeholder('None')
|
||||
// ->relationship('configFrom', 'name', ignoreRecord: true)
|
||||
->helperText('If you would like to default to settings from another Egg select it from the menu above.'),
|
||||
Forms\Components\TextInput::make('config_stop')
|
||||
TextInput::make('config_stop')
|
||||
->required()
|
||||
->maxLength(191)
|
||||
->maxLength(255)
|
||||
->label('Stop Command')
|
||||
->helperText('The command that should be sent to server processes to stop them gracefully. If you need to send a SIGINT you should enter ^C here.'),
|
||||
Forms\Components\Textarea::make('config_startup')->rows(10)->json()
|
||||
Textarea::make('config_startup')->rows(10)->json()
|
||||
->label('Start Configuration')
|
||||
->default('{}')
|
||||
->helperText('List of values the daemon should be looking for when booting a server to determine completion.'),
|
||||
Forms\Components\Textarea::make('config_files')->rows(10)->json()
|
||||
Textarea::make('config_files')->rows(10)->json()
|
||||
->label('Configuration Files')
|
||||
->default('{}')
|
||||
->helperText('This should be a JSON representation of configuration files to modify and what parts should be changed.'),
|
||||
Forms\Components\Textarea::make('config_logs')->rows(10)->json()
|
||||
Textarea::make('config_logs')->rows(10)->json()
|
||||
->label('Log Configuration')
|
||||
->default('{}')
|
||||
->helperText('This should be a JSON representation of where log files are stored, and whether or not the daemon should be creating custom logs.'),
|
||||
]),
|
||||
Forms\Components\Tabs\Tab::make('Egg Variables')
|
||||
Tab::make('Egg Variables')
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\Repeater::make('variables')
|
||||
Repeater::make('variables')
|
||||
->label('')
|
||||
->addActionLabel('Add New Egg Variable')
|
||||
->grid()
|
||||
@@ -137,46 +149,46 @@ class CreateEgg extends CreateRecord
|
||||
return $data;
|
||||
})
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
TextInput::make('name')
|
||||
->live()
|
||||
->debounce(750)
|
||||
->maxLength(191)
|
||||
->maxLength(255)
|
||||
->columnSpanFull()
|
||||
->afterStateUpdated(fn (Forms\Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())
|
||||
)
|
||||
->required(),
|
||||
Forms\Components\Textarea::make('description')->columnSpanFull(),
|
||||
Forms\Components\TextInput::make('env_variable')
|
||||
Textarea::make('description')->columnSpanFull(),
|
||||
TextInput::make('env_variable')
|
||||
->label('Environment Variable')
|
||||
->maxLength(191)
|
||||
->maxLength(255)
|
||||
->prefix('{{')
|
||||
->suffix('}}')
|
||||
->hintIcon('tabler-code')
|
||||
->hintIconTooltip(fn ($state) => "{{{$state}}}")
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('default_value')->maxLength(191),
|
||||
Forms\Components\Fieldset::make('User Permissions')
|
||||
TextInput::make('default_value')->maxLength(255),
|
||||
Fieldset::make('User Permissions')
|
||||
->schema([
|
||||
Forms\Components\Checkbox::make('user_viewable')->label('Viewable'),
|
||||
Forms\Components\Checkbox::make('user_editable')->label('Editable'),
|
||||
Checkbox::make('user_viewable')->label('Viewable'),
|
||||
Checkbox::make('user_editable')->label('Editable'),
|
||||
]),
|
||||
Forms\Components\Textarea::make('rules')->columnSpanFull(),
|
||||
Textarea::make('rules')->columnSpanFull(),
|
||||
]),
|
||||
]),
|
||||
Forms\Components\Tabs\Tab::make('Install Script')
|
||||
Tab::make('Install Script')
|
||||
->columns(3)
|
||||
->schema([
|
||||
|
||||
Forms\Components\Hidden::make('copy_script_from'),
|
||||
Hidden::make('copy_script_from'),
|
||||
//->placeholder('None')
|
||||
//->relationship('scriptFrom', 'name', ignoreRecord: true),
|
||||
|
||||
Forms\Components\TextInput::make('script_container')
|
||||
TextInput::make('script_container')
|
||||
->required()
|
||||
->maxLength(191)
|
||||
->maxLength(255)
|
||||
->default('alpine:3.4'),
|
||||
|
||||
Forms\Components\Select::make('script_entry')
|
||||
Select::make('script_entry')
|
||||
->selectablePlaceholder(false)
|
||||
->default('bash')
|
||||
->options(['bash', 'ash', '/bin/bash'])
|
||||
|
||||
@@ -3,10 +3,29 @@
|
||||
namespace App\Filament\Resources\EggResource\Pages;
|
||||
|
||||
use App\Filament\Resources\EggResource;
|
||||
use App\Filament\Resources\EggResource\RelationManagers\ServersRelationManager;
|
||||
use App\Models\Egg;
|
||||
use App\Services\Eggs\Sharing\EggImporterService;
|
||||
use Exception;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\KeyValue;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\Tabs\Tab;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
|
||||
use App\Services\Eggs\Sharing\EggExporterService;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
|
||||
@@ -18,64 +37,64 @@ class EditEgg extends EditRecord
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\Tabs::make()->tabs([
|
||||
Forms\Components\Tabs\Tab::make('Configuration')
|
||||
Tabs::make()->tabs([
|
||||
Tab::make('Configuration')
|
||||
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(191)
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 1])
|
||||
->maxLength(255)
|
||||
->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')
|
||||
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')
|
||||
TextInput::make('id')
|
||||
->label('Egg ID')
|
||||
->disabled(),
|
||||
Forms\Components\Textarea::make('description')
|
||||
Textarea::make('description')
|
||||
->rows(3)
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->helperText('A description of this Egg that will be displayed throughout the Panel as needed.'),
|
||||
Forms\Components\TextInput::make('author')
|
||||
TextInput::make('author')
|
||||
->required()
|
||||
->maxLength(191)
|
||||
->maxLength(255)
|
||||
->email()
|
||||
->disabled()
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->helperText('The author of this version of the Egg. Uploading a new Egg configuration from a different author will change this.'),
|
||||
Forms\Components\Textarea::make('startup')
|
||||
Textarea::make('startup')
|
||||
->rows(2)
|
||||
->columnSpanFull()
|
||||
->required()
|
||||
->helperText('The default startup command that should be used for new servers using this Egg.'),
|
||||
Forms\Components\TagsInput::make('file_denylist')
|
||||
TagsInput::make('file_denylist')
|
||||
->hidden() // latest wings breaks it.
|
||||
->placeholder('denied-file.txt')
|
||||
->helperText('A list of files that the end user is not allowed to edit.')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
Forms\Components\TagsInput::make('features')
|
||||
TagsInput::make('features')
|
||||
->placeholder('Add Feature')
|
||||
->helperText('')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
Forms\Components\Toggle::make('force_outgoing_ip')
|
||||
Toggle::make('force_outgoing_ip')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's primary allocation IP.
|
||||
Required for certain games to work properly when the Node has multiple public IP addresses.
|
||||
Enabling this option will disable internal networking for any servers using this egg, causing them to be unable to internally access other servers on the same node."),
|
||||
Forms\Components\Hidden::make('script_is_privileged')
|
||||
Hidden::make('script_is_privileged')
|
||||
->helperText('The docker images available to servers using this egg.'),
|
||||
Forms\Components\TagsInput::make('tags')
|
||||
TagsInput::make('tags')
|
||||
->placeholder('Add Tags')
|
||||
->helperText('')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
Forms\Components\TextInput::make('update_url')
|
||||
TextInput::make('update_url')
|
||||
->disabled()
|
||||
->helperText('Not implemented.')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
Forms\Components\KeyValue::make('docker_images')
|
||||
KeyValue::make('docker_images')
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->required()
|
||||
@@ -85,32 +104,32 @@ class EditEgg extends EditRecord
|
||||
->helperText('The docker images available to servers using this egg.'),
|
||||
]),
|
||||
|
||||
Forms\Components\Tabs\Tab::make('Process Management')
|
||||
Tab::make('Process Management')
|
||||
->columns()
|
||||
->schema([
|
||||
Forms\Components\Select::make('config_from')
|
||||
Select::make('config_from')
|
||||
->label('Copy Settings From')
|
||||
->placeholder('None')
|
||||
->relationship('configFrom', 'name', ignoreRecord: true)
|
||||
->helperText('If you would like to default to settings from another Egg select it from the menu above.'),
|
||||
Forms\Components\TextInput::make('config_stop')
|
||||
->maxLength(191)
|
||||
TextInput::make('config_stop')
|
||||
->maxLength(255)
|
||||
->label('Stop Command')
|
||||
->helperText('The command that should be sent to server processes to stop them gracefully. If you need to send a SIGINT you should enter ^C here.'),
|
||||
Forms\Components\Textarea::make('config_startup')->rows(10)->json()
|
||||
Textarea::make('config_startup')->rows(10)->json()
|
||||
->label('Start Configuration')
|
||||
->helperText('List of values the daemon should be looking for when booting a server to determine completion.'),
|
||||
Forms\Components\Textarea::make('config_files')->rows(10)->json()
|
||||
Textarea::make('config_files')->rows(10)->json()
|
||||
->label('Configuration Files')
|
||||
->helperText('This should be a JSON representation of configuration files to modify and what parts should be changed.'),
|
||||
Forms\Components\Textarea::make('config_logs')->rows(10)->json()
|
||||
Textarea::make('config_logs')->rows(10)->json()
|
||||
->label('Log Configuration')
|
||||
->helperText('This should be a JSON representation of where log files are stored, and whether or not the daemon should be creating custom logs.'),
|
||||
]),
|
||||
Forms\Components\Tabs\Tab::make('Egg Variables')
|
||||
Tab::make('Egg Variables')
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\Repeater::make('variables')
|
||||
Repeater::make('variables')
|
||||
->label('')
|
||||
->grid()
|
||||
->relationship('variables')
|
||||
@@ -139,48 +158,48 @@ class EditEgg extends EditRecord
|
||||
return $data;
|
||||
})
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
TextInput::make('name')
|
||||
->live()
|
||||
->debounce(750)
|
||||
->maxLength(191)
|
||||
->maxLength(255)
|
||||
->columnSpanFull()
|
||||
->afterStateUpdated(fn (Forms\Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())
|
||||
)
|
||||
->required(),
|
||||
Forms\Components\Textarea::make('description')->columnSpanFull(),
|
||||
Forms\Components\TextInput::make('env_variable')
|
||||
Textarea::make('description')->columnSpanFull(),
|
||||
TextInput::make('env_variable')
|
||||
->label('Environment Variable')
|
||||
->maxLength(191)
|
||||
->maxLength(255)
|
||||
->prefix('{{')
|
||||
->suffix('}}')
|
||||
->hintIcon('tabler-code')
|
||||
->hintIconTooltip(fn ($state) => "{{{$state}}}")
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('default_value')->maxLength(191),
|
||||
Forms\Components\Fieldset::make('User Permissions')
|
||||
TextInput::make('default_value')->maxLength(255),
|
||||
Fieldset::make('User Permissions')
|
||||
->schema([
|
||||
Forms\Components\Checkbox::make('user_viewable')->label('Viewable'),
|
||||
Forms\Components\Checkbox::make('user_editable')->label('Editable'),
|
||||
Checkbox::make('user_viewable')->label('Viewable'),
|
||||
Checkbox::make('user_editable')->label('Editable'),
|
||||
]),
|
||||
Forms\Components\TextInput::make('rules')->columnSpanFull(),
|
||||
TextInput::make('rules')->columnSpanFull(),
|
||||
]),
|
||||
]),
|
||||
Forms\Components\Tabs\Tab::make('Install Script')
|
||||
Tab::make('Install Script')
|
||||
->columns(3)
|
||||
->schema([
|
||||
|
||||
Forms\Components\Select::make('copy_script_from')
|
||||
Select::make('copy_script_from')
|
||||
->placeholder('None')
|
||||
->relationship('scriptFrom', 'name', ignoreRecord: true),
|
||||
|
||||
Forms\Components\TextInput::make('script_container')
|
||||
TextInput::make('script_container')
|
||||
->required()
|
||||
->maxLength(191)
|
||||
->maxLength(255)
|
||||
->default('alpine:3.4'),
|
||||
|
||||
Forms\Components\TextInput::make('script_entry')
|
||||
TextInput::make('script_entry')
|
||||
->required()
|
||||
->maxLength(191)
|
||||
->maxLength(255)
|
||||
->default('ash'),
|
||||
|
||||
MonacoEditor::make('script_install')
|
||||
@@ -198,19 +217,97 @@ class EditEgg extends EditRecord
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make()
|
||||
Actions\DeleteAction::make('deleteEgg')
|
||||
->disabled(fn (Egg $egg): bool => $egg->servers()->count() > 0)
|
||||
->label(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? 'Delete Egg' : 'Egg In Use'),
|
||||
Actions\ExportAction::make()
|
||||
->icon('tabler-download')
|
||||
->label('Export Egg')
|
||||
->label(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? 'Delete' : 'In Use'),
|
||||
|
||||
Actions\Action::make('exportEgg')
|
||||
->label('Export')
|
||||
->color('primary')
|
||||
// TODO uses old admin panel export service
|
||||
->url(fn (Egg $egg): string => route('admin.eggs.export', ['egg' => $egg['id']])),
|
||||
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
|
||||
echo $service->handle($egg->id);
|
||||
}, 'egg-' . $egg->getKebabName() . '.json')),
|
||||
|
||||
Actions\Action::make('importEgg')
|
||||
->label('Import')
|
||||
->form([
|
||||
Placeholder::make('warning')
|
||||
->label('This will overwrite the current egg to the one you upload.'),
|
||||
Tabs::make('Tabs')
|
||||
->tabs([
|
||||
Tab::make('From File')
|
||||
->icon('tabler-file-upload')
|
||||
->schema([
|
||||
FileUpload::make('egg')
|
||||
->label('Egg')
|
||||
->hint('eg. minecraft.json')
|
||||
->acceptedFileTypes(['application/json'])
|
||||
->storeFiles(false),
|
||||
]),
|
||||
Tab::make('From URL')
|
||||
->icon('tabler-world-upload')
|
||||
->schema([
|
||||
TextInput::make('url')
|
||||
->label('URL')
|
||||
->hint('Link to the egg file (eg. minecraft.json)')
|
||||
->url(),
|
||||
]),
|
||||
])
|
||||
->contained(false),
|
||||
|
||||
])
|
||||
->action(function (array $data, Egg $egg): void {
|
||||
/** @var EggImporterService $eggImportService */
|
||||
$eggImportService = resolve(EggImporterService::class);
|
||||
|
||||
if (!empty($data['egg'])) {
|
||||
try {
|
||||
$eggImportService->fromFile($data['egg'], $egg);
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Import Failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
report($exception);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($data['url'])) {
|
||||
try {
|
||||
$eggImportService->fromUrl($data['url'], $egg);
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Import Failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
report($exception);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->refreshForm();
|
||||
Notification::make()
|
||||
->title('Import Success')
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
];
|
||||
}
|
||||
|
||||
public function refreshForm(): void
|
||||
{
|
||||
$this->fillForm();
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
@@ -219,7 +316,7 @@ class EditEgg extends EditRecord
|
||||
public function getRelationManagers(): array
|
||||
{
|
||||
return [
|
||||
EggResource\RelationManagers\ServersRelationManager::class,
|
||||
ServersRelationManager::class,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,20 @@ namespace App\Filament\Resources\EggResource\Pages;
|
||||
|
||||
use App\Filament\Resources\EggResource;
|
||||
use App\Models\Egg;
|
||||
use App\Services\Eggs\Sharing\EggExporterService;
|
||||
use App\Services\Eggs\Sharing\EggImporterService;
|
||||
use Exception;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\Tabs\Tab;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
use Filament\Tables;
|
||||
@@ -21,54 +29,37 @@ class ListEggs extends ListRecords
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->searchable(false)
|
||||
->searchable(true)
|
||||
->defaultPaginationPageOption(25)
|
||||
->checkIfRecordIsSelectableUsing(fn (Egg $egg) => $egg->servers_count <= 0)
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('id')
|
||||
TextColumn::make('id')
|
||||
->label('Id')
|
||||
->hidden()
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->hidden(),
|
||||
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')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
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(),
|
||||
Tables\Actions\ExportAction::make()
|
||||
EditAction::make(),
|
||||
Tables\Actions\Action::make('export')
|
||||
->icon('tabler-download')
|
||||
->label('Export')
|
||||
->color('primary')
|
||||
// TODO uses old admin panel export service
|
||||
->url(fn (Egg $egg): string => route('admin.eggs.export', ['egg' => $egg])),
|
||||
])
|
||||
->headerActions([
|
||||
//
|
||||
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
|
||||
echo $service->handle($egg->id);
|
||||
}, 'egg-' . $egg->getKebabName() . '.json')),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
@@ -80,21 +71,57 @@ 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([
|
||||
Tab::make('From File')
|
||||
->icon('tabler-file-upload')
|
||||
->schema([
|
||||
FileUpload::make('egg')
|
||||
->label('Egg')
|
||||
->hint('This should be the json file ( egg-minecraft.json )')
|
||||
->acceptedFileTypes(['application/json'])
|
||||
->storeFiles(false)
|
||||
->multiple(),
|
||||
]),
|
||||
Tab::make('From URL')
|
||||
->icon('tabler-world-upload')
|
||||
->schema([
|
||||
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')
|
||||
|
||||
@@ -4,7 +4,8 @@ namespace App\Filament\Resources\EggResource\RelationManagers;
|
||||
|
||||
use App\Models\Server;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\SelectColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ServersRelationManager extends RelationManager
|
||||
@@ -18,23 +19,23 @@ class ServersRelationManager extends RelationManager
|
||||
->emptyStateDescription('No Servers')->emptyStateHeading('No servers are assigned this egg.')
|
||||
->searchable(false)
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('user.username')
|
||||
TextColumn::make('user.username')
|
||||
->label('Owner')
|
||||
->icon('tabler-user')
|
||||
->url(fn (Server $server): string => route('filament.admin.resources.users.edit', ['record' => $server->user]))
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
TextColumn::make('name')
|
||||
->icon('tabler-brand-docker')
|
||||
->url(fn (Server $server): string => route('filament.admin.resources.servers.edit', ['record' => $server]))
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('node.name')
|
||||
TextColumn::make('node.name')
|
||||
->icon('tabler-server-2')
|
||||
->url(fn (Server $server): string => route('filament.admin.resources.nodes.edit', ['record' => $server->node])),
|
||||
Tables\Columns\TextColumn::make('image')
|
||||
TextColumn::make('image')
|
||||
->label('Docker Image'),
|
||||
Tables\Columns\SelectColumn::make('allocation.id')
|
||||
SelectColumn::make('allocation.id')
|
||||
->label('Primary Allocation')
|
||||
->options(fn ($state, Server $server) => [$server->allocation->id => $server->allocation->address])
|
||||
->options(fn (Server $server) => [$server->allocation->id => $server->allocation->address])
|
||||
->selectablePlaceholder(false)
|
||||
->sortable(),
|
||||
]);
|
||||
|
||||
@@ -11,6 +11,7 @@ class MountResource extends Resource
|
||||
protected static ?string $model = Mount::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-layers-linked';
|
||||
protected static ?string $navigationGroup = 'Advanced';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
|
||||
@@ -4,11 +4,14 @@ namespace App\Filament\Resources\MountResource\Pages;
|
||||
|
||||
use App\Filament\Resources\MountResource;
|
||||
use Filament\Forms\Components\Group;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Filament\Forms;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
@@ -23,11 +26,11 @@ class CreateMount extends CreateRecord
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
TextInput::make('name')
|
||||
->required()
|
||||
->helperText('Unique name used to separate this mount from another.')
|
||||
->maxLength(64),
|
||||
Forms\Components\ToggleButtons::make('read_only')
|
||||
ToggleButtons::make('read_only')
|
||||
->label('Read only?')
|
||||
->helperText('Is the mount read only inside the container?')
|
||||
->options([
|
||||
@@ -45,15 +48,15 @@ class CreateMount extends CreateRecord
|
||||
->inline()
|
||||
->default(false)
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('source')
|
||||
TextInput::make('source')
|
||||
->required()
|
||||
->helperText('File path on the host system to mount to a container.')
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('target')
|
||||
->maxLength(255),
|
||||
TextInput::make('target')
|
||||
->required()
|
||||
->helperText('Where the mount will be accessible inside a container.')
|
||||
->maxLength(191),
|
||||
Forms\Components\ToggleButtons::make('user_mountable')
|
||||
->maxLength(255),
|
||||
ToggleButtons::make('user_mountable')
|
||||
->hidden()
|
||||
->label('User mountable?')
|
||||
->options([
|
||||
@@ -71,10 +74,10 @@ class CreateMount extends CreateRecord
|
||||
->default(false)
|
||||
->inline()
|
||||
->required(),
|
||||
Forms\Components\Textarea::make('description')
|
||||
Textarea::make('description')
|
||||
->helperText('A longer description for this mount.')
|
||||
->columnSpanFull(),
|
||||
Forms\Components\Hidden::make('user_mountable')->default(1),
|
||||
Hidden::make('user_mountable')->default(1),
|
||||
])->columnSpan(1)->columns([
|
||||
'default' => 1,
|
||||
'lg' => 2,
|
||||
|
||||
@@ -4,8 +4,10 @@ namespace App\Filament\Resources\MountResource\Pages;
|
||||
|
||||
use App\Filament\Resources\MountResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Group;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
@@ -20,11 +22,11 @@ class EditMount extends EditRecord
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
TextInput::make('name')
|
||||
->required()
|
||||
->helperText('Unique name used to separate this mount from another.')
|
||||
->maxLength(64),
|
||||
Forms\Components\ToggleButtons::make('read_only')
|
||||
ToggleButtons::make('read_only')
|
||||
->label('Read only?')
|
||||
->helperText('Is the mount read only inside the container?')
|
||||
->options([
|
||||
@@ -42,15 +44,15 @@ class EditMount extends EditRecord
|
||||
->inline()
|
||||
->default(false)
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('source')
|
||||
TextInput::make('source')
|
||||
->required()
|
||||
->helperText('File path on the host system to mount to a container.')
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('target')
|
||||
->maxLength(255),
|
||||
TextInput::make('target')
|
||||
->required()
|
||||
->helperText('Where the mount will be accessible inside a container.')
|
||||
->maxLength(191),
|
||||
Forms\Components\ToggleButtons::make('user_mountable')
|
||||
->maxLength(255),
|
||||
ToggleButtons::make('user_mountable')
|
||||
->hidden()
|
||||
->label('User mountable?')
|
||||
->options([
|
||||
@@ -68,7 +70,7 @@ class EditMount extends EditRecord
|
||||
->default(false)
|
||||
->inline()
|
||||
->required(),
|
||||
Forms\Components\Textarea::make('description')
|
||||
Textarea::make('description')
|
||||
->helperText('A longer description for this mount.')
|
||||
->columnSpanFull(),
|
||||
])->columnSpan(1)->columns([
|
||||
|
||||
@@ -6,9 +6,13 @@ use App\Filament\Resources\MountResource;
|
||||
use App\Models\Mount;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\CreateAction;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Tables;
|
||||
|
||||
class ListMounts extends ListRecords
|
||||
{
|
||||
@@ -18,31 +22,28 @@ class ListMounts extends ListRecords
|
||||
return $table
|
||||
->searchable(false)
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
TextColumn::make('name')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('source')
|
||||
TextColumn::make('source')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('target')
|
||||
TextColumn::make('target')
|
||||
->searchable(),
|
||||
Tables\Columns\IconColumn::make('read_only')
|
||||
IconColumn::make('read_only')
|
||||
->icon(fn (bool $state) => $state ? 'tabler-circle-check-filled' : 'tabler-circle-x-filled')
|
||||
->color(fn (bool $state) => $state ? 'success' : 'danger')
|
||||
->sortable(),
|
||||
Tables\Columns\IconColumn::make('user_mountable')
|
||||
IconColumn::make('user_mountable')
|
||||
->hidden()
|
||||
->icon(fn (bool $state) => $state ? 'tabler-circle-check-filled' : 'tabler-circle-x-filled')
|
||||
->color(fn (bool $state) => $state ? 'success' : 'danger')
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
])
|
||||
->emptyStateIcon('tabler-layers-linked')
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\NodeResource\Pages;
|
||||
use App\Filament\Resources\NodeResource\RelationManagers;
|
||||
use App\Filament\Resources\NodeResource\RelationManagers\AllocationsRelationManager;
|
||||
use App\Filament\Resources\NodeResource\RelationManagers\NodesRelationManager;
|
||||
use App\Models\Node;
|
||||
use Filament\Resources\Resource;
|
||||
|
||||
@@ -23,8 +24,8 @@ class NodeResource extends Resource
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
RelationManagers\AllocationsRelationManager::class,
|
||||
RelationManagers\NodesRelationManager::class,
|
||||
AllocationsRelationManager::class,
|
||||
NodesRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,18 @@
|
||||
namespace App\Filament\Resources\NodeResource\Pages;
|
||||
|
||||
use App\Filament\Resources\NodeResource;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Components\Wizard;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class CreateNode extends CreateRecord
|
||||
@@ -18,21 +27,21 @@ class CreateNode extends CreateRecord
|
||||
|
||||
public function form(Forms\Form $form): Forms\Form
|
||||
{
|
||||
return $form->schema([
|
||||
Tabs::make('Tabs')
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'lg' => 4,
|
||||
])
|
||||
->persistTabInQueryString()
|
||||
->columnSpanFull()
|
||||
->tabs([
|
||||
Tabs\Tab::make('Basic Settings')
|
||||
return $form
|
||||
->schema([
|
||||
Wizard::make([
|
||||
Step::make('basic')
|
||||
->label('Basic Settings')
|
||||
->icon('tabler-server')
|
||||
->columnSpanFull()
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'lg' => 4,
|
||||
])
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('fqdn')
|
||||
TextInput::make('fqdn')
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
->autofocus()
|
||||
@@ -55,7 +64,7 @@ class CreateNode extends CreateRecord
|
||||
return "
|
||||
This is the domain name that points to your node's IP Address.
|
||||
If you've already set up this, you can verify it by checking the next field!
|
||||
";
|
||||
";
|
||||
})
|
||||
->hintColor('danger')
|
||||
->hint(function ($state) {
|
||||
@@ -65,7 +74,7 @@ class CreateNode extends CreateRecord
|
||||
|
||||
return '';
|
||||
})
|
||||
->afterStateUpdated(function (Forms\Set $set, ?string $state) {
|
||||
->afterStateUpdated(function (Set $set, ?string $state) {
|
||||
$set('dns', null);
|
||||
$set('ip', null);
|
||||
|
||||
@@ -91,19 +100,19 @@ class CreateNode extends CreateRecord
|
||||
|
||||
$set('dns', false);
|
||||
})
|
||||
->maxLength(191),
|
||||
->maxLength(255),
|
||||
|
||||
Forms\Components\TextInput::make('ip')
|
||||
TextInput::make('ip')
|
||||
->disabled()
|
||||
->hidden(),
|
||||
|
||||
Forms\Components\ToggleButtons::make('dns')
|
||||
ToggleButtons::make('dns')
|
||||
->label('DNS Record Check')
|
||||
->helperText('This lets you know if your DNS record correctly points to an IP Address.')
|
||||
->disabled()
|
||||
->inline()
|
||||
->default(null)
|
||||
->hint(fn (Forms\Get $get) => $get('ip'))
|
||||
->hint(fn (Get $get) => $get('ip'))
|
||||
->hintColor('success')
|
||||
->options([
|
||||
true => 'Valid',
|
||||
@@ -120,7 +129,7 @@ class CreateNode extends CreateRecord
|
||||
'lg' => 1,
|
||||
]),
|
||||
|
||||
Forms\Components\TextInput::make('daemon_listen')
|
||||
TextInput::make('daemon_listen')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
@@ -129,13 +138,13 @@ class CreateNode extends CreateRecord
|
||||
])
|
||||
->label(trans('strings.port'))
|
||||
->helperText('If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.')
|
||||
->minValue(0)
|
||||
->maxValue(65536)
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(8080)
|
||||
->required()
|
||||
->integer(),
|
||||
|
||||
Forms\Components\TextInput::make('name')
|
||||
TextInput::make('name')
|
||||
->label('Display Name')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
@@ -148,7 +157,7 @@ class CreateNode extends CreateRecord
|
||||
->helperText('This name is for display only and can be changed later.')
|
||||
->maxLength(100),
|
||||
|
||||
Forms\Components\ToggleButtons::make('scheme')
|
||||
ToggleButtons::make('scheme')
|
||||
->label('Communicate over SSL')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
@@ -156,9 +165,8 @@ class CreateNode extends CreateRecord
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->required()
|
||||
->inline()
|
||||
->helperText(function (Forms\Get $get) {
|
||||
->helperText(function (Get $get) {
|
||||
if (request()->isSecure()) {
|
||||
return new HtmlString('Your Panel is using a secure SSL connection,<br>so your Daemon must too.');
|
||||
}
|
||||
@@ -184,31 +192,18 @@ class CreateNode extends CreateRecord
|
||||
])
|
||||
->default(fn () => request()->isSecure() ? 'https' : 'http'),
|
||||
]),
|
||||
Tabs\Tab::make('Advanced Settings')
|
||||
Step::make('advanced')
|
||||
->label('Advanced Settings')
|
||||
->icon('tabler-server-cog')
|
||||
->columnSpanFull()
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'lg' => 4,
|
||||
])
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('upload_size')
|
||||
->label('Upload Limit')
|
||||
->helperText('Enter the maximum size of files that can be uploaded through the web-based file manager.')
|
||||
->columnSpan(1)
|
||||
->numeric()->required()
|
||||
->default(256)
|
||||
->minValue(1)
|
||||
->maxValue(1024)
|
||||
->suffix('MiB'),
|
||||
Forms\Components\ToggleButtons::make('public')
|
||||
->label('Automatic Allocation')->inline()
|
||||
->default(true)
|
||||
->columnSpan(1)
|
||||
->options([
|
||||
true => 'Yes',
|
||||
false => 'No',
|
||||
])
|
||||
->colors([
|
||||
true => 'success',
|
||||
false => 'danger',
|
||||
]),
|
||||
Forms\Components\ToggleButtons::make('maintenance_mode')
|
||||
ToggleButtons::make('maintenance_mode')
|
||||
->label('Maintenance Mode')->inline()
|
||||
->columnSpan(1)
|
||||
->default(false)
|
||||
@@ -222,22 +217,55 @@ class CreateNode extends CreateRecord
|
||||
true => 'danger',
|
||||
false => 'success',
|
||||
]),
|
||||
Forms\Components\TagsInput::make('tags')
|
||||
ToggleButtons::make('public')
|
||||
->default(true)
|
||||
->columnSpan(1)
|
||||
->label('Automatic Allocation')->inline()
|
||||
->options([
|
||||
true => 'Yes',
|
||||
false => 'No',
|
||||
])
|
||||
->colors([
|
||||
true => 'success',
|
||||
false => 'danger',
|
||||
]),
|
||||
TagsInput::make('tags')
|
||||
->label('Tags')
|
||||
->disabled()
|
||||
->placeholder('Not Implemented')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('Not Implemented')
|
||||
->columnSpan(1),
|
||||
Forms\Components\Grid::make()
|
||||
->columnSpan(2),
|
||||
TextInput::make('upload_size')
|
||||
->label('Upload Limit')
|
||||
->helperText('Enter the maximum size of files that can be uploaded through the web-based file manager.')
|
||||
->columnSpan(1)
|
||||
->numeric()->required()
|
||||
->default(256)
|
||||
->minValue(1)
|
||||
->maxValue(1024)
|
||||
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'),
|
||||
TextInput::make('daemon_sftp')
|
||||
->columnSpan(1)
|
||||
->label('SFTP Port')
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(2022)
|
||||
->required()
|
||||
->integer(),
|
||||
TextInput::make('daemon_sftp_alias')
|
||||
->columnSpan(2)
|
||||
->label('SFTP Alias')
|
||||
->helperText('Display alias for the SFTP address. Leave empty to use the Node FQDN.'),
|
||||
Grid::make()
|
||||
->columns(6)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('unlimited_mem')
|
||||
ToggleButtons::make('unlimited_mem')
|
||||
->label('Memory')->inlineLabel()->inline()
|
||||
->afterStateUpdated(fn (Forms\Set $set) => $set('memory', 0))
|
||||
->afterStateUpdated(fn (Forms\Set $set) => $set('memory_overallocate', 0))
|
||||
->formatStateUsing(fn (Forms\Get $get) => $get('memory') == 0)
|
||||
->afterStateUpdated(fn (Set $set) => $set('memory', 0))
|
||||
->afterStateUpdated(fn (Set $set) => $set('memory_overallocate', 0))
|
||||
->formatStateUsing(fn (Get $get) => $get('memory') == 0)
|
||||
->live()
|
||||
->options([
|
||||
true => 'Unlimited',
|
||||
@@ -248,19 +276,20 @@ class CreateNode extends CreateRecord
|
||||
false => 'warning',
|
||||
])
|
||||
->columnSpan(2),
|
||||
Forms\Components\TextInput::make('memory')
|
||||
TextInput::make('memory')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
|
||||
->hidden(fn (Get $get) => $get('unlimited_mem'))
|
||||
->label('Memory Limit')->inlineLabel()
|
||||
->suffix('MiB')
|
||||
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->default(0),
|
||||
Forms\Components\TextInput::make('memory_overallocate')
|
||||
->default(0)
|
||||
->required(),
|
||||
TextInput::make('memory_overallocate')
|
||||
->dehydratedWhenHidden()
|
||||
->label('Overallocate')->inlineLabel()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
|
||||
->hidden(fn (Get $get) => $get('unlimited_mem'))
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The % allowable to go over the set limit.')
|
||||
->columnSpan(2)
|
||||
@@ -268,18 +297,19 @@ class CreateNode extends CreateRecord
|
||||
->minValue(-1)
|
||||
->maxValue(100)
|
||||
->default(0)
|
||||
->suffix('%'),
|
||||
->suffix('%')
|
||||
->required(),
|
||||
]),
|
||||
Forms\Components\Grid::make()
|
||||
Grid::make()
|
||||
->columns(6)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('unlimited_disk')
|
||||
ToggleButtons::make('unlimited_disk')
|
||||
->label('Disk')->inlineLabel()->inline()
|
||||
->live()
|
||||
->afterStateUpdated(fn (Forms\Set $set) => $set('disk', 0))
|
||||
->afterStateUpdated(fn (Forms\Set $set) => $set('disk_overallocate', 0))
|
||||
->formatStateUsing(fn (Forms\Get $get) => $get('disk') == 0)
|
||||
->afterStateUpdated(fn (Set $set) => $set('disk', 0))
|
||||
->afterStateUpdated(fn (Set $set) => $set('disk_overallocate', 0))
|
||||
->formatStateUsing(fn (Get $get) => $get('disk') == 0)
|
||||
->options([
|
||||
true => 'Unlimited',
|
||||
false => 'Limited',
|
||||
@@ -289,18 +319,19 @@ class CreateNode extends CreateRecord
|
||||
false => 'warning',
|
||||
])
|
||||
->columnSpan(2),
|
||||
Forms\Components\TextInput::make('disk')
|
||||
TextInput::make('disk')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
|
||||
->hidden(fn (Get $get) => $get('unlimited_disk'))
|
||||
->label('Disk Limit')->inlineLabel()
|
||||
->suffix('MiB')
|
||||
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->default(0),
|
||||
Forms\Components\TextInput::make('disk_overallocate')
|
||||
->default(0)
|
||||
->required(),
|
||||
TextInput::make('disk_overallocate')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
|
||||
->hidden(fn (Get $get) => $get('unlimited_disk'))
|
||||
->label('Overallocate')->inlineLabel()
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The % allowable to go over the set limit.')
|
||||
@@ -309,11 +340,64 @@ class CreateNode extends CreateRecord
|
||||
->minValue(-1)
|
||||
->maxValue(100)
|
||||
->default(0)
|
||||
->suffix('%'),
|
||||
->suffix('%')
|
||||
->required(),
|
||||
]),
|
||||
Grid::make()
|
||||
->columns(6)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
ToggleButtons::make('unlimited_cpu')
|
||||
->label('CPU')->inlineLabel()->inline()
|
||||
->live()
|
||||
->afterStateUpdated(fn (Set $set) => $set('cpu', 0))
|
||||
->afterStateUpdated(fn (Set $set) => $set('cpu_overallocate', 0))
|
||||
->formatStateUsing(fn (Get $get) => $get('cpu') == 0)
|
||||
->options([
|
||||
true => 'Unlimited',
|
||||
false => 'Limited',
|
||||
])
|
||||
->colors([
|
||||
true => 'primary',
|
||||
false => 'warning',
|
||||
])
|
||||
->columnSpan(2),
|
||||
TextInput::make('cpu')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Get $get) => $get('unlimited_cpu'))
|
||||
->label('CPU Limit')->inlineLabel()
|
||||
->suffix('%')
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->default(0)
|
||||
->minValue(0)
|
||||
->required(),
|
||||
TextInput::make('cpu_overallocate')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (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('%')
|
||||
->required(),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
])->columnSpanFull()
|
||||
->nextAction(fn (Action $action) => $action->label('Next Step'))
|
||||
->submitAction(new HtmlString(Blade::render(<<<'BLADE'
|
||||
<x-filament::button
|
||||
type="submit"
|
||||
size="sm"
|
||||
>
|
||||
Create Node
|
||||
</x-filament::button>
|
||||
BLADE))),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getRedirectUrlParameters(): array
|
||||
@@ -322,4 +406,9 @@ class CreateNode extends CreateRecord
|
||||
'tab' => '-configuration-tab',
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,20 @@ 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\Grid;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\Tabs\Tab;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||
@@ -30,10 +41,10 @@ class EditNode extends EditRecord
|
||||
->persistTabInQueryString()
|
||||
->columnSpanFull()
|
||||
->tabs([
|
||||
Tabs\Tab::make('Basic Settings')
|
||||
Tab::make('Basic Settings')
|
||||
->icon('tabler-server')
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('fqdn')
|
||||
TextInput::make('fqdn')
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
->autofocus()
|
||||
@@ -66,7 +77,7 @@ class EditNode extends EditRecord
|
||||
|
||||
return '';
|
||||
})
|
||||
->afterStateUpdated(function (Forms\Set $set, ?string $state) {
|
||||
->afterStateUpdated(function (Set $set, ?string $state) {
|
||||
$set('dns', null);
|
||||
$set('ip', null);
|
||||
|
||||
@@ -92,19 +103,19 @@ class EditNode extends EditRecord
|
||||
|
||||
$set('dns', false);
|
||||
})
|
||||
->maxLength(191),
|
||||
->maxLength(255),
|
||||
|
||||
Forms\Components\TextInput::make('ip')
|
||||
TextInput::make('ip')
|
||||
->disabled()
|
||||
->hidden(),
|
||||
|
||||
Forms\Components\ToggleButtons::make('dns')
|
||||
ToggleButtons::make('dns')
|
||||
->label('DNS Record Check')
|
||||
->helperText('This lets you know if your DNS record correctly points to an IP Address.')
|
||||
->disabled()
|
||||
->inline()
|
||||
->default(null)
|
||||
->hint(fn (Forms\Get $get) => $get('ip'))
|
||||
->hint(fn (Get $get) => $get('ip'))
|
||||
->hintColor('success')
|
||||
->options([
|
||||
true => 'Valid',
|
||||
@@ -121,7 +132,7 @@ class EditNode extends EditRecord
|
||||
'lg' => 1,
|
||||
]),
|
||||
|
||||
Forms\Components\TextInput::make('daemon_listen')
|
||||
TextInput::make('daemon_listen')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
@@ -130,13 +141,13 @@ class EditNode extends EditRecord
|
||||
])
|
||||
->label(trans('strings.port'))
|
||||
->helperText('If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.')
|
||||
->minValue(0)
|
||||
->maxValue(65536)
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(8080)
|
||||
->required()
|
||||
->integer(),
|
||||
|
||||
Forms\Components\TextInput::make('name')
|
||||
TextInput::make('name')
|
||||
->label('Display Name')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
@@ -149,7 +160,7 @@ class EditNode extends EditRecord
|
||||
->helperText('This name is for display only and can be changed later.')
|
||||
->maxLength(100),
|
||||
|
||||
Forms\Components\ToggleButtons::make('scheme')
|
||||
ToggleButtons::make('scheme')
|
||||
->label('Communicate over SSL')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
@@ -157,9 +168,8 @@ class EditNode extends EditRecord
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->required()
|
||||
->inline()
|
||||
->helperText(function (Forms\Get $get) {
|
||||
->helperText(function (Get $get) {
|
||||
if (request()->isSecure()) {
|
||||
return new HtmlString('Your Panel is using a secure SSL connection,<br>so your Daemon must too.');
|
||||
}
|
||||
@@ -184,27 +194,50 @@ class EditNode extends EditRecord
|
||||
'https' => 'tabler-lock',
|
||||
])
|
||||
->default(fn () => request()->isSecure() ? 'https' : 'http'), ]),
|
||||
Tabs\Tab::make('Advanced Settings')
|
||||
Tab::make('Advanced Settings')
|
||||
->columns(['default' => 1, 'sm' => 1, 'md' => 4, 'lg' => 6])
|
||||
->icon('tabler-server-cog')
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('id')
|
||||
TextInput::make('id')
|
||||
->label('Node ID')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 1])
|
||||
->disabled(),
|
||||
Forms\Components\TextInput::make('uuid')
|
||||
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')
|
||||
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),
|
||||
Forms\Components\ToggleButtons::make('public')
|
||||
->hintIconTooltip('Not Implemented'),
|
||||
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(config('panel.use_binary_prefix') ? 'MiB' : 'MB'),
|
||||
TextInput::make('daemon_sftp')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
|
||||
->label('SFTP Port')
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(2022)
|
||||
->required()
|
||||
->integer(),
|
||||
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.'),
|
||||
ToggleButtons::make('public')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
|
||||
->label('Automatic Allocation')->inline()
|
||||
->columnSpan(1)
|
||||
->options([
|
||||
true => 'Yes',
|
||||
false => 'No',
|
||||
@@ -213,37 +246,28 @@ class EditNode extends EditRecord
|
||||
true => 'success',
|
||||
false => 'danger',
|
||||
]),
|
||||
Forms\Components\ToggleButtons::make('maintenance_mode')
|
||||
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)
|
||||
Grid::make()
|
||||
->columns(['default' => 1, 'sm' => 1, 'md' => 3, 'lg' => 6])
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('unlimited_mem')
|
||||
ToggleButtons::make('unlimited_mem')
|
||||
->label('Memory')->inlineLabel()->inline()
|
||||
->afterStateUpdated(fn (Forms\Set $set) => $set('memory', 0))
|
||||
->afterStateUpdated(fn (Forms\Set $set) => $set('memory_overallocate', 0))
|
||||
->formatStateUsing(fn (Forms\Get $get) => $get('memory') == 0)
|
||||
->afterStateUpdated(fn (Set $set) => $set('memory', 0))
|
||||
->afterStateUpdated(fn (Set $set) => $set('memory_overallocate', 0))
|
||||
->formatStateUsing(fn (Get $get) => $get('memory') == 0)
|
||||
->live()
|
||||
->options([
|
||||
true => 'Unlimited',
|
||||
@@ -253,39 +277,79 @@ class EditNode extends EditRecord
|
||||
true => 'primary',
|
||||
false => 'warning',
|
||||
])
|
||||
->columnSpan(2),
|
||||
Forms\Components\TextInput::make('memory')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2]),
|
||||
TextInput::make('memory')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
|
||||
->hidden(fn (Get $get) => $get('unlimited_mem'))
|
||||
->label('Memory Limit')->inlineLabel()
|
||||
->suffix('MiB')
|
||||
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
|
||||
->required()
|
||||
->columnSpan(2)
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
|
||||
->numeric()
|
||||
->minValue(0),
|
||||
Forms\Components\TextInput::make('memory_overallocate')
|
||||
TextInput::make('memory_overallocate')
|
||||
->dehydratedWhenHidden()
|
||||
->label('Overallocate')->inlineLabel()
|
||||
->required()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
|
||||
->hidden(fn (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()
|
||||
Grid::make()
|
||||
->columns(['default' => 1, 'sm' => 1, 'md' => 3, 'lg' => 6])
|
||||
->schema([
|
||||
ToggleButtons::make('unlimited_disk')
|
||||
->label('Disk')->inlineLabel()->inline()
|
||||
->live()
|
||||
->afterStateUpdated(fn (Set $set) => $set('disk', 0))
|
||||
->afterStateUpdated(fn (Set $set) => $set('disk_overallocate', 0))
|
||||
->formatStateUsing(fn (Get $get) => $get('disk') == 0)
|
||||
->options([
|
||||
true => 'Unlimited',
|
||||
false => 'Limited',
|
||||
])
|
||||
->colors([
|
||||
true => 'primary',
|
||||
false => 'warning',
|
||||
])
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2]),
|
||||
TextInput::make('disk')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Get $get) => $get('unlimited_disk'))
|
||||
->label('Disk Limit')->inlineLabel()
|
||||
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
|
||||
->required()
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
|
||||
->numeric()
|
||||
->minValue(0),
|
||||
TextInput::make('disk_overallocate')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Get $get) => $get('unlimited_disk'))
|
||||
->label('Overallocate')->inlineLabel()
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The % allowable to go over the set limit.')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(-1)
|
||||
->maxValue(100)
|
||||
->suffix('%'),
|
||||
]),
|
||||
Grid::make()
|
||||
->columns(6)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('unlimited_disk')
|
||||
->label('Disk')->inlineLabel()->inline()
|
||||
ToggleButtons::make('unlimited_cpu')
|
||||
->label('CPU')->inlineLabel()->inline()
|
||||
->live()
|
||||
->afterStateUpdated(fn (Forms\Set $set) => $set('disk', 0))
|
||||
->afterStateUpdated(fn (Forms\Set $set) => $set('disk_overallocate', 0))
|
||||
->formatStateUsing(fn (Forms\Get $get) => $get('disk') == 0)
|
||||
->afterStateUpdated(fn (Set $set) => $set('cpu', 0))
|
||||
->afterStateUpdated(fn (Set $set) => $set('cpu_overallocate', 0))
|
||||
->formatStateUsing(fn (Get $get) => $get('cpu') == 0)
|
||||
->options([
|
||||
true => 'Unlimited',
|
||||
false => 'Limited',
|
||||
@@ -295,18 +359,18 @@ class EditNode extends EditRecord
|
||||
false => 'warning',
|
||||
])
|
||||
->columnSpan(2),
|
||||
Forms\Components\TextInput::make('disk')
|
||||
TextInput::make('cpu')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
|
||||
->label('Disk Limit')->inlineLabel()
|
||||
->suffix('MiB')
|
||||
->hidden(fn (Get $get) => $get('unlimited_cpu'))
|
||||
->label('CPU Limit')->inlineLabel()
|
||||
->suffix('%')
|
||||
->required()
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->minValue(0),
|
||||
Forms\Components\TextInput::make('disk_overallocate')
|
||||
TextInput::make('cpu_overallocate')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
|
||||
->hidden(fn (Get $get) => $get('unlimited_cpu'))
|
||||
->label('Overallocate')->inlineLabel()
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The % allowable to go over the set limit.')
|
||||
@@ -318,20 +382,33 @@ class EditNode extends EditRecord
|
||||
->suffix('%'),
|
||||
]),
|
||||
]),
|
||||
Tabs\Tab::make('Configuration File')
|
||||
Tab::make('Configuration File')
|
||||
->icon('tabler-code')
|
||||
->schema([
|
||||
Forms\Components\Placeholder::make('instructions')
|
||||
Placeholder::make('instructions')
|
||||
->columnSpanFull()
|
||||
->content(new HtmlString('
|
||||
Save this file to your <span title="usually /etc/pelican/">daemon\'s root directory</span>, named <code>config.yml</code>
|
||||
')),
|
||||
Forms\Components\Textarea::make('config')
|
||||
Textarea::make('config')
|
||||
->label('/etc/pelican/config.yml')
|
||||
->disabled()
|
||||
->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(function (NodeUpdateService $nodeUpdateService, Node $node) {
|
||||
$nodeUpdateService->handle($node, [], true);
|
||||
Notification::make()->success()->title('Daemon Key Reset')->send();
|
||||
$this->fillForm();
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
@@ -367,4 +444,9 @@ class EditNode extends EditRecord
|
||||
NodeMemoryChart::class,
|
||||
];
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
$this->fillForm();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,13 @@ use App\Filament\Resources\NodeResource;
|
||||
use App\Models\Node;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\CreateAction;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Tables;
|
||||
|
||||
class ListNodes extends ListRecords
|
||||
{
|
||||
@@ -20,64 +24,67 @@ class ListNodes extends ListRecords
|
||||
->searchable(false)
|
||||
->checkIfRecordIsSelectableUsing(fn (Node $node) => $node->servers_count <= 0)
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('uuid')
|
||||
TextColumn::make('uuid')
|
||||
->label('UUID')
|
||||
->searchable()
|
||||
->hidden(),
|
||||
Tables\Columns\IconColumn::make('health')
|
||||
IconColumn::make('health')
|
||||
->alignCenter()
|
||||
->state(fn (Node $node) => $node)
|
||||
->view('livewire.columns.version-column'),
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
TextColumn::make('name')
|
||||
->icon('tabler-server-2')
|
||||
->sortable()
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('fqdn')
|
||||
TextColumn::make('fqdn')
|
||||
->visibleFrom('md')
|
||||
->label('Address')
|
||||
->icon('tabler-network')
|
||||
->sortable()
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('memory')
|
||||
TextColumn::make('memory')
|
||||
->visibleFrom('sm')
|
||||
->icon('tabler-device-desktop-analytics')
|
||||
->numeric()
|
||||
->suffix(' GiB')
|
||||
->formatStateUsing(fn ($state) => number_format($state / 1024, 2))
|
||||
->suffix(config('panel.use_binary_prefix') ? ' GiB' : ' GB')
|
||||
->formatStateUsing(fn ($state) => number_format($state / (config('panel.use_binary_prefix') ? 1024 : 1000), 2))
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('disk')
|
||||
TextColumn::make('disk')
|
||||
->visibleFrom('sm')
|
||||
->icon('tabler-file')
|
||||
->numeric()
|
||||
->suffix(' GiB')
|
||||
->formatStateUsing(fn ($state) => number_format($state / 1024, 2))
|
||||
->suffix(config('panel.use_binary_prefix') ? ' GiB' : ' GB')
|
||||
->formatStateUsing(fn ($state) => number_format($state / (config('panel.use_binary_prefix') ? 1024 : 1000), 2))
|
||||
->sortable(),
|
||||
Tables\Columns\IconColumn::make('scheme')
|
||||
TextColumn::make('cpu')
|
||||
->visibleFrom('sm')
|
||||
->icon('tabler-file')
|
||||
->numeric()
|
||||
->suffix(' %')
|
||||
->sortable(),
|
||||
IconColumn::make('scheme')
|
||||
->visibleFrom('xl')
|
||||
->label('SSL')
|
||||
->trueIcon('tabler-lock')
|
||||
->falseIcon('tabler-lock-open-off')
|
||||
->state(fn (Node $node) => $node->scheme === 'https'),
|
||||
Tables\Columns\IconColumn::make('public')
|
||||
IconColumn::make('public')
|
||||
->visibleFrom('lg')
|
||||
->trueIcon('tabler-eye-check')
|
||||
->falseIcon('tabler-eye-cancel'),
|
||||
Tables\Columns\TextColumn::make('servers_count')
|
||||
TextColumn::make('servers_count')
|
||||
->visibleFrom('sm')
|
||||
->counts('servers')
|
||||
->label('Servers')
|
||||
->sortable()
|
||||
->icon('tabler-brand-docker'),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
])
|
||||
->emptyStateIcon('tabler-server-2')
|
||||
|
||||
@@ -3,15 +3,24 @@
|
||||
namespace App\Filament\Resources\NodeResource\RelationManagers;
|
||||
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Server;
|
||||
use App\Models\Node;
|
||||
use App\Services\Allocations\AssignmentService;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\TextInputColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
/**
|
||||
* @method Node getOwnerRecord()
|
||||
*/
|
||||
class AllocationsRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'allocations';
|
||||
@@ -22,7 +31,7 @@ class AllocationsRelationManager extends RelationManager
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('ip')
|
||||
TextInput::make('ip')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
]);
|
||||
@@ -40,20 +49,21 @@ class AllocationsRelationManager extends RelationManager
|
||||
->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->server_id === null)
|
||||
->searchable()
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('server.name')
|
||||
TextColumn::make('id'),
|
||||
TextColumn::make('port')
|
||||
->searchable()
|
||||
->label('Port'),
|
||||
TextColumn::make('server.name')
|
||||
->label('Server')
|
||||
->icon('tabler-brand-docker')
|
||||
->searchable()
|
||||
->url(fn (Allocation $allocation): string => $allocation->server ? route('filament.admin.resources.servers.edit', ['record' => $allocation->server]) : ''),
|
||||
Tables\Columns\TextInputColumn::make('ip_alias')
|
||||
TextInputColumn::make('ip_alias')
|
||||
->searchable()
|
||||
->label('Alias'),
|
||||
Tables\Columns\TextInputColumn::make('ip')
|
||||
TextInputColumn::make('ip')
|
||||
->searchable()
|
||||
->label('IP'),
|
||||
Tables\Columns\TextColumn::make('port')
|
||||
->searchable()
|
||||
->label('Port'),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
@@ -64,20 +74,20 @@ class AllocationsRelationManager extends RelationManager
|
||||
->headerActions([
|
||||
Tables\Actions\Action::make('create new allocation')->label('Create Allocations')
|
||||
->form(fn () => [
|
||||
Forms\Components\TextInput::make('allocation_ip')
|
||||
->datalist($this->getOwnerRecord()->ipAddresses() ?? [])
|
||||
TextInput::make('allocation_ip')
|
||||
->datalist($this->getOwnerRecord()->ipAddresses())
|
||||
->label('IP Address')
|
||||
->inlineLabel()
|
||||
->ipv4()
|
||||
->helperText("Usually your machine's public IP unless you are port forwarding.")
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('allocation_alias')
|
||||
TextInput::make('allocation_alias')
|
||||
->label('Alias')
|
||||
->inlineLabel()
|
||||
->default(null)
|
||||
->helperText('Optional display name to help you remember what these are.')
|
||||
->required(false),
|
||||
Forms\Components\TagsInput::make('allocation_ports')
|
||||
TagsInput::make('allocation_ports')
|
||||
->placeholder('Examples: 27015, 27017-27019')
|
||||
->helperText(new HtmlString('
|
||||
These are the ports that users can connect to this Server through.
|
||||
@@ -87,7 +97,7 @@ class AllocationsRelationManager extends RelationManager
|
||||
->label('Ports')
|
||||
->inlineLabel()
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, Forms\Set $set) {
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
$ports = collect();
|
||||
$update = false;
|
||||
foreach ($state as $portEntry) {
|
||||
@@ -112,7 +122,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);
|
||||
}
|
||||
}
|
||||
@@ -141,9 +151,8 @@ class AllocationsRelationManager extends RelationManager
|
||||
->action(fn (array $data) => resolve(AssignmentService::class)->handle($this->getOwnerRecord(), $data)),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
// Tables\Actions\DissociateBulkAction::make(),
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
namespace App\Filament\Resources\NodeResource\RelationManagers;
|
||||
|
||||
use App\Models\Server;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\SelectColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
|
||||
@@ -18,34 +19,34 @@ class NodesRelationManager extends RelationManager
|
||||
return $table
|
||||
->searchable(false)
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('user.username')
|
||||
TextColumn::make('user.username')
|
||||
->label('Owner')
|
||||
->icon('tabler-user')
|
||||
->url(fn (Server $server): string => route('filament.admin.resources.users.edit', ['record' => $server->user]))
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
TextColumn::make('name')
|
||||
->icon('tabler-brand-docker')
|
||||
->url(fn (Server $server): string => route('filament.admin.resources.servers.edit', ['record' => $server]))
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('egg.name')
|
||||
TextColumn::make('egg.name')
|
||||
->icon('tabler-egg')
|
||||
->url(fn (Server $server): string => route('filament.admin.resources.eggs.edit', ['record' => $server->user]))
|
||||
->sortable(),
|
||||
Tables\Columns\SelectColumn::make('allocation.id')
|
||||
SelectColumn::make('allocation.id')
|
||||
->label('Primary Allocation')
|
||||
->options(fn ($state, Server $server) => [$server->allocation->id => $server->allocation->address])
|
||||
->options(fn (Server $server) => [$server->allocation->id => $server->allocation->address])
|
||||
->selectablePlaceholder(false)
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('memory')->icon('tabler-device-desktop-analytics'),
|
||||
Tables\Columns\TextColumn::make('cpu')->icon('tabler-cpu'),
|
||||
Tables\Columns\TextColumn::make('databases_count')
|
||||
TextColumn::make('memory')->icon('tabler-device-desktop-analytics'),
|
||||
TextColumn::make('cpu')->icon('tabler-cpu'),
|
||||
TextColumn::make('databases_count')
|
||||
->counts('databases')
|
||||
->label('Databases')
|
||||
->icon('tabler-database')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('backups_count')
|
||||
TextColumn::make('backups_count')
|
||||
->counts('backups')
|
||||
->label('Backups')
|
||||
->icon('tabler-file-download')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ use App\Models\Server;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\CreateAction;
|
||||
use Filament\Tables\Grouping\Group;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Tables;
|
||||
|
||||
@@ -18,35 +19,18 @@ class ListServers extends ListRecords
|
||||
{
|
||||
return $table
|
||||
->searchable(false)
|
||||
->defaultGroup('node.name')
|
||||
->groups([
|
||||
Group::make('node.name')->getDescriptionFromRecordUsing(fn (Server $server): string => str($server->node->description)->limit(150)),
|
||||
Group::make('user.username')->getDescriptionFromRecordUsing(fn (Server $server): string => $server->user->email),
|
||||
Group::make('egg.name')->getDescriptionFromRecordUsing(fn (Server $server): string => str($server->egg->description)->limit(150)),
|
||||
])
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('status')
|
||||
Tables\Columns\TextColumn::make('condition')
|
||||
->default('unknown')
|
||||
->badge()
|
||||
->default(function (Server $server) {
|
||||
if ($server->status !== null) {
|
||||
return $server->status;
|
||||
}
|
||||
|
||||
return $server->retrieveStatus() ?? 'node_fail';
|
||||
})
|
||||
->icon(fn ($state) => match ($state) {
|
||||
'node_fail' => 'tabler-server-off',
|
||||
'running' => 'tabler-heartbeat',
|
||||
'removing' => 'tabler-heart-x',
|
||||
'offline' => 'tabler-heart-off',
|
||||
'paused' => 'tabler-heart-pause',
|
||||
'installing' => 'tabler-heart-bolt',
|
||||
'suspended' => 'tabler-heart-cancel',
|
||||
default => 'tabler-heart-question',
|
||||
})
|
||||
->color(fn ($state): string => match ($state) {
|
||||
'running' => 'success',
|
||||
'installing', 'restarting' => 'primary',
|
||||
'paused', 'removing' => 'warning',
|
||||
'node_fail', 'install_failed', 'suspended' => 'danger',
|
||||
default => 'gray',
|
||||
}),
|
||||
|
||||
->icon(fn (Server $server) => $server->conditionIcon())
|
||||
->color(fn (Server $server) => $server->conditionColor()),
|
||||
Tables\Columns\TextColumn::make('uuid')
|
||||
->hidden()
|
||||
->label('UUID')
|
||||
@@ -58,19 +42,25 @@ class ListServers extends ListRecords
|
||||
Tables\Columns\TextColumn::make('node.name')
|
||||
->icon('tabler-server-2')
|
||||
->url(fn (Server $server): string => route('filament.admin.resources.nodes.edit', ['record' => $server->node]))
|
||||
->sortable(),
|
||||
->hidden(fn (Table $table) => $table->getGrouping()?->getId() === 'node.name')
|
||||
->sortable()
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('egg.name')
|
||||
->icon('tabler-egg')
|
||||
->url(fn (Server $server): string => route('filament.admin.resources.eggs.edit', ['record' => $server->egg]))
|
||||
->sortable(),
|
||||
->hidden(fn (Table $table) => $table->getGrouping()?->getId() === 'egg.name')
|
||||
->sortable()
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('user.username')
|
||||
->icon('tabler-user')
|
||||
->label('Owner')
|
||||
->url(fn (Server $server): string => route('filament.admin.resources.users.edit', ['record' => $server->user]))
|
||||
->sortable(),
|
||||
->hidden(fn (Table $table) => $table->getGrouping()?->getId() === 'user.username')
|
||||
->sortable()
|
||||
->searchable(),
|
||||
Tables\Columns\SelectColumn::make('allocation_id')
|
||||
->label('Primary Allocation')
|
||||
->options(fn ($state, Server $server) => $server->allocations->mapWithKeys(
|
||||
->options(fn (Server $server) => $server->allocations->mapWithKeys(
|
||||
fn ($allocation) => [$allocation->id => $allocation->address])
|
||||
)
|
||||
->selectablePlaceholder(false)
|
||||
@@ -83,9 +73,6 @@ class ListServers extends ListRecords
|
||||
->numeric()
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\Action::make('View')
|
||||
->icon('tabler-terminal')
|
||||
@@ -93,6 +80,7 @@ class ListServers extends ListRecords
|
||||
Tables\Actions\EditAction::make(),
|
||||
])
|
||||
->emptyStateIcon('tabler-brand-docker')
|
||||
->searchable()
|
||||
->emptyStateDescription('')
|
||||
->emptyStateHeading('No Servers')
|
||||
->emptyStateActions([
|
||||
|
||||
@@ -3,12 +3,16 @@
|
||||
namespace App\Filament\Resources\ServerResource\RelationManagers;
|
||||
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Server;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
/**
|
||||
* @method Server getOwnerRecord()
|
||||
*/
|
||||
class AllocationsRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'allocations';
|
||||
@@ -27,21 +31,23 @@ 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',
|
||||
true => 'tabler-star-filled',
|
||||
default => 'tabler-star',
|
||||
})
|
||||
->color(fn ($state) => match ($state) {
|
||||
false => 'gray',
|
||||
true => 'warning',
|
||||
default => 'gray',
|
||||
})
|
||||
->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]))
|
||||
->default(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id)
|
||||
@@ -57,12 +63,15 @@ 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([
|
||||
Tables\Actions\DissociateBulkAction::make(),
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\UserResource\Pages;
|
||||
use App\Filament\Resources\UserResource\RelationManagers;
|
||||
use App\Filament\Resources\UserResource\RelationManagers\ServersRelationManager;
|
||||
use App\Models\User;
|
||||
use Filament\Resources\Resource;
|
||||
|
||||
@@ -23,7 +23,7 @@ class UserResource extends Resource
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
RelationManagers\ServersRelationManager::class,
|
||||
ServersRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -2,15 +2,18 @@
|
||||
|
||||
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;
|
||||
use chillerlan\QRCode\QRCode;
|
||||
use chillerlan\QRCode\QROptions;
|
||||
use DateTimeZone;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
@@ -20,13 +23,18 @@ 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;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
/**
|
||||
* @method User getUser()
|
||||
*/
|
||||
class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
{
|
||||
protected function getForms(): array
|
||||
@@ -45,7 +53,7 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
->label(trans('strings.username'))
|
||||
->disabled()
|
||||
->readOnly()
|
||||
->maxLength(191)
|
||||
->maxLength(255)
|
||||
->unique(ignoreRecord: true)
|
||||
->autofocus(),
|
||||
|
||||
@@ -54,7 +62,7 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
->label(trans('strings.email'))
|
||||
->email()
|
||||
->required()
|
||||
->maxLength(191)
|
||||
->maxLength(255)
|
||||
->unique(ignoreRecord: true),
|
||||
|
||||
TextInput::make('password')
|
||||
@@ -78,6 +86,12 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
->visible(fn (Get $get): bool => filled($get('password')))
|
||||
->dehydrated(false),
|
||||
|
||||
Select::make('timezone')
|
||||
->required()
|
||||
->prefixIcon('tabler-clock-pin')
|
||||
->options(fn () => collect(DateTimeZone::listIdentifiers())->mapWithKeys(fn ($tz) => [$tz => $tz]))
|
||||
->searchable(),
|
||||
|
||||
Select::make('language')
|
||||
->label(trans('strings.language'))
|
||||
->required()
|
||||
@@ -99,15 +113,31 @@ 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'),
|
||||
];
|
||||
}
|
||||
/** @var TwoFactorSetupService */
|
||||
$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'),
|
||||
'svgLogoScale' => 0.05,
|
||||
'addLogoSpace' => true,
|
||||
'logoSpaceWidth' => 13,
|
||||
'logoSpaceHeight' => 13,
|
||||
@@ -115,22 +145,24 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
|
||||
// https://github.com/chillerlan/php-qrcode/blob/main/examples/svgWithLogo.php
|
||||
|
||||
// SVG logo options (see extended class)
|
||||
$options->svgLogo = public_path('pelican.svg'); // logo from: https://github.com/simple-icons/simple-icons
|
||||
$options->svgLogoScale = 0.05;
|
||||
// $options->svgLogoCssClass = 'dark';
|
||||
|
||||
// QROptions
|
||||
// @phpstan-ignore property.protected
|
||||
$options->version = Version::AUTO;
|
||||
// $options->outputInterface = QRSvgWithLogo::class;
|
||||
// @phpstan-ignore property.protected
|
||||
$options->outputBase64 = false;
|
||||
// @phpstan-ignore property.protected
|
||||
$options->eccLevel = EccLevel::H; // ECC level H is necessary when using logos
|
||||
// @phpstan-ignore property.protected
|
||||
$options->addQuietzone = true;
|
||||
// $options->drawLightModules = true;
|
||||
// @phpstan-ignore property.protected
|
||||
$options->connectPaths = true;
|
||||
// @phpstan-ignore property.protected
|
||||
$options->drawCircularModules = true;
|
||||
// $options->circleRadius = 0.45;
|
||||
|
||||
// @phpstan-ignore property.protected
|
||||
$options->svgDefs = '<linearGradient id="gradient" x1="100%" y2="100%">
|
||||
<stop stop-color="#7dd4fc" offset="0"/>
|
||||
<stop stop-color="#38bdf8" offset="0.5"/>
|
||||
@@ -147,9 +179,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,8 +200,12 @@ 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')
|
||||
->live(),
|
||||
|
||||
TagsInput::make('allowed_ips')
|
||||
->live()
|
||||
->splitKeys([',', ' ', 'Tab'])
|
||||
->placeholder('Example: 127.0.0.1 or 192.168.1.1')
|
||||
->label('Whitelisted IP\'s')
|
||||
@@ -167,9 +213,10 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
->columnSpanFull(),
|
||||
])->headerActions([
|
||||
Action::make('Create')
|
||||
->disabled(fn (Get $get) => $get('description') === null)
|
||||
->successRedirectUrl(route('filament.admin.auth.profile', ['tab' => '-api-keys-tab']))
|
||||
->action(function (Get $get, Action $action) {
|
||||
$token = auth()->user()->createToken(
|
||||
->action(function (Get $get, Action $action, $user) {
|
||||
$token = $user->createToken(
|
||||
$get('description'),
|
||||
$get('allowed_ips'),
|
||||
);
|
||||
@@ -182,8 +229,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 +283,43 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
protected function handleRecordUpdate($record, $data): \Illuminate\Database\Eloquent\Model
|
||||
{
|
||||
if ($token = $data['2facode'] ?? null) {
|
||||
/** @var ToggleTwoFactorService $service */
|
||||
$service = resolve(ToggleTwoFactorService::class);
|
||||
|
||||
$tokens = $service->handle($record, $token, true);
|
||||
cache()->set("users.$record->id.2fa.tokens", implode("\n", $tokens), now()->addSeconds(15));
|
||||
|
||||
$this->redirectRoute('filament.admin.auth.profile', ['tab' => '-2fa-tab']);
|
||||
}
|
||||
|
||||
if ($token = $data['2fa-disable-code'] ?? null) {
|
||||
/** @var ToggleTwoFactorService $service */
|
||||
$service = resolve(ToggleTwoFactorService::class);
|
||||
|
||||
$service->handle($record, $token, false);
|
||||
|
||||
cache()->forget("users.$record->id.2fa.state");
|
||||
}
|
||||
|
||||
return parent::handleRecordUpdate($record, $data);
|
||||
}
|
||||
|
||||
public function exception($e, $stopPropagation): void
|
||||
{
|
||||
if ($e instanceof TwoFactorAuthenticationTokenInvalid) {
|
||||
Notification::make()
|
||||
->title('Invalid 2FA Code')
|
||||
->body($e->getMessage())
|
||||
->color('danger')
|
||||
->icon('tabler-2fa')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
$stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Filament\Resources\UserResource\Pages;
|
||||
|
||||
use App\Filament\Resources\UserResource;
|
||||
use App\Services\Exceptions\FilamentExceptionHandler;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use App\Models\User;
|
||||
@@ -19,8 +20,8 @@ class EditUser extends EditRecord
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()->schema([
|
||||
Forms\Components\TextInput::make('username')->required()->maxLength(191),
|
||||
Forms\Components\TextInput::make('email')->email()->required()->maxLength(191),
|
||||
Forms\Components\TextInput::make('username')->required()->maxLength(255),
|
||||
Forms\Components\TextInput::make('email')->email()->required()->maxLength(255),
|
||||
|
||||
Forms\Components\TextInput::make('password')
|
||||
->dehydrateStateUsing(fn (string $state): string => Hash::make($state))
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ class ListUsers extends ListRecords
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
])
|
||||
->checkIfRecordIsSelectableUsing(fn (User $user) => !$user->servers_count)
|
||||
->checkIfRecordIsSelectableUsing(fn (User $user) => auth()->user()->id !== $user->id && !$user->servers_count)
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
@@ -85,12 +85,12 @@ class ListUsers extends ListRecords
|
||||
Forms\Components\TextInput::make('username')
|
||||
->alphaNum()
|
||||
->required()
|
||||
->maxLength(191),
|
||||
->maxLength(255),
|
||||
Forms\Components\TextInput::make('email')
|
||||
->email()
|
||||
->required()
|
||||
->unique()
|
||||
->maxLength(191),
|
||||
->maxLength(255),
|
||||
|
||||
Forms\Components\TextInput::make('password')
|
||||
->hintIcon('tabler-question-mark')
|
||||
|
||||
@@ -68,7 +68,7 @@ class ServersRelationManager extends RelationManager
|
||||
->sortable(),
|
||||
Tables\Columns\SelectColumn::make('allocation.id')
|
||||
->label('Primary Allocation')
|
||||
->options(fn ($state, Server $server) => [$server->allocation->id => $server->allocation->address])
|
||||
->options(fn (Server $server) => [$server->allocation->id => $server->allocation->address])
|
||||
->selectablePlaceholder(false)
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('image')->hidden(),
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
namespace App\Http\Controllers\Admin\Eggs;
|
||||
|
||||
use App\Exceptions\Service\Egg\NoParentConfigurationFoundException;
|
||||
use Illuminate\View\View;
|
||||
use App\Models\Egg;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
use Illuminate\View\Factory as ViewFactory;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\Eggs\EggUpdateService;
|
||||
use App\Services\Eggs\EggCreationService;
|
||||
use App\Http\Requests\Admin\Egg\EggFormRequest;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
class EggController extends Controller
|
||||
{
|
||||
@@ -19,8 +19,6 @@ class EggController extends Controller
|
||||
*/
|
||||
public function __construct(
|
||||
protected AlertsMessageBag $alert,
|
||||
protected EggCreationService $creationService,
|
||||
protected EggUpdateService $updateService,
|
||||
protected ViewFactory $view
|
||||
) {
|
||||
}
|
||||
@@ -58,7 +56,16 @@ class EggController extends Controller
|
||||
$data['docker_images'] = $this->normalizeDockerImages($data['docker_images'] ?? null);
|
||||
$data['author'] = $request->user()->email;
|
||||
|
||||
$egg = $this->creationService->handle($data);
|
||||
$data['config_from'] = array_get($data, 'config_from');
|
||||
if (!is_null($data['config_from'])) {
|
||||
$parentEgg = Egg::query()->find(array_get($data, 'config_from'));
|
||||
throw_unless($parentEgg, new NoParentConfigurationFoundException(trans('exceptions.egg.invalid_copy_id')));
|
||||
}
|
||||
|
||||
$egg = Egg::query()->create(array_merge($data, [
|
||||
'uuid' => Uuid::uuid4()->toString(),
|
||||
]));
|
||||
|
||||
$this->alert->success(trans('admin/eggs.notices.egg_created'))->flash();
|
||||
|
||||
return redirect()->route('admin.eggs.view', $egg->id);
|
||||
@@ -90,7 +97,13 @@ class EggController extends Controller
|
||||
$data = $request->validated();
|
||||
$data['docker_images'] = $this->normalizeDockerImages($data['docker_images'] ?? null);
|
||||
|
||||
$this->updateService->handle($egg, $data);
|
||||
$eggId = array_get($data, 'config_from');
|
||||
$copiedFromEgg = Egg::query()->find($eggId);
|
||||
|
||||
throw_unless($copiedFromEgg, new NoParentConfigurationFoundException(trans('exceptions.egg.invalid_copy_id')));
|
||||
|
||||
$egg->update($data);
|
||||
|
||||
$this->alert->success(trans('admin/eggs.notices.updated'))->flash();
|
||||
|
||||
return redirect()->route('admin.eggs.view', $egg->id);
|
||||
|
||||
@@ -10,7 +10,6 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use App\Services\Eggs\Sharing\EggExporterService;
|
||||
use App\Services\Eggs\Sharing\EggImporterService;
|
||||
use App\Http\Requests\Admin\Egg\EggImportFormRequest;
|
||||
use App\Services\Eggs\Sharing\EggUpdateImporterService;
|
||||
|
||||
class EggShareController extends Controller
|
||||
{
|
||||
@@ -21,7 +20,6 @@ class EggShareController extends Controller
|
||||
protected AlertsMessageBag $alert,
|
||||
protected EggExporterService $exporterService,
|
||||
protected EggImporterService $importerService,
|
||||
protected EggUpdateImporterService $updateImporterService
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -46,7 +44,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 +59,7 @@ class EggShareController extends Controller
|
||||
*/
|
||||
public function update(EggImportFormRequest $request, Egg $egg): RedirectResponse
|
||||
{
|
||||
$this->updateImporterService->handle($egg, $request->file('import_file'));
|
||||
$this->importerService->fromFile($request->file('import_file'), $egg);
|
||||
$this->alert->success(trans('admin/eggs.notices.updated_via_import'))->flash();
|
||||
|
||||
return redirect()->route('admin.eggs.view', ['egg' => $egg]);
|
||||
|
||||
@@ -56,7 +56,7 @@ class NodeAutoDeployController extends Controller
|
||||
|
||||
return new JsonResponse([
|
||||
'node' => $node->id,
|
||||
'token' => $key->identifier . decrypt($key->token),
|
||||
'token' => $key->identifier . $key->token,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers\Admin\Nodes;
|
||||
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Node;
|
||||
use Spatie\QueryBuilder\QueryBuilder;
|
||||
use App\Http\Controllers\Controller;
|
||||
@@ -13,7 +12,7 @@ class NodeController extends Controller
|
||||
/**
|
||||
* Returns a listing of nodes on the system.
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
public function index(): View
|
||||
{
|
||||
$nodes = QueryBuilder::for(
|
||||
Node::query()->withCount('servers')
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers\Admin\Nodes;
|
||||
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Node;
|
||||
use Illuminate\Support\Collection;
|
||||
use App\Models\Allocation;
|
||||
@@ -29,16 +28,10 @@ class NodeViewController extends Controller
|
||||
/**
|
||||
* Returns index view for a specific node on the system.
|
||||
*/
|
||||
public function index(Request $request, Node $node): View
|
||||
public function index(Node $node): View
|
||||
{
|
||||
$node->loadCount('servers');
|
||||
|
||||
$stats = Node::query()
|
||||
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk')
|
||||
->join('servers', 'servers.node_id', '=', 'nodes.id')
|
||||
->where('node_id', '=', $node->id)
|
||||
->first();
|
||||
|
||||
return view('admin.nodes.view.index', [
|
||||
'node' => $node,
|
||||
'version' => $this->versionService,
|
||||
@@ -48,7 +41,7 @@ class NodeViewController extends Controller
|
||||
/**
|
||||
* Returns the settings page for a specific node.
|
||||
*/
|
||||
public function settings(Request $request, Node $node): View
|
||||
public function settings(Node $node): View
|
||||
{
|
||||
return view('admin.nodes.view.settings', [
|
||||
'node' => $node,
|
||||
@@ -58,7 +51,7 @@ class NodeViewController extends Controller
|
||||
/**
|
||||
* Return the node configuration page for a specific node.
|
||||
*/
|
||||
public function configuration(Request $request, Node $node): View
|
||||
public function configuration(Node $node): View
|
||||
{
|
||||
return view('admin.nodes.view.configuration', compact('node'));
|
||||
}
|
||||
@@ -66,7 +59,7 @@ class NodeViewController extends Controller
|
||||
/**
|
||||
* Return the node allocation management page.
|
||||
*/
|
||||
public function allocations(Request $request, Node $node): View
|
||||
public function allocations(Node $node): View
|
||||
{
|
||||
$node->setRelation(
|
||||
'allocations',
|
||||
@@ -92,7 +85,7 @@ class NodeViewController extends Controller
|
||||
/**
|
||||
* Return a listing of servers that exist for this specific node.
|
||||
*/
|
||||
public function servers(Request $request, Node $node): View
|
||||
public function servers(Node $node): View
|
||||
{
|
||||
$this->plainInject([
|
||||
'node' => Collection::wrap($node->makeVisible(['daemon_token_id', 'daemon_token']))
|
||||
|
||||
@@ -53,7 +53,6 @@ class CreateServerController extends Controller
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws \App\Exceptions\DisplayException
|
||||
* @throws \App\Exceptions\Service\Deployment\NoViableAllocationException
|
||||
* @throws \App\Exceptions\Service\Deployment\NoViableNodeException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function store(ServerFormRequest $request): RedirectResponse
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers\Admin\Servers;
|
||||
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Server;
|
||||
use Spatie\QueryBuilder\QueryBuilder;
|
||||
use Spatie\QueryBuilder\AllowedFilter;
|
||||
@@ -16,7 +15,7 @@ class ServerController extends Controller
|
||||
* Returns all the servers that exist on the system using a paginated result set. If
|
||||
* a query is passed along in the request it is also passed to the repository function.
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
public function index(): View
|
||||
{
|
||||
$servers = QueryBuilder::for(Server::query()->with('node', 'user', 'allocation'))
|
||||
->allowedFilters([
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Enums\ServerState;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Response;
|
||||
use App\Models\Mount;
|
||||
use App\Models\Server;
|
||||
use App\Models\Database;
|
||||
use App\Models\MountServer;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
use App\Exceptions\DisplayException;
|
||||
@@ -70,7 +70,7 @@ class ServersController extends Controller
|
||||
* @throws \App\Exceptions\DisplayException
|
||||
* @throws \App\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function toggleInstall(Server $server): RedirectResponse
|
||||
public function toggleInstall(Server $server)
|
||||
{
|
||||
if ($server->status === ServerState::InstallFailed) {
|
||||
throw new DisplayException(trans('admin/server.exceptions.marked_as_failed'));
|
||||
@@ -79,9 +79,13 @@ class ServersController extends Controller
|
||||
$server->status = $server->isInstalled() ? ServerState::Installing : null;
|
||||
$server->save();
|
||||
|
||||
$this->alert->success(trans('admin/server.alerts.install_toggled'))->flash();
|
||||
Notification::make()
|
||||
->title('Success!')
|
||||
->body(trans('admin/server.alerts.install_toggled'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return redirect()->route('admin.servers.view.manage', $server->id);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,12 +94,15 @@ class ServersController extends Controller
|
||||
* @throws \App\Exceptions\DisplayException
|
||||
* @throws \App\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function reinstallServer(Server $server): RedirectResponse
|
||||
public function reinstallServer(Server $server)
|
||||
{
|
||||
$this->reinstallService->handle($server);
|
||||
$this->alert->success(trans('admin/server.alerts.server_reinstalled'))->flash();
|
||||
|
||||
return redirect()->route('admin.servers.view.manage', $server->id);
|
||||
Notification::make()
|
||||
->title('Success!')
|
||||
->body(trans('admin/server.alerts.server_reinstalled'))
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -228,12 +235,7 @@ class ServersController extends Controller
|
||||
*/
|
||||
public function addMount(Request $request, Server $server): RedirectResponse
|
||||
{
|
||||
$mountServer = (new MountServer())->forceFill([
|
||||
'mount_id' => $request->input('mount_id'),
|
||||
'server_id' => $server->id,
|
||||
]);
|
||||
|
||||
$mountServer->saveOrFail();
|
||||
$server->mounts()->attach($request->input('mount_id'));
|
||||
|
||||
$this->alert->success('Mount was added successfully.')->flash();
|
||||
|
||||
@@ -245,7 +247,7 @@ class ServersController extends Controller
|
||||
*/
|
||||
public function deleteMount(Server $server, Mount $mount): RedirectResponse
|
||||
{
|
||||
MountServer::where('mount_id', $mount->id)->where('server_id', $server->id)->delete();
|
||||
$server->mounts()->detach($mount);
|
||||
|
||||
$this->alert->success('Mount was removed successfully.')->flash();
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ class UserController extends Controller
|
||||
/**
|
||||
* Display user index page.
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
public function index(): View
|
||||
{
|
||||
$users = QueryBuilder::for(
|
||||
User::query()->select('users.*')
|
||||
|
||||
165
app/Http/Controllers/Api/Application/Mounts/MountController.php
Normal file
165
app/Http/Controllers/Api/Application/Mounts/MountController.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Application\Mounts;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Contracts\Translation\Translator;
|
||||
use Spatie\QueryBuilder\QueryBuilder;
|
||||
use App\Models\Mount;
|
||||
use App\Http\Controllers\Api\Application\ApplicationApiController;
|
||||
use App\Transformers\Api\Application\MountTransformer;
|
||||
use App\Http\Requests\Api\Application\Mounts\GetMountRequest;
|
||||
use App\Http\Requests\Api\Application\Mounts\StoreMountRequest;
|
||||
use App\Http\Requests\Api\Application\Mounts\DeleteMountRequest;
|
||||
use App\Http\Requests\Api\Application\Mounts\UpdateMountRequest;
|
||||
use App\Exceptions\Service\HasActiveServersException;
|
||||
|
||||
class MountController extends ApplicationApiController
|
||||
{
|
||||
/**
|
||||
* MountController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected Translator $translator
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all the mounts currently available on the Panel.
|
||||
*/
|
||||
public function index(GetMountRequest $request): array
|
||||
{
|
||||
$mounts = QueryBuilder::for(Mount::query())
|
||||
->allowedFilters(['uuid', 'name'])
|
||||
->allowedSorts(['id', 'uuid'])
|
||||
->paginate($request->query('per_page') ?? 50);
|
||||
|
||||
return $this->fractal->collection($mounts)
|
||||
->transformWith($this->getTransformer(MountTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return data for a single instance of a mount.
|
||||
*/
|
||||
public function view(GetMountRequest $request, Mount $mount): array
|
||||
{
|
||||
return $this->fractal->item($mount)
|
||||
->transformWith($this->getTransformer(MountTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new mount on the Panel. Returns the created mount and an HTTP/201
|
||||
* status response on success.
|
||||
*
|
||||
* @throws \App\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function store(StoreMountRequest $request): JsonResponse
|
||||
{
|
||||
$model = (new Mount())->fill($request->validated());
|
||||
$model->forceFill(['uuid' => Uuid::uuid4()->toString()]);
|
||||
|
||||
$model->saveOrFail();
|
||||
$mount = $model->fresh();
|
||||
|
||||
return $this->fractal->item($mount)
|
||||
->transformWith($this->getTransformer(MountTransformer::class))
|
||||
->addMeta([
|
||||
'resource' => route('api.application.mounts.view', [
|
||||
'mount' => $mount->id,
|
||||
]),
|
||||
])
|
||||
->respond(201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing mount on the Panel.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function update(UpdateMountRequest $request, Mount $mount): array
|
||||
{
|
||||
$mount->forceFill($request->validated())->save();
|
||||
|
||||
return $this->fractal->item($mount)
|
||||
->transformWith($this->getTransformer(MountTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a given mount from the Panel as long as there are no servers
|
||||
* currently attached to it.
|
||||
*
|
||||
* @throws \App\Exceptions\Service\HasActiveServersException
|
||||
*/
|
||||
public function delete(DeleteMountRequest $request, Mount $mount): JsonResponse
|
||||
{
|
||||
if ($mount->servers()->count() > 0) {
|
||||
throw new HasActiveServersException($this->translator->get('exceptions.mount.servers_attached'));
|
||||
}
|
||||
|
||||
$mount->delete();
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds eggs to the mount's many-to-many relation.
|
||||
*/
|
||||
public function addEggs(Request $request, Mount $mount): array
|
||||
{
|
||||
$validatedData = $request->validate([
|
||||
'eggs' => 'required|exists:eggs,id',
|
||||
]);
|
||||
|
||||
$eggs = $validatedData['eggs'] ?? [];
|
||||
if (count($eggs) > 0) {
|
||||
$mount->eggs()->attach($eggs);
|
||||
}
|
||||
|
||||
return $this->fractal->item($mount)
|
||||
->transformWith($this->getTransformer(MountTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds nodes to the mount's many-to-many relation.
|
||||
*/
|
||||
public function addNodes(Request $request, Mount $mount): array
|
||||
{
|
||||
$data = $request->validate(['nodes' => 'required|exists:nodes,id']);
|
||||
|
||||
$nodes = $data['nodes'] ?? [];
|
||||
if (count($nodes) > 0) {
|
||||
$mount->nodes()->attach($nodes);
|
||||
}
|
||||
|
||||
return $this->fractal->item($mount)
|
||||
->transformWith($this->getTransformer(MountTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an egg from the mount's many-to-many relation.
|
||||
*/
|
||||
public function deleteEgg(Mount $mount, int $egg_id): JsonResponse
|
||||
{
|
||||
$mount->eggs()->detach($egg_id);
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a node from the mount's many-to-many relation.
|
||||
*/
|
||||
public function deleteNode(Mount $mount, int $node_id): JsonResponse
|
||||
{
|
||||
$mount->nodes()->detach($node_id);
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ class NodeController extends ApplicationApiController
|
||||
{
|
||||
$nodes = QueryBuilder::for(Node::query())
|
||||
->allowedFilters(['uuid', 'name', 'fqdn', 'daemon_token_id'])
|
||||
->allowedSorts(['id', 'uuid', 'memory', 'disk'])
|
||||
->allowedSorts(['id', 'uuid', 'memory', 'disk', 'cpu'])
|
||||
->paginate($request->query('per_page') ?? 50);
|
||||
|
||||
return $this->fractal->collection($nodes)
|
||||
|
||||
@@ -9,9 +9,6 @@ use App\Http\Requests\Api\Application\Nodes\GetDeployableNodesRequest;
|
||||
|
||||
class NodeDeploymentController extends ApplicationApiController
|
||||
{
|
||||
/**
|
||||
* NodeDeploymentController constructor.
|
||||
*/
|
||||
public function __construct(private FindViableNodesService $viableNodesService)
|
||||
{
|
||||
parent::__construct();
|
||||
@@ -21,16 +18,17 @@ class NodeDeploymentController extends ApplicationApiController
|
||||
* Finds any nodes that are available using the given deployment criteria. This works
|
||||
* similarly to the server creation process, but allows you to pass the deployment object
|
||||
* to this endpoint and get back a list of all Nodes satisfying the requirements.
|
||||
*
|
||||
* @throws \App\Exceptions\Service\Deployment\NoViableNodeException
|
||||
*/
|
||||
public function __invoke(GetDeployableNodesRequest $request): array
|
||||
{
|
||||
$data = $request->validated();
|
||||
$nodes = $this->viableNodesService
|
||||
->setMemory($data['memory'])
|
||||
->setDisk($data['disk'])
|
||||
->handle((int) $request->query('per_page'), (int) $request->query('page'));
|
||||
|
||||
$nodes = $this->viableNodesService->handle(
|
||||
$data['memory'] ?? 0,
|
||||
$data['disk'] ?? 0,
|
||||
$data['cpu'] ?? 0,
|
||||
$data['tags'] ?? $data['location_ids'] ?? [],
|
||||
);
|
||||
|
||||
return $this->fractal->collection($nodes)
|
||||
->transformWith($this->getTransformer(NodeTransformer::class))
|
||||
|
||||
@@ -50,7 +50,6 @@ class ServerController extends ApplicationApiController
|
||||
* @throws \App\Exceptions\DisplayException
|
||||
* @throws \App\Exceptions\Model\DataValidationException
|
||||
* @throws \App\Exceptions\Service\Deployment\NoViableAllocationException
|
||||
* @throws \App\Exceptions\Service\Deployment\NoViableNodeException
|
||||
*/
|
||||
public function store(StoreServerRequest $request): JsonResponse
|
||||
{
|
||||
|
||||
@@ -75,11 +75,11 @@ class ServerManagementController extends ApplicationApiController
|
||||
|
||||
if ($this->transferServerService->handle($server, $validatedData)) {
|
||||
// Transfer started
|
||||
$this->returnNoContent();
|
||||
} else {
|
||||
// Node was not viable
|
||||
return new Response('', Response::HTTP_NOT_ACCEPTABLE);
|
||||
return $this->returnNoContent();
|
||||
}
|
||||
|
||||
// Node was not viable
|
||||
return new Response('', Response::HTTP_NOT_ACCEPTABLE);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,9 +8,9 @@ use Illuminate\Auth\AuthManager;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Facades\Activity;
|
||||
use App\Services\Users\UserUpdateService;
|
||||
use App\Transformers\Api\Client\AccountTransformer;
|
||||
use App\Http\Requests\Api\Client\Account\UpdateEmailRequest;
|
||||
use App\Http\Requests\Api\Client\Account\UpdatePasswordRequest;
|
||||
use App\Transformers\Api\Client\UserTransformer;
|
||||
|
||||
class AccountController extends ClientApiController
|
||||
{
|
||||
@@ -25,7 +25,7 @@ class AccountController extends ClientApiController
|
||||
public function index(Request $request): array
|
||||
{
|
||||
return $this->fractal->item($request->user())
|
||||
->transformWith($this->getTransformer(AccountTransformer::class))
|
||||
->transformWith($this->getTransformer(UserTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ abstract class AbstractLoginController extends Controller
|
||||
'data' => [
|
||||
'complete' => true,
|
||||
'intended' => $this->redirectPath(),
|
||||
'user' => $user->toVueObject(),
|
||||
'user' => $user->toReactObject(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
61
app/Http/Controllers/Auth/OAuthController.php
Normal file
61
app/Http/Controllers/Auth/OAuthController.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use Illuminate\Auth\AuthManager;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Laravel\Socialite\Facades\Socialite;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Services\Users\UserUpdateService;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class OAuthController extends Controller
|
||||
{
|
||||
/**
|
||||
* OAuthController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private AuthManager $auth,
|
||||
private UserUpdateService $updateService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect user to the OAuth provider
|
||||
*/
|
||||
protected function redirect(string $driver): RedirectResponse
|
||||
{
|
||||
return Socialite::with($driver)->redirect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback from OAuth provider.
|
||||
*/
|
||||
protected function callback(Request $request, string $driver): RedirectResponse
|
||||
{
|
||||
$oauthUser = Socialite::driver($driver)->user();
|
||||
|
||||
// User is already logged in and wants to link a new OAuth Provider
|
||||
if ($request->user()) {
|
||||
$oauth = $request->user()->oauth;
|
||||
$oauth[$driver] = $oauthUser->getId();
|
||||
|
||||
$this->updateService->handle($request->user(), ['oauth' => $oauth]);
|
||||
|
||||
return redirect()->route('account');
|
||||
}
|
||||
|
||||
try {
|
||||
$user = User::query()->whereJsonContains('oauth->'. $driver, $oauthUser->getId())->firstOrFail();
|
||||
|
||||
$this->auth->guard()->login($user, true);
|
||||
} catch (Exception $e) {
|
||||
// No user found - redirect to normal login
|
||||
return redirect()->route('auth.login');
|
||||
}
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
}
|
||||
44
app/Http/Controllers/Base/OAuthController.php
Normal file
44
app/Http/Controllers/Base/OAuthController.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Base;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Laravel\Socialite\Facades\Socialite;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\Users\UserUpdateService;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class OAuthController extends Controller
|
||||
{
|
||||
/**
|
||||
* OAuthController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private UserUpdateService $updateService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Link a new OAuth
|
||||
*/
|
||||
protected function link(Request $request): RedirectResponse
|
||||
{
|
||||
$driver = $request->get('driver');
|
||||
|
||||
return Socialite::with($driver)->redirect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a OAuth link
|
||||
*/
|
||||
protected function unlink(Request $request): Response
|
||||
{
|
||||
$oauth = $request->user()->oauth;
|
||||
unset($oauth[$request->get('driver')]);
|
||||
|
||||
$this->updateService->handle($request->user(), ['oauth' => $oauth]);
|
||||
|
||||
return new Response('', Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -9,14 +9,14 @@ class EggFormRequest extends AdminFormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = [
|
||||
'name' => 'required|string|max:191',
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'docker_images' => 'required|string',
|
||||
'force_outgoing_ip' => 'sometimes|boolean',
|
||||
'file_denylist' => 'array',
|
||||
'startup' => 'required|string',
|
||||
'config_from' => 'sometimes|bail|nullable|numeric',
|
||||
'config_stop' => 'required_without:config_from|nullable|string|max:191',
|
||||
'config_stop' => 'required_without:config_from|nullable|string|max:255',
|
||||
'config_startup' => 'required_without:config_from|nullable|json',
|
||||
'config_logs' => 'required_without:config_from|nullable|json',
|
||||
'config_files' => 'required_without:config_from|nullable|json',
|
||||
|
||||
@@ -13,9 +13,9 @@ class EggVariableFormRequest extends AdminFormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'required|string|min:1|max:191',
|
||||
'name' => 'required|string|min:1|max:255',
|
||||
'description' => 'sometimes|nullable|string',
|
||||
'env_variable' => 'required|regex:/^[\w]{1,191}$/|notIn:' . EggVariable::RESERVED_ENV_NAMES,
|
||||
'env_variable' => 'required|regex:/^[\w]{1,255}$/|notIn:' . EggVariable::RESERVED_ENV_NAMES,
|
||||
'options' => 'sometimes|required|array',
|
||||
'rules' => 'bail|required|string',
|
||||
'default_value' => 'present',
|
||||
|
||||
@@ -10,7 +10,7 @@ class AllocationFormRequest extends AdminFormRequest
|
||||
{
|
||||
return [
|
||||
'allocation_ip' => 'required|string',
|
||||
'allocation_alias' => 'sometimes|nullable|string|max:191',
|
||||
'allocation_alias' => 'sometimes|nullable|string|max:255',
|
||||
'allocation_ports' => 'required|array',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ class AdvancedSettingsFormRequest extends AdminFormRequest
|
||||
{
|
||||
return [
|
||||
'recaptcha:enabled' => 'required|in:true,false',
|
||||
'recaptcha:secret_key' => 'required|string|max:191',
|
||||
'recaptcha:website_key' => 'required|string|max:191',
|
||||
'recaptcha:secret_key' => 'required|string|max:255',
|
||||
'recaptcha:website_key' => 'required|string|max:255',
|
||||
'panel:guzzle:timeout' => 'required|integer|between:1,60',
|
||||
'panel:guzzle:connect_timeout' => 'required|integer|between:1,60',
|
||||
'panel:client_features:allocations:enabled' => 'required|in:true,false',
|
||||
|
||||
@@ -13,7 +13,7 @@ class BaseSettingsFormRequest extends AdminFormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'app:name' => 'required|string|max:191',
|
||||
'app:name' => 'required|string|max:255',
|
||||
'panel:auth:2fa_required' => 'required|integer|in:0,1,2',
|
||||
'app:locale' => ['required', 'string', Rule::in(array_keys($this->getAvailableLanguages()))],
|
||||
];
|
||||
|
||||
@@ -16,10 +16,10 @@ class MailSettingsFormRequest extends AdminFormRequest
|
||||
'mail:mailers:smtp:host' => 'required|string',
|
||||
'mail:mailers:smtp:port' => 'required|integer|between:1,65535',
|
||||
'mail:mailers:smtp:encryption' => ['present', Rule::in([null, 'tls', 'ssl'])],
|
||||
'mail:mailers:smtp:username' => 'nullable|string|max:191',
|
||||
'mail:mailers:smtp:password' => 'nullable|string|max:191',
|
||||
'mail:mailers:smtp:username' => 'nullable|string|max:255',
|
||||
'mail:mailers:smtp:password' => 'nullable|string|max:255',
|
||||
'mail:from:address' => 'required|string|email',
|
||||
'mail:from:name' => 'nullable|string|max:191',
|
||||
'mail:from:name' => 'nullable|string|max:255',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ class StoreAllocationRequest extends ApplicationApiRequest
|
||||
{
|
||||
return [
|
||||
'ip' => 'required|string',
|
||||
'alias' => 'sometimes|nullable|string|max:191',
|
||||
'alias' => 'sometimes|nullable|string|max:255',
|
||||
'ports' => 'required|array',
|
||||
'ports.*' => 'string',
|
||||
];
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Application\Mounts;
|
||||
|
||||
use App\Services\Acl\Api\AdminAcl;
|
||||
use App\Http\Requests\Api\Application\ApplicationApiRequest;
|
||||
|
||||
class DeleteMountRequest extends ApplicationApiRequest
|
||||
{
|
||||
protected ?string $resource = AdminAcl::RESOURCE_MOUNTS;
|
||||
|
||||
protected int $permission = AdminAcl::WRITE;
|
||||
}
|
||||
13
app/Http/Requests/Api/Application/Mounts/GetMountRequest.php
Normal file
13
app/Http/Requests/Api/Application/Mounts/GetMountRequest.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Application\Mounts;
|
||||
|
||||
use App\Services\Acl\Api\AdminAcl;
|
||||
use App\Http\Requests\Api\Application\ApplicationApiRequest;
|
||||
|
||||
class GetMountRequest extends ApplicationApiRequest
|
||||
{
|
||||
protected ?string $resource = AdminAcl::RESOURCE_MOUNTS;
|
||||
|
||||
protected int $permission = AdminAcl::READ;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Application\Mounts;
|
||||
|
||||
use App\Services\Acl\Api\AdminAcl;
|
||||
use App\Http\Requests\Api\Application\ApplicationApiRequest;
|
||||
|
||||
class StoreMountRequest extends ApplicationApiRequest
|
||||
{
|
||||
protected ?string $resource = AdminAcl::RESOURCE_MOUNTS;
|
||||
|
||||
protected int $permission = AdminAcl::WRITE;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Application\Mounts;
|
||||
|
||||
use App\Models\Mount;
|
||||
|
||||
class UpdateMountRequest extends StoreMountRequest
|
||||
{
|
||||
/**
|
||||
* Apply validation rules to this request.
|
||||
*/
|
||||
public function rules(array $rules = null): array
|
||||
{
|
||||
/** @var Mount $mount */
|
||||
$mount = $this->route()->parameter('mount');
|
||||
|
||||
return Mount::getRulesForUpdate($mount->id);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,11 @@ class GetDeployableNodesRequest extends GetNodesRequest
|
||||
'page' => 'integer',
|
||||
'memory' => 'required|integer|min:0',
|
||||
'disk' => 'required|integer|min:0',
|
||||
'cpu' => 'sometimes|integer|min:0',
|
||||
'tags' => 'sometimes|array',
|
||||
|
||||
/** @deprecated use tags instead */
|
||||
'location_ids' => 'sometimes|array',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,13 +28,14 @@ class StoreNodeRequest extends ApplicationApiRequest
|
||||
'memory_overallocate',
|
||||
'disk',
|
||||
'disk_overallocate',
|
||||
'cpu',
|
||||
'cpu_overallocate',
|
||||
'upload_size',
|
||||
'daemon_listen',
|
||||
'daemon_sftp',
|
||||
'daemon_sftp_alias',
|
||||
'daemon_base',
|
||||
])->mapWithKeys(function ($value, $key) {
|
||||
$key = ($key === 'daemon_sftp') ? 'daemon_sftp' : $key;
|
||||
|
||||
return [snake_case($key) => $value];
|
||||
})->toArray();
|
||||
}
|
||||
@@ -58,12 +59,8 @@ class StoreNodeRequest extends ApplicationApiRequest
|
||||
public function validated($key = null, $default = null): array
|
||||
{
|
||||
$response = parent::validated();
|
||||
$response['daemon_listen'] = $response['daemon_listen'];
|
||||
$response['daemon_sftp'] = $response['daemon_sftp'];
|
||||
$response['daemon_base'] = $response['daemon_base'] ?? (new Node())->getAttribute('daemon_base');
|
||||
|
||||
unset($response['daemon_base'], $response['daemon_listen'], $response['daemon_sftp']);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ class StoreServerRequest extends ApplicationApiRequest
|
||||
'description' => array_merge(['nullable'], $rules['description']),
|
||||
'user' => $rules['owner_id'],
|
||||
'egg' => $rules['egg_id'],
|
||||
'docker_image' => $rules['image'],
|
||||
'startup' => $rules['startup'],
|
||||
'docker_image' => 'sometimes|string',
|
||||
'startup' => 'sometimes|string',
|
||||
'environment' => 'present|array',
|
||||
'skip_scripts' => 'sometimes|boolean',
|
||||
'oom_killer' => 'sometimes|boolean',
|
||||
@@ -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;
|
||||
|
||||
@@ -20,10 +20,10 @@ class UpdateServerStartupRequest extends ApplicationApiRequest
|
||||
$data = Server::getRulesForUpdate($this->parameter('server', Server::class));
|
||||
|
||||
return [
|
||||
'startup' => $data['startup'],
|
||||
'startup' => 'sometimes|string',
|
||||
'environment' => 'present|array',
|
||||
'egg' => $data['egg_id'],
|
||||
'image' => $data['image'],
|
||||
'image' => 'sometimes|string',
|
||||
'skip_scripts' => 'present|boolean',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ class StoreUserRequest extends ApplicationApiRequest
|
||||
'username',
|
||||
'password',
|
||||
'language',
|
||||
'timezone',
|
||||
'root_admin',
|
||||
])->toArray();
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ class StoreBackupRequest extends ClientApiRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'nullable|string|max:191',
|
||||
'name' => 'nullable|string|max:255',
|
||||
'is_locked' => 'nullable|boolean',
|
||||
'ignored' => 'nullable|string',
|
||||
];
|
||||
|
||||
@@ -14,7 +14,7 @@ class StoreSubuserRequest extends SubuserRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => 'required|email|between:1,191',
|
||||
'email' => 'required|email|between:1,255',
|
||||
'permissions' => 'required|array',
|
||||
'permissions.*' => 'string',
|
||||
];
|
||||
|
||||
@@ -27,6 +27,7 @@ class AssetComposer
|
||||
'enabled' => config('recaptcha.enabled', false),
|
||||
'siteKey' => config('recaptcha.website_key') ?? '',
|
||||
],
|
||||
'usesSyncDriver' => config('queue.default') === 'sync',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ class RunTaskJob extends Job implements ShouldQueue
|
||||
*/
|
||||
public function __construct(public Task $task, public bool $manualRun = false)
|
||||
{
|
||||
$this->queue = 'standard';
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
@@ -112,7 +103,8 @@ class Allocation extends Model
|
||||
return !is_null($this->ip_alias);
|
||||
}
|
||||
|
||||
public function address(): Attribute
|
||||
/** @return Attribute<string, never> */
|
||||
protected function address(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => "$this->ip:$this->port",
|
||||
|
||||
@@ -15,7 +15,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
* @property int $key_type
|
||||
* @property string $identifier
|
||||
* @property string $token
|
||||
* @property array|null $allowed_ips
|
||||
* @property array $allowed_ips
|
||||
* @property string|null $memo
|
||||
* @property \Illuminate\Support\Carbon|null $last_used_at
|
||||
* @property \Illuminate\Support\Carbon|null $expires_at
|
||||
@@ -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,14 @@ class ApiKey extends Model
|
||||
'r_' . AdminAcl::RESOURCE_EGGS,
|
||||
'r_' . AdminAcl::RESOURCE_NODES,
|
||||
'r_' . AdminAcl::RESOURCE_SERVERS,
|
||||
'r_' . AdminAcl::RESOURCE_MOUNTS,
|
||||
];
|
||||
|
||||
/**
|
||||
* Default attributes when creating a new model.
|
||||
*/
|
||||
protected $attributes = [
|
||||
'allowed_ips' => '[]',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -126,7 +135,7 @@ class ApiKey extends Model
|
||||
'identifier' => 'required|string|size:16|unique:api_keys,identifier',
|
||||
'token' => 'required|string',
|
||||
'memo' => 'required|nullable|string|max:500',
|
||||
'allowed_ips' => 'nullable|array',
|
||||
'allowed_ips' => 'array',
|
||||
'allowed_ips.*' => 'string',
|
||||
'last_used_at' => 'nullable|date',
|
||||
'expires_at' => 'nullable|date',
|
||||
@@ -137,6 +146,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 +156,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 +166,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 +196,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;
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user