mirror of
https://github.com/pelican-dev/panel.git
synced 2026-05-04 18:00:48 +03:00
Compare commits
1 Commits
auto-claud
...
release/v1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0208bb4630 |
43
.github/workflows/ci.yaml
vendored
43
.github/workflows/ci.yaml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
php: [8.2, 8.3, 8.4, 8.5]
|
||||
php: [8.2, 8.3, 8.4]
|
||||
env:
|
||||
DB_CONNECTION: sqlite
|
||||
DB_DATABASE: testing.sqlite
|
||||
@@ -61,17 +61,14 @@ jobs:
|
||||
- name: Create SQLite file
|
||||
run: touch database/testing.sqlite
|
||||
|
||||
- name: Run Migrations
|
||||
run: php artisan migrate --force --seed
|
||||
|
||||
- name: Unit tests
|
||||
run: vendor/bin/pest tests/Unit --parallel
|
||||
run: vendor/bin/pest tests/Unit
|
||||
env:
|
||||
DB_HOST: UNIT_NO_DB
|
||||
SKIP_MIGRATIONS: true
|
||||
|
||||
- name: Integration tests
|
||||
run: vendor/bin/pest tests/Integration --parallel
|
||||
run: vendor/bin/pest tests/Integration
|
||||
|
||||
mysql:
|
||||
name: MySQL
|
||||
@@ -79,7 +76,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
php: [8.2, 8.3, 8.4, 8.5]
|
||||
php: [8.2, 8.3, 8.4]
|
||||
database: ["mysql:8"]
|
||||
services:
|
||||
database:
|
||||
@@ -123,20 +120,14 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
|
||||
|
||||
- name: Run Migrations
|
||||
run: php artisan migrate --force --seed
|
||||
env:
|
||||
DB_PORT: ${{ job.services.database.ports[3306] }}
|
||||
DB_USERNAME: root
|
||||
|
||||
- name: Unit tests
|
||||
run: vendor/bin/pest tests/Unit --parallel
|
||||
run: vendor/bin/pest tests/Unit
|
||||
env:
|
||||
DB_HOST: UNIT_NO_DB
|
||||
SKIP_MIGRATIONS: true
|
||||
|
||||
- name: Integration tests
|
||||
run: vendor/bin/pest tests/Integration --parallel
|
||||
run: vendor/bin/pest tests/Integration
|
||||
env:
|
||||
DB_PORT: ${{ job.services.database.ports[3306] }}
|
||||
DB_USERNAME: root
|
||||
@@ -147,7 +138,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
php: [8.2, 8.3, 8.4, 8.5]
|
||||
php: [8.2, 8.3, 8.4]
|
||||
database: ["mariadb:10.6", "mariadb:10.11", "mariadb:11.4"]
|
||||
services:
|
||||
database:
|
||||
@@ -191,20 +182,14 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
|
||||
|
||||
- name: Run Migrations
|
||||
run: php artisan migrate --force --seed
|
||||
env:
|
||||
DB_PORT: ${{ job.services.database.ports[3306] }}
|
||||
DB_USERNAME: root
|
||||
|
||||
- name: Unit tests
|
||||
run: vendor/bin/pest tests/Unit --parallel
|
||||
run: vendor/bin/pest tests/Unit
|
||||
env:
|
||||
DB_HOST: UNIT_NO_DB
|
||||
SKIP_MIGRATIONS: true
|
||||
|
||||
- name: Integration tests
|
||||
run: vendor/bin/pest tests/Integration --parallel
|
||||
run: vendor/bin/pest tests/Integration
|
||||
env:
|
||||
DB_PORT: ${{ job.services.database.ports[3306] }}
|
||||
DB_USERNAME: root
|
||||
@@ -215,7 +200,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
php: [8.2, 8.3, 8.4, 8.5]
|
||||
php: [8.2, 8.3, 8.4]
|
||||
database: ["postgres:14"]
|
||||
services:
|
||||
database:
|
||||
@@ -253,7 +238,6 @@ jobs:
|
||||
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
|
||||
@@ -266,14 +250,11 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
|
||||
|
||||
- name: Run Migrations
|
||||
run: php artisan migrate --force --seed
|
||||
|
||||
- name: Unit tests
|
||||
run: vendor/bin/pest tests/Unit --parallel
|
||||
run: vendor/bin/pest tests/Unit
|
||||
env:
|
||||
DB_HOST: UNIT_NO_DB
|
||||
SKIP_MIGRATIONS: true
|
||||
|
||||
- name: Integration tests
|
||||
run: vendor/bin/pest tests/Integration --parallel
|
||||
run: vendor/bin/pest tests/Integration
|
||||
|
||||
6
.github/workflows/lint.yaml
vendored
6
.github/workflows/lint.yaml
vendored
@@ -3,7 +3,7 @@ name: Lint
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
pint:
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: "8.4"
|
||||
php-version: "8.3"
|
||||
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
php: [8.2, 8.3, 8.4, 8.5]
|
||||
php: [ 8.2, 8.3, 8.4 ]
|
||||
steps:
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
46
Dockerfile
46
Dockerfile
@@ -38,7 +38,7 @@ RUN yarn config set network-timeout 300000 \
|
||||
FROM --platform=$TARGETOS/$TARGETARCH composer AS composerbuild
|
||||
|
||||
# Copy full code to optimize autoload
|
||||
COPY --exclude=docker/ . ./
|
||||
COPY --exclude=Caddyfile --exclude=docker/ . ./
|
||||
|
||||
RUN composer dump-autoload --optimize
|
||||
|
||||
@@ -50,7 +50,7 @@ FROM --platform=$TARGETOS/$TARGETARCH yarn AS yarnbuild
|
||||
WORKDIR /build
|
||||
|
||||
# Copy full code
|
||||
COPY --exclude=docker/ . ./
|
||||
COPY --exclude=Caddyfile --exclude=docker/ . ./
|
||||
COPY --from=composer /build .
|
||||
|
||||
RUN yarn run build
|
||||
@@ -62,34 +62,38 @@ FROM --platform=$TARGETOS/$TARGETARCH localhost:5000/base-php:$TARGETARCH AS fin
|
||||
|
||||
WORKDIR /var/www/html
|
||||
|
||||
# Install additional required libraries
|
||||
RUN apk add --no-cache \
|
||||
# packages for running the panel
|
||||
caddy ca-certificates supervisor supercronic fcgi \
|
||||
# required for installing plugins. Pulled from https://github.com/pelican-dev/panel/pull/2034
|
||||
zip unzip 7zip bzip2-dev yarn git
|
||||
caddy ca-certificates supervisor supercronic fcgi
|
||||
|
||||
COPY --chown=root:www-data --chmod=770 --from=composerbuild /build .
|
||||
COPY --chown=root:www-data --chmod=770 --from=yarnbuild /build/public ./public
|
||||
COPY --chown=root:www-data --chmod=640 --from=composerbuild /build .
|
||||
COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public
|
||||
|
||||
# Create and remove directories
|
||||
RUN mkdir -p /pelican-data/storage /pelican-data/plugins /var/run/supervisord \
|
||||
&& rm -rf /var/www/html/plugins \
|
||||
# Symlinks for env, database, storage, and plugins
|
||||
&& ln -s /pelican-data/.env /var/www/html/.env \
|
||||
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
|
||||
&& ln -s /pelican-data/storage /var/www/html/public/storage \
|
||||
&& ln -s /pelican-data/storage /var/www/html/storage/app/public \
|
||||
# Set permissions
|
||||
# First ensure all files are owned by root and restrict www-data to read access
|
||||
RUN chown root:www-data ./ \
|
||||
&& chmod 750 ./ \
|
||||
# Files should not have execute set, but directories need it
|
||||
&& find ./ -type d -exec chmod 750 {} \; \
|
||||
# Create necessary directories
|
||||
&& mkdir -p /pelican-data/storage /pelican-data/plugins /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \
|
||||
# Symlinks for env, database, storage, and plugins
|
||||
&& ln -s /pelican-data/.env ./.env \
|
||||
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
|
||||
&& ln -sf /var/www/html/storage/app/public /var/www/html/public/storage \
|
||||
&& ln -s /pelican-data/storage/avatars /var/www/html/storage/app/public/avatars \
|
||||
&& ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
|
||||
&& ln -s /pelican-data/plugins /var/www/html/plugins \
|
||||
# Allow www-data write permissions where necessary
|
||||
&& chown -R www-data: /pelican-data .env ./storage ./plugins ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
|
||||
&& chmod -R 770 /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
|
||||
&& chown -R www-data: /usr/local/etc/php/ /usr/local/etc/php-fpm.d/
|
||||
# Allow www-data write permissions where necessary
|
||||
&& chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
|
||||
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
|
||||
&& chown -R www-data: /usr/local/etc/php/
|
||||
|
||||
# Configure Supervisor
|
||||
COPY docker/supervisord.conf /etc/supervisord.conf
|
||||
COPY docker/Caddyfile /etc/caddy/Caddyfile
|
||||
# Add Laravel scheduler to crontab
|
||||
COPY docker/crontab /etc/crontabs/crontab
|
||||
COPY docker/crontab /etc/supercronic/crontab
|
||||
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
COPY docker/healthcheck.sh /healthcheck.sh
|
||||
|
||||
@@ -5,6 +5,6 @@ FROM --platform=$TARGETOS/$TARGETARCH php:8.4-fpm-alpine
|
||||
|
||||
ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
|
||||
|
||||
RUN install-php-extensions bcmath gd intl zip pcntl pdo_mysql pdo_pgsql bz2
|
||||
RUN install-php-extensions bcmath gd intl zip opcache pcntl posix pdo_mysql pdo_pgsql
|
||||
|
||||
RUN rm /usr/local/bin/install-php-extensions
|
||||
|
||||
@@ -5,7 +5,7 @@ FROM --platform=$TARGETOS/$TARGETARCH php:8.4-fpm-alpine AS base
|
||||
|
||||
ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
|
||||
|
||||
RUN install-php-extensions bcmath gd intl zip pcntl pdo_mysql pdo_pgsql bz2
|
||||
RUN install-php-extensions bcmath gd intl zip opcache pcntl posix pdo_mysql pdo_pgsql
|
||||
|
||||
RUN rm /usr/local/bin/install-php-extensions
|
||||
|
||||
@@ -42,7 +42,7 @@ RUN yarn config set network-timeout 300000 \
|
||||
FROM --platform=$TARGETOS/$TARGETARCH composer AS composerbuild
|
||||
|
||||
# Copy full code to optimize autoload
|
||||
COPY --exclude=docker/ . ./
|
||||
COPY --exclude=Caddyfile --exclude=docker/ . ./
|
||||
|
||||
RUN composer dump-autoload --optimize
|
||||
|
||||
@@ -54,7 +54,7 @@ FROM --platform=$TARGETOS/$TARGETARCH yarn AS yarnbuild
|
||||
WORKDIR /build
|
||||
|
||||
# Copy full code
|
||||
COPY --exclude=docker/ . ./
|
||||
COPY --exclude=Caddyfile --exclude=docker/ . ./
|
||||
COPY --from=composer /build .
|
||||
|
||||
RUN yarn run build
|
||||
@@ -68,33 +68,36 @@ WORKDIR /var/www/html
|
||||
|
||||
# Install additional required libraries
|
||||
RUN apk add --no-cache \
|
||||
# packages for running the panel
|
||||
caddy ca-certificates supervisor supercronic fcgi coreutils \
|
||||
# required for installing plugins. Pulled from https://github.com/pelican-dev/panel/pull/2034
|
||||
zip unzip 7zip bzip2-dev yarn git
|
||||
caddy ca-certificates supervisor supercronic fcgi coreutils
|
||||
|
||||
COPY --chown=root:www-data --chmod=770 --from=composerbuild /build .
|
||||
COPY --chown=root:www-data --chmod=770 --from=yarnbuild /build/public ./public
|
||||
COPY --chown=root:www-data --chmod=640 --from=composerbuild /build .
|
||||
COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public
|
||||
|
||||
# Create and remove directories
|
||||
RUN mkdir -p /pelican-data/storage /pelican-data/plugins /var/run/supervisord \
|
||||
&& rm -rf /var/www/html/plugins \
|
||||
# Symlinks for env, database, storage, and plugins
|
||||
&& ln -s /pelican-data/.env /var/www/html/.env \
|
||||
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
|
||||
&& ln -s /pelican-data/storage /var/www/html/public/storage \
|
||||
&& ln -s /pelican-data/storage /var/www/html/storage/app/public \
|
||||
&& ln -s /pelican-data/plugins /var/www/html \
|
||||
# Allow www-data write permissions where necessary
|
||||
&& chown -R www-data: /pelican-data .env ./storage ./plugins ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
|
||||
&& chmod -R 770 /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
|
||||
&& chown -R www-data: /usr/local/etc/php/ /usr/local/etc/php-fpm.d/
|
||||
# Set permissions
|
||||
# First ensure all files are owned by root and restrict www-data to read access
|
||||
RUN chown root:www-data ./ \
|
||||
&& chmod 750 ./ \
|
||||
# Files should not have execute set, but directories need it
|
||||
&& find ./ -type d -exec chmod 750 {} \; \
|
||||
# Create necessary directories
|
||||
&& mkdir -p /pelican-data/storage /pelican-data/plugins /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \
|
||||
# Symlinks for env, database, storage, and plugins
|
||||
&& ln -s /pelican-data/.env ./.env \
|
||||
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
|
||||
&& ln -sf /var/www/html/storage/app/public /var/www/html/public/storage \
|
||||
&& ln -s /pelican-data/storage/avatars /var/www/html/storage/app/public/avatars \
|
||||
&& ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
|
||||
&& ln -s /pelican-data/plugins /var/www/html/plugins \
|
||||
# Allow www-data write permissions where necessary
|
||||
&& chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
|
||||
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
|
||||
&& chown -R www-data: /usr/local/etc/php/
|
||||
|
||||
# Configure Supervisor
|
||||
COPY docker/supervisord.conf /etc/supervisord.conf
|
||||
COPY docker/Caddyfile /etc/caddy/Caddyfile
|
||||
# Add Laravel scheduler to crontab
|
||||
COPY docker/crontab /etc/crontabs/crontab
|
||||
COPY docker/crontab /etc/supercronic/crontab
|
||||
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
COPY docker/healthcheck.sh /healthcheck.sh
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Overrides;
|
||||
|
||||
use Illuminate\Foundation\Console\ConfigCacheCommand as BaseConfigCacheCommand;
|
||||
|
||||
class ConfigCacheCommand extends BaseConfigCacheCommand
|
||||
{
|
||||
/**
|
||||
* Prevent config from being cached
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->components->warn('Configuration caching has been disabled.');
|
||||
|
||||
$this->line(' Reason: This application uses dynamic plugins. Caching config');
|
||||
$this->line(' prevents /plugins/config/*.php files from being loaded correctly.');
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Overrides;
|
||||
|
||||
use Illuminate\Foundation\Console\OptimizeCommand as BaseOptimizeCommand;
|
||||
|
||||
class OptimizeCommand extends BaseOptimizeCommand
|
||||
{
|
||||
/**
|
||||
* Prevent config from being cached
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function getOptimizeTasks()
|
||||
{
|
||||
return array_except(parent::getOptimizeTasks(), 'config');
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ namespace App\Console\Commands\Plugin;
|
||||
use App\Enums\PluginStatus;
|
||||
use App\Models\Plugin;
|
||||
use App\Services\Helpers\PluginService;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class InstallPluginCommand extends Command
|
||||
@@ -32,12 +31,8 @@ class InstallPluginCommand extends Command
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$pluginService->installPlugin($plugin);
|
||||
$pluginService->installPlugin($plugin);
|
||||
|
||||
$this->info('Plugin installed and enabled.');
|
||||
} catch (Exception $exception) {
|
||||
$this->error('Could not install plugin: ' . $exception->getMessage());
|
||||
}
|
||||
$this->info('Plugin installed and enabled.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace App\Console\Commands\Plugin;
|
||||
use App\Enums\PluginStatus;
|
||||
use App\Models\Plugin;
|
||||
use App\Services\Helpers\PluginService;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class UninstallPluginCommand extends Command
|
||||
@@ -37,12 +36,8 @@ class UninstallPluginCommand extends Command
|
||||
$deleteFiles = $this->confirm('Do you also want to delete the plugin files?');
|
||||
}
|
||||
|
||||
try {
|
||||
$pluginService->uninstallPlugin($plugin, $deleteFiles);
|
||||
$pluginService->uninstallPlugin($plugin, $deleteFiles);
|
||||
|
||||
$this->info('Plugin uninstalled' . ($deleteFiles ? ' and files deleted' : '') . '.');
|
||||
} catch (Exception $exception) {
|
||||
$this->error('Could not uninstall plugin: ' . $exception->getMessage());
|
||||
}
|
||||
$this->info('Plugin uninstalled' . ($deleteFiles ? ' and files deleted' : '') . '.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace App\Console\Commands\Plugin;
|
||||
|
||||
use App\Models\Plugin;
|
||||
use App\Services\Helpers\PluginService;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class UpdatePluginCommand extends Command
|
||||
@@ -31,12 +30,8 @@ class UpdatePluginCommand extends Command
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$pluginService->updatePlugin($plugin);
|
||||
$pluginService->updatePlugin($plugin);
|
||||
|
||||
$this->info('Plugin updated.');
|
||||
} catch (Exception $exception) {
|
||||
$this->error('Could not update plugin: ' . $exception->getMessage());
|
||||
}
|
||||
$this->info('Plugin updated.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Middleware\ThrottleRequests;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* A basic resource throttler for individual servers. This is applied in addition
|
||||
* to existing rate limits and allows the code to slow down speedy users that might
|
||||
* be creating resources a little too quickly for comfort. This throttle generally
|
||||
* only applies to creation flows, and not general view/edit/delete flows.
|
||||
*/
|
||||
enum ResourceLimit: string
|
||||
{
|
||||
case Websocket = 'websocket';
|
||||
case AllocationCreate = 'allocation-create';
|
||||
case BackupRestore = 'backup-restore';
|
||||
case DatabaseCreate = 'database-create';
|
||||
case ScheduleCreate = 'schedule-create';
|
||||
case SubuserCreate = 'subuser-create';
|
||||
case FilePull = 'file-pull';
|
||||
|
||||
public function throttleKey(): string
|
||||
{
|
||||
return "api.client:server-resource:{$this->name}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a middleware that will throttle the specific resource by server. This
|
||||
* throttle applies to any user making changes to that resource on the specific
|
||||
* server, it is NOT per-user.
|
||||
*/
|
||||
public function middleware(): string
|
||||
{
|
||||
return ThrottleRequests::using($this->throttleKey());
|
||||
}
|
||||
|
||||
public function limit(): Limit
|
||||
{
|
||||
return match ($this) {
|
||||
self::Websocket => Limit::perMinute(5),
|
||||
self::BackupRestore => Limit::perMinutes(15, 3),
|
||||
self::DatabaseCreate => Limit::perMinute(2),
|
||||
self::SubuserCreate => Limit::perMinutes(15, 10),
|
||||
self::FilePull => Limit::perMinutes(10, 5),
|
||||
default => Limit::perMinute(2),
|
||||
};
|
||||
}
|
||||
|
||||
public static function boot(): void
|
||||
{
|
||||
foreach (self::cases() as $case) {
|
||||
RateLimiter::for($case->throttleKey(), function (Request $request) use ($case) {
|
||||
Assert::isInstanceOf($server = $request->route()->parameter('server'), Server::class);
|
||||
|
||||
return $case->limit()->by($server->uuid);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum StepPosition: string
|
||||
{
|
||||
case Before = 'before';
|
||||
case After = 'after';
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum TabPosition: string
|
||||
{
|
||||
case Before = 'before';
|
||||
case After = 'after';
|
||||
}
|
||||
@@ -2,10 +2,8 @@
|
||||
|
||||
namespace App\Extensions\OAuth;
|
||||
|
||||
use App\Models\User;
|
||||
use Filament\Schemas\Components\Component;
|
||||
use Filament\Schemas\Components\Wizard\Step;
|
||||
use Laravel\Socialite\Contracts\User as OAuthUser;
|
||||
|
||||
interface OAuthSchemaInterface
|
||||
{
|
||||
@@ -35,7 +33,7 @@ interface OAuthSchemaInterface
|
||||
|
||||
public function isEnabled(): bool;
|
||||
|
||||
public function shouldCreateMissingUser(OAuthUser $user): bool;
|
||||
public function shouldCreateMissingUsers(): bool;
|
||||
|
||||
public function shouldLinkMissingUser(User $user, OAuthUser $oauthUser): bool;
|
||||
public function shouldLinkMissingUsers(): bool;
|
||||
}
|
||||
|
||||
@@ -18,11 +18,11 @@ final class GithubSchema extends OAuthSchema
|
||||
public function getSetupSteps(): array
|
||||
{
|
||||
return array_merge([
|
||||
Step::make('Register new GitHub OAuth App')
|
||||
Step::make('Register new Github OAuth App')
|
||||
->schema([
|
||||
TextEntry::make('create_application')
|
||||
->hiddenLabel()
|
||||
->state(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://github.com/settings/developers" target="_blank">GitHub Developer Dashboard</x-filament::link>, go to <b>OAuth Apps</b> and click on <b>New OAuth App</b>.</p><p>Enter an <b>Application name</b> (e.g. your panel name), set <b>Homepage URL</b> to your panel url and enter the below url as <b>Authorization callback URL</b>.</p>'))),
|
||||
->state(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://github.com/settings/developers" target="_blank">Github Developer Dashboard</x-filament::link>, go to <b>OAuth Apps</b> and click on <b>New OAuth App</b>.</p><p>Enter an <b>Application name</b> (e.g. your panel name), set <b>Homepage URL</b> to your panel url and enter the below url as <b>Authorization callback URL</b>.</p>'))),
|
||||
TextInput::make('_noenv_callback')
|
||||
->label('Authorization callback URL')
|
||||
->dehydrated()
|
||||
|
||||
@@ -3,14 +3,12 @@
|
||||
namespace App\Extensions\OAuth\Schemas;
|
||||
|
||||
use App\Extensions\OAuth\OAuthSchemaInterface;
|
||||
use App\Models\User;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Schemas\Components\Component;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
use Filament\Schemas\Components\Wizard\Step;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Socialite\Contracts\User as OAuthUser;
|
||||
|
||||
abstract class OAuthSchema implements OAuthSchemaInterface
|
||||
{
|
||||
@@ -123,14 +121,14 @@ abstract class OAuthSchema implements OAuthSchemaInterface
|
||||
return env("OAUTH_{$id}_ENABLED", false);
|
||||
}
|
||||
|
||||
public function shouldCreateMissingUser(OAuthUser $user): bool
|
||||
public function shouldCreateMissingUsers(): bool
|
||||
{
|
||||
$id = Str::upper($this->getId());
|
||||
|
||||
return env("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS", false);
|
||||
}
|
||||
|
||||
public function shouldLinkMissingUser(User $user, OAuthUser $oauthUser): bool
|
||||
public function shouldLinkMissingUsers(): bool
|
||||
{
|
||||
$id = Str::upper($this->getId());
|
||||
|
||||
|
||||
@@ -56,9 +56,7 @@ class ListLogs extends BaseListLogs
|
||||
->modalHeading(trans('admin/log.actions.upload_logs'))
|
||||
->modalDescription(fn ($record) => trans('admin/log.actions.upload_logs_description', ['file' => $record['date'], 'url' => 'https://logs.pelican.dev']))
|
||||
->action(function ($record) {
|
||||
$prefix = config('filament-log-viewer.pattern.prefix', 'laravel-');
|
||||
$extension = config('filament-log-viewer.pattern.extension', '.log');
|
||||
$logPath = storage_path('logs/' . $prefix . $record['date'] . $extension);
|
||||
$logPath = storage_path('logs/' . $record['date']);
|
||||
|
||||
if (!file_exists($logPath)) {
|
||||
Notification::make()
|
||||
@@ -75,12 +73,18 @@ class ListLogs extends BaseListLogs
|
||||
$uploadLines = $totalLines <= 1000 ? $lines : array_slice($lines, -1000);
|
||||
$content = implode("\n", $uploadLines);
|
||||
|
||||
$logUrl = 'https://logs.pelican.dev';
|
||||
try {
|
||||
$response = Http::timeout(10)
|
||||
->asMultipart()
|
||||
->attach('c', $content)
|
||||
->attach('e', '14d')
|
||||
->post('https://logs.pelican.dev');
|
||||
$response = Http::timeout(10)->asMultipart()->post($logUrl, [
|
||||
[
|
||||
'name' => 'c',
|
||||
'contents' => $content,
|
||||
],
|
||||
[
|
||||
'name' => 'e',
|
||||
'contents' => '14d',
|
||||
],
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
Notification::make()
|
||||
|
||||
@@ -10,7 +10,6 @@ use App\Notifications\MailTested;
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
use App\Traits\Filament\CanCustomizeHeaderActions;
|
||||
use App\Traits\Filament\CanCustomizeHeaderWidgets;
|
||||
use App\Traits\Filament\CanCustomizeTabs;
|
||||
use BackedEnum;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
@@ -54,7 +53,6 @@ class Settings extends Page implements HasSchemas
|
||||
CanCustomizeHeaderActions::getHeaderActions insteadof InteractsWithHeaderActions;
|
||||
}
|
||||
use CanCustomizeHeaderWidgets;
|
||||
use CanCustomizeTabs;
|
||||
use EnvironmentWriterTrait;
|
||||
use InteractsWithForms;
|
||||
|
||||
@@ -98,7 +96,11 @@ class Settings extends Page implements HasSchemas
|
||||
return trans('admin/setting.title');
|
||||
}
|
||||
|
||||
/** @return array<Component> */
|
||||
/**
|
||||
* @return array<Component>
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function getFormSchema(): array
|
||||
{
|
||||
return [
|
||||
@@ -106,44 +108,34 @@ class Settings extends Page implements HasSchemas
|
||||
->columns()
|
||||
->persistTabInQueryString()
|
||||
->disabled(fn () => !user()?->can('update settings'))
|
||||
->tabs($this->getTabs()),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Tab[]
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function getDefaultTabs(): array
|
||||
{
|
||||
return [
|
||||
Tab::make('general')
|
||||
->label(trans('admin/setting.navigation.general'))
|
||||
->icon('tabler-home')
|
||||
->schema($this->generalSettings()),
|
||||
Tab::make('captcha')
|
||||
->label(trans('admin/setting.navigation.captcha'))
|
||||
->icon('tabler-shield')
|
||||
->schema($this->captchaSettings())
|
||||
->columns(1),
|
||||
Tab::make('mail')
|
||||
->label(trans('admin/setting.navigation.mail'))
|
||||
->icon('tabler-mail')
|
||||
->schema($this->mailSettings()),
|
||||
Tab::make('backup')
|
||||
->label(trans('admin/setting.navigation.backup'))
|
||||
->icon('tabler-box')
|
||||
->schema($this->backupSettings()),
|
||||
Tab::make('oauth')
|
||||
->label(trans('admin/setting.navigation.oauth'))
|
||||
->icon('tabler-brand-oauth')
|
||||
->schema($this->oauthSettings())
|
||||
->columns(1),
|
||||
Tab::make('misc')
|
||||
->label(trans('admin/setting.navigation.misc'))
|
||||
->icon('tabler-tool')
|
||||
->schema($this->miscSettings()),
|
||||
->tabs([
|
||||
Tab::make('general')
|
||||
->label(trans('admin/setting.navigation.general'))
|
||||
->icon('tabler-home')
|
||||
->schema($this->generalSettings()),
|
||||
Tab::make('captcha')
|
||||
->label(trans('admin/setting.navigation.captcha'))
|
||||
->icon('tabler-shield')
|
||||
->schema($this->captchaSettings())
|
||||
->columns(1),
|
||||
Tab::make('mail')
|
||||
->label(trans('admin/setting.navigation.mail'))
|
||||
->icon('tabler-mail')
|
||||
->schema($this->mailSettings()),
|
||||
Tab::make('backup')
|
||||
->label(trans('admin/setting.navigation.backup'))
|
||||
->icon('tabler-box')
|
||||
->schema($this->backupSettings()),
|
||||
Tab::make('oauth')
|
||||
->label(trans('admin/setting.navigation.oauth'))
|
||||
->icon('tabler-brand-oauth')
|
||||
->schema($this->oauthSettings())
|
||||
->columns(1),
|
||||
Tab::make('misc')
|
||||
->label(trans('admin/setting.navigation.misc'))
|
||||
->icon('tabler-tool')
|
||||
->schema($this->miscSettings()),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -264,7 +256,7 @@ class Settings extends Page implements HasSchemas
|
||||
->connectTimeout(3)
|
||||
->get('https://api.cloudflare.com/client/v4/ips');
|
||||
|
||||
if ($response->status() === 200) {
|
||||
if ($response->getStatusCode() === 200) {
|
||||
$result = $response->json('result');
|
||||
foreach (['ipv4_cidrs', 'ipv6_cidrs'] as $value) {
|
||||
$ips->push(...data_get($result, $value));
|
||||
@@ -831,6 +823,7 @@ class Settings extends Page implements HasSchemas
|
||||
|
||||
$this->writeToEnvironment($data);
|
||||
|
||||
Artisan::call('config:clear');
|
||||
Artisan::call('queue:restart');
|
||||
|
||||
$this->redirect($this->getUrl());
|
||||
|
||||
@@ -33,9 +33,7 @@ class ViewLogs extends BaseViewLog
|
||||
->modalHeading(trans('admin/log.actions.upload_logs'))
|
||||
->modalDescription(fn () => trans('admin/log.actions.upload_logs_description', ['file' => $this->resolveRecordDate(), 'url' => 'https://logs.pelican.dev']))
|
||||
->action(function () {
|
||||
$prefix = config('filament-log-viewer.pattern.prefix', 'laravel-');
|
||||
$extension = config('filament-log-viewer.pattern.extension', '.log');
|
||||
$logPath = storage_path('logs/' . $prefix . $this->resolveRecordDate() . $extension);
|
||||
$logPath = storage_path('logs/' . $this->resolveRecordDate());
|
||||
|
||||
if (!file_exists($logPath)) {
|
||||
Notification::make()
|
||||
@@ -52,12 +50,18 @@ class ViewLogs extends BaseViewLog
|
||||
$uploadLines = $totalLines <= 1000 ? $lines : array_slice($lines, -1000);
|
||||
$content = implode("\n", $uploadLines);
|
||||
|
||||
$logUrl = 'https://logs.pelican.dev';
|
||||
try {
|
||||
$response = Http::timeout(10)
|
||||
->asMultipart()
|
||||
->attach('c', $content)
|
||||
->attach('e', '14d')
|
||||
->post('https://logs.pelican.dev');
|
||||
$response = Http::timeout(10)->asMultipart()->post($logUrl, [
|
||||
[
|
||||
'name' => 'c',
|
||||
'contents' => $content,
|
||||
],
|
||||
[
|
||||
'name' => 'e',
|
||||
'contents' => '14d',
|
||||
],
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
Notification::make()
|
||||
|
||||
@@ -37,7 +37,7 @@ class DatabasesRelationManager extends RelationManager
|
||||
->formatStateUsing(fn (Database $record) => $record->remote === '%' ? trans('admin/databasehost.anywhere'). ' ( % )' : $record->remote),
|
||||
TextInput::make('max_connections')
|
||||
->label(trans('admin/databasehost.table.max_connections'))
|
||||
->formatStateUsing(fn (Database $record) => $record->max_connections ?: trans('admin/databasehost.unlimited')),
|
||||
->formatStateUsing(fn (Database $record) => $record->max_connections === 0 ? trans('admin/databasehost.unlimited') : $record->max_connections),
|
||||
TextInput::make('jdbc')
|
||||
->label(trans('admin/databasehost.table.connection_string'))
|
||||
->columnSpanFull()
|
||||
@@ -63,7 +63,7 @@ class DatabasesRelationManager extends RelationManager
|
||||
->url(fn (Database $database) => route('filament.admin.resources.servers.edit', ['record' => $database->server_id])),
|
||||
TextColumn::make('max_connections')
|
||||
->label(trans('admin/databasehost.table.max_connections'))
|
||||
->formatStateUsing(fn ($record) => $record->max_connections ?: trans('server/database.unlimited')),
|
||||
->formatStateUsing(fn ($record) => $record->max_connections === 0 ? trans('admin/databasehost.unlimited') : $record->max_connections),
|
||||
DateTimeColumn::make('created_at')
|
||||
->label(trans('admin/databasehost.table.created_at')),
|
||||
])
|
||||
|
||||
@@ -9,7 +9,7 @@ use App\Filament\Components\Forms\Fields\MonacoEditor;
|
||||
use App\Models\EggVariable;
|
||||
use App\Traits\Filament\CanCustomizeHeaderActions;
|
||||
use App\Traits\Filament\CanCustomizeHeaderWidgets;
|
||||
use App\Traits\Filament\CanCustomizeTabs;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\ActionGroup;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
@@ -37,7 +37,6 @@ class CreateEgg extends CreateRecord
|
||||
{
|
||||
use CanCustomizeHeaderActions;
|
||||
use CanCustomizeHeaderWidgets;
|
||||
use CanCustomizeTabs;
|
||||
|
||||
protected static string $resource = EggResource::class;
|
||||
|
||||
@@ -58,231 +57,227 @@ class CreateEgg extends CreateRecord
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Tabs::make()
|
||||
->tabs($this->getTabs())
|
||||
->columnSpanFull()
|
||||
->persistTabInQueryString(),
|
||||
]);
|
||||
}
|
||||
|
||||
/** @return Tab[] */
|
||||
protected function getDefaultTabs(): array
|
||||
{
|
||||
return [
|
||||
Tab::make('configuration')
|
||||
->label(trans('admin/egg.tabs.configuration'))
|
||||
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label(trans('admin/egg.name'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->helperText(trans('admin/egg.name_help')),
|
||||
TextInput::make('author')
|
||||
->label(trans('admin/egg.author'))
|
||||
->maxLength(255)
|
||||
->required()
|
||||
->email()
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->helperText(trans('admin/egg.author_help')),
|
||||
Textarea::make('description')
|
||||
->label(trans('admin/egg.description'))
|
||||
->rows(2)
|
||||
->columnSpanFull()
|
||||
->helperText(trans('admin/egg.description_help')),
|
||||
KeyValue::make('startup_commands')
|
||||
->label(trans('admin/egg.startup_commands'))
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->required()
|
||||
->addActionLabel(trans('admin/egg.add_startup'))
|
||||
->keyLabel(trans('admin/egg.startup_name'))
|
||||
->keyPlaceholder('Default')
|
||||
->valueLabel(trans('admin/egg.startup_command'))
|
||||
->valuePlaceholder('java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}')
|
||||
->helperText(trans('admin/egg.startup_help')),
|
||||
TagsInput::make('file_denylist')
|
||||
->label(trans('admin/egg.file_denylist'))
|
||||
->placeholder('denied-file.txt')
|
||||
->helperText(trans('admin/egg.file_denylist_help'))
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
TagsInput::make('features')
|
||||
->label(trans('admin/egg.features'))
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 1]),
|
||||
Toggle::make('force_outgoing_ip')
|
||||
->label(trans('admin/egg.force_ip'))
|
||||
->hintIcon('tabler-question-mark', trans('admin/egg.force_ip_help')),
|
||||
Hidden::make('script_is_privileged')
|
||||
->default(1),
|
||||
TagsInput::make('tags')
|
||||
->label(trans('admin/egg.tags'))
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
TextInput::make('update_url')
|
||||
->label(trans('admin/egg.update_url'))
|
||||
->hintIcon('tabler-question-mark', trans('admin/egg.update_url_help'))
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->url(),
|
||||
KeyValue::make('docker_images')
|
||||
->label(trans('admin/egg.docker_images'))
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->required()
|
||||
->addActionLabel(trans('admin/egg.add_image'))
|
||||
->keyLabel(trans('admin/egg.docker_name'))
|
||||
->keyPlaceholder('Java 21')
|
||||
->valueLabel(trans('admin/egg.docker_uri'))
|
||||
->valuePlaceholder('ghcr.io/pelican-eggs/yolks:java_21')
|
||||
->helperText(trans('admin/egg.docker_help')),
|
||||
]),
|
||||
Tab::make('process_management')
|
||||
->label(trans('admin/egg.tabs.process_management'))
|
||||
->columns()
|
||||
->schema([
|
||||
CopyFrom::make('copy_process_from')
|
||||
->process(),
|
||||
TextInput::make('config_stop')
|
||||
->label(trans('admin/egg.stop_command'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->helperText(trans('admin/egg.stop_command_help')),
|
||||
Textarea::make('config_startup')->rows(10)->json()
|
||||
->label(trans('admin/egg.start_config'))
|
||||
->default('{}')
|
||||
->helperText(trans('admin/egg.start_config_help')),
|
||||
Textarea::make('config_files')->rows(10)->json()
|
||||
->label(trans('admin/egg.config_files'))
|
||||
->default('{}')
|
||||
->helperText(trans('admin/egg.config_files_help')),
|
||||
Textarea::make('config_logs')->rows(10)->json()
|
||||
->label(trans('admin/egg.log_config'))
|
||||
->default('{}')
|
||||
->helperText(trans('admin/egg.log_config_help')),
|
||||
]),
|
||||
Tab::make('egg_variables')
|
||||
->label(trans('admin/egg.tabs.egg_variables'))
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Repeater::make('variables')
|
||||
->hiddenLabel()
|
||||
->addActionLabel(trans('admin/egg.add_new_variable'))
|
||||
->grid()
|
||||
->relationship('variables')
|
||||
->reorderable()->orderColumn()
|
||||
->collapsible()->collapsed()
|
||||
->columnSpan(2)
|
||||
->defaultItems(0)
|
||||
->itemLabel(fn (array $state) => $state['name'])
|
||||
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
|
||||
$data['default_value'] ??= '';
|
||||
$data['description'] ??= '';
|
||||
$data['rules'] ??= [];
|
||||
$data['user_viewable'] ??= '';
|
||||
$data['user_editable'] ??= '';
|
||||
|
||||
return $data;
|
||||
})
|
||||
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
|
||||
$data['default_value'] ??= '';
|
||||
$data['description'] ??= '';
|
||||
$data['rules'] ??= [];
|
||||
$data['user_viewable'] ??= '';
|
||||
$data['user_editable'] ??= '';
|
||||
|
||||
return $data;
|
||||
})
|
||||
Tabs::make()->tabs([
|
||||
Tab::make('configuration')
|
||||
->label(trans('admin/egg.tabs.configuration'))
|
||||
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label(trans('admin/egg.name'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->helperText(trans('admin/egg.name_help')),
|
||||
TextInput::make('author')
|
||||
->label(trans('admin/egg.author'))
|
||||
->maxLength(255)
|
||||
->required()
|
||||
->email()
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->helperText(trans('admin/egg.author_help')),
|
||||
Textarea::make('description')
|
||||
->label(trans('admin/egg.description'))
|
||||
->rows(2)
|
||||
->columnSpanFull()
|
||||
->helperText(trans('admin/egg.description_help')),
|
||||
KeyValue::make('startup_commands')
|
||||
->label(trans('admin/egg.startup_commands'))
|
||||
->live()
|
||||
->debounce(750)
|
||||
->maxLength(255)
|
||||
->columnSpanFull()
|
||||
->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString()))
|
||||
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
|
||||
->validationMessages([
|
||||
'unique' => trans('admin/egg.error_unique'),
|
||||
])
|
||||
->required(),
|
||||
Textarea::make('description')->label(trans('admin/egg.description'))->columnSpanFull(),
|
||||
TextInput::make('env_variable')
|
||||
->label(trans('admin/egg.environment_variable'))
|
||||
->required()
|
||||
->addActionLabel(trans('admin/egg.add_startup'))
|
||||
->keyLabel(trans('admin/egg.startup_name'))
|
||||
->keyPlaceholder('Default')
|
||||
->valueLabel(trans('admin/egg.startup_command'))
|
||||
->valuePlaceholder('java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}')
|
||||
->helperText(trans('admin/egg.startup_help')),
|
||||
TagsInput::make('file_denylist')
|
||||
->label(trans('admin/egg.file_denylist'))
|
||||
->placeholder('denied-file.txt')
|
||||
->helperText(trans('admin/egg.file_denylist_help'))
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
TagsInput::make('features')
|
||||
->label(trans('admin/egg.features'))
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 1]),
|
||||
Toggle::make('force_outgoing_ip')
|
||||
->label(trans('admin/egg.force_ip'))
|
||||
->hintIcon('tabler-question-mark', trans('admin/egg.force_ip_help')),
|
||||
Hidden::make('script_is_privileged')
|
||||
->default(1),
|
||||
TagsInput::make('tags')
|
||||
->label(trans('admin/egg.tags'))
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
TextInput::make('update_url')
|
||||
->label(trans('admin/egg.update_url'))
|
||||
->hintIcon('tabler-question-mark', trans('admin/egg.update_url_help'))
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->url(),
|
||||
KeyValue::make('docker_images')
|
||||
->label(trans('admin/egg.docker_images'))
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->required()
|
||||
->addActionLabel(trans('admin/egg.add_image'))
|
||||
->keyLabel(trans('admin/egg.docker_name'))
|
||||
->keyPlaceholder('Java 21')
|
||||
->valueLabel(trans('admin/egg.docker_uri'))
|
||||
->valuePlaceholder('ghcr.io/pelican-eggs/yolks:java_21')
|
||||
->helperText(trans('admin/egg.docker_help')),
|
||||
]),
|
||||
|
||||
Tab::make('process_management')
|
||||
->label(trans('admin/egg.tabs.process_management'))
|
||||
->columns()
|
||||
->schema([
|
||||
CopyFrom::make('copy_process_from')
|
||||
->process(),
|
||||
TextInput::make('config_stop')
|
||||
->label(trans('admin/egg.stop_command'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->prefix('{{')
|
||||
->suffix('}}')
|
||||
->hintIcon('tabler-code', fn ($state) => "{{{$state}}}")
|
||||
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
|
||||
->rules(EggVariable::getRulesForField('env_variable'))
|
||||
->validationMessages([
|
||||
'unique' => trans('admin/egg.error_unique'),
|
||||
'required' => trans('admin/egg.error_required'),
|
||||
'*' => trans('admin/egg.error_reserved'),
|
||||
])
|
||||
->required(),
|
||||
TextInput::make('default_value')->label(trans('admin/egg.default_value')),
|
||||
Fieldset::make(trans('admin/egg.user_permissions'))
|
||||
->helperText(trans('admin/egg.stop_command_help')),
|
||||
Textarea::make('config_startup')->rows(10)->json()
|
||||
->label(trans('admin/egg.start_config'))
|
||||
->default('{}')
|
||||
->helperText(trans('admin/egg.start_config_help')),
|
||||
Textarea::make('config_files')->rows(10)->json()
|
||||
->label(trans('admin/egg.config_files'))
|
||||
->default('{}')
|
||||
->helperText(trans('admin/egg.config_files_help')),
|
||||
Textarea::make('config_logs')->rows(10)->json()
|
||||
->label(trans('admin/egg.log_config'))
|
||||
->default('{}')
|
||||
->helperText(trans('admin/egg.log_config_help')),
|
||||
]),
|
||||
Tab::make('egg_variables')
|
||||
->label(trans('admin/egg.tabs.egg_variables'))
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Repeater::make('variables')
|
||||
->hiddenLabel()
|
||||
->addActionLabel(trans('admin/egg.add_new_variable'))
|
||||
->grid()
|
||||
->relationship('variables')
|
||||
->reorderable()->orderColumn()
|
||||
->collapsible()->collapsed()
|
||||
->columnSpan(2)
|
||||
->defaultItems(0)
|
||||
->itemLabel(fn (array $state) => $state['name'])
|
||||
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
|
||||
$data['default_value'] ??= '';
|
||||
$data['description'] ??= '';
|
||||
$data['rules'] ??= [];
|
||||
$data['user_viewable'] ??= '';
|
||||
$data['user_editable'] ??= '';
|
||||
|
||||
return $data;
|
||||
})
|
||||
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
|
||||
$data['default_value'] ??= '';
|
||||
$data['description'] ??= '';
|
||||
$data['rules'] ??= [];
|
||||
$data['user_viewable'] ??= '';
|
||||
$data['user_editable'] ??= '';
|
||||
|
||||
return $data;
|
||||
})
|
||||
->schema([
|
||||
Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')),
|
||||
Checkbox::make('user_editable')->label(trans('admin/egg.editable')),
|
||||
]),
|
||||
TagsInput::make('rules')
|
||||
->label(trans('admin/egg.rules'))
|
||||
->columnSpanFull()
|
||||
->reorderable()
|
||||
->suggestions([
|
||||
'required',
|
||||
'nullable',
|
||||
'string',
|
||||
'integer',
|
||||
'numeric',
|
||||
'boolean',
|
||||
'alpha',
|
||||
'alpha_dash',
|
||||
'alpha_num',
|
||||
'url',
|
||||
'email',
|
||||
'regex:',
|
||||
'min:',
|
||||
'max:',
|
||||
'between:',
|
||||
'between:1024,65535',
|
||||
'in:',
|
||||
'in:true,false',
|
||||
TextInput::make('name')
|
||||
->label(trans('admin/egg.name'))
|
||||
->live()
|
||||
->debounce(750)
|
||||
->maxLength(255)
|
||||
->columnSpanFull()
|
||||
->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString()))
|
||||
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
|
||||
->validationMessages([
|
||||
'unique' => trans('admin/egg.error_unique'),
|
||||
])
|
||||
->required(),
|
||||
Textarea::make('description')->label(trans('admin/egg.description'))->columnSpanFull(),
|
||||
TextInput::make('env_variable')
|
||||
->label(trans('admin/egg.environment_variable'))
|
||||
->maxLength(255)
|
||||
->prefix('{{')
|
||||
->suffix('}}')
|
||||
->hintIcon('tabler-code', fn ($state) => "{{{$state}}}")
|
||||
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
|
||||
->rules(EggVariable::getRulesForField('env_variable'))
|
||||
->validationMessages([
|
||||
'unique' => trans('admin/egg.error_unique'),
|
||||
'required' => trans('admin/egg.error_required'),
|
||||
'*' => trans('admin/egg.error_reserved'),
|
||||
])
|
||||
->required(),
|
||||
TextInput::make('default_value')->label(trans('admin/egg.default_value')),
|
||||
Fieldset::make(trans('admin/egg.user_permissions'))
|
||||
->schema([
|
||||
Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')),
|
||||
Checkbox::make('user_editable')->label(trans('admin/egg.editable')),
|
||||
]),
|
||||
TagsInput::make('rules')
|
||||
->label(trans('admin/egg.rules'))
|
||||
->columnSpanFull()
|
||||
->reorderable()
|
||||
->suggestions([
|
||||
'required',
|
||||
'nullable',
|
||||
'string',
|
||||
'integer',
|
||||
'numeric',
|
||||
'boolean',
|
||||
'alpha',
|
||||
'alpha_dash',
|
||||
'alpha_num',
|
||||
'url',
|
||||
'email',
|
||||
'regex:',
|
||||
'min:',
|
||||
'max:',
|
||||
'between:',
|
||||
'between:1024,65535',
|
||||
'in:',
|
||||
'in:true,false',
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('install_script')
|
||||
->label(trans('admin/egg.tabs.install_script'))
|
||||
->columns(3)
|
||||
->schema([
|
||||
CopyFrom::make('copy_script_from')
|
||||
->script(),
|
||||
TextInput::make('script_container')
|
||||
->label(trans('admin/egg.script_container'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->default('ghcr.io/pelican-eggs/installers:debian'),
|
||||
Select::make('script_entry')
|
||||
->label(trans('admin/egg.script_entry'))
|
||||
->selectablePlaceholder(false)
|
||||
->default('bash')
|
||||
->options([
|
||||
'bash' => 'bash',
|
||||
'ash' => 'ash',
|
||||
'/bin/bash' => '/bin/bash',
|
||||
])
|
||||
->required(),
|
||||
MonacoEditor::make('script_install')
|
||||
->label(trans('admin/egg.script_install'))
|
||||
->language(EditorLanguages::shell)
|
||||
->columnSpanFull()
|
||||
->lazy(),
|
||||
]),
|
||||
];
|
||||
Tab::make('install_script')
|
||||
->label(trans('admin/egg.tabs.install_script'))
|
||||
->columns(3)
|
||||
->schema([
|
||||
CopyFrom::make('copy_script_from')
|
||||
->script(),
|
||||
TextInput::make('script_container')
|
||||
->label(trans('admin/egg.script_container'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->default('ghcr.io/pelican-eggs/installers:debian'),
|
||||
Select::make('script_entry')
|
||||
->label(trans('admin/egg.script_entry'))
|
||||
->selectablePlaceholder(false)
|
||||
->default('bash')
|
||||
->options([
|
||||
'bash' => 'bash',
|
||||
'ash' => 'ash',
|
||||
'/bin/bash' => '/bin/bash',
|
||||
])
|
||||
->required(),
|
||||
MonacoEditor::make('script_install')
|
||||
->label(trans('admin/egg.script_install'))
|
||||
->language(EditorLanguages::shell)
|
||||
->columnSpanFull()
|
||||
->lazy(),
|
||||
]),
|
||||
|
||||
])->columnSpanFull()->persistTabInQueryString(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function handleRecordCreation(array $data): Model
|
||||
|
||||
@@ -12,7 +12,6 @@ use App\Models\Egg;
|
||||
use App\Models\EggVariable;
|
||||
use App\Traits\Filament\CanCustomizeHeaderActions;
|
||||
use App\Traits\Filament\CanCustomizeHeaderWidgets;
|
||||
use App\Traits\Filament\CanCustomizeTabs;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\ActionGroup;
|
||||
@@ -48,398 +47,391 @@ class EditEgg extends EditRecord
|
||||
{
|
||||
use CanCustomizeHeaderActions;
|
||||
use CanCustomizeHeaderWidgets;
|
||||
use CanCustomizeTabs;
|
||||
|
||||
protected static string $resource = EggResource::class;
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Tabs::make()
|
||||
->tabs($this->getTabs())
|
||||
->columnSpanFull()
|
||||
->persistTabInQueryString(),
|
||||
]);
|
||||
}
|
||||
|
||||
/** @return Tab[] */
|
||||
protected function getDefaultTabs(): array
|
||||
{
|
||||
return [
|
||||
Tab::make('configuration')
|
||||
->label(trans('admin/egg.tabs.configuration'))
|
||||
->columns(['default' => 2, 'sm' => 2, 'md' => 4, 'lg' => 6])
|
||||
->icon('tabler-egg')
|
||||
->schema([
|
||||
Grid::make(2)
|
||||
->columnSpan(1)
|
||||
Tabs::make()->tabs([
|
||||
Tab::make('configuration')
|
||||
->label(trans('admin/egg.tabs.configuration'))
|
||||
->columns(['default' => 2, 'sm' => 2, 'md' => 4, 'lg' => 6])
|
||||
->icon('tabler-egg')
|
||||
->schema([
|
||||
Image::make('', '')
|
||||
->hidden(fn ($record) => !$record->image)
|
||||
->url(fn ($record) => $record->image)
|
||||
->alt('')
|
||||
->alignJustify()
|
||||
->imageSize(150)
|
||||
->columnSpanFull(),
|
||||
Flex::make([
|
||||
Action::make('uploadImage')
|
||||
->iconButton()
|
||||
->iconSize(IconSize::Large)
|
||||
->icon('tabler-photo-up')
|
||||
->modal()
|
||||
->modalHeading('')
|
||||
->modalSubmitActionLabel(trans('admin/egg.import.import_image'))
|
||||
->schema([
|
||||
Tabs::make()
|
||||
->contained(false)
|
||||
->tabs([
|
||||
Tab::make(trans('admin/egg.import.url'))
|
||||
->schema([
|
||||
Hidden::make('imageUrl'),
|
||||
Hidden::make('imageExtension'),
|
||||
TextInput::make('image_url')
|
||||
->label(trans('admin/egg.import.image_url'))
|
||||
->reactive()
|
||||
->autocomplete(false)
|
||||
->debounce(500)
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
if (!$state) {
|
||||
$set('image_url_error', null);
|
||||
$set('imageUrl', null);
|
||||
$set('imageExtension', null);
|
||||
Grid::make(2)
|
||||
->columnSpan(1)
|
||||
->schema([
|
||||
Image::make('', '')
|
||||
->hidden(fn ($record) => !$record->image)
|
||||
->url(fn ($record) => $record->image)
|
||||
->alt('')
|
||||
->alignJustify()
|
||||
->imageSize(150)
|
||||
->columnSpanFull(),
|
||||
Flex::make([
|
||||
Action::make('uploadImage')
|
||||
->iconButton()
|
||||
->iconSize(IconSize::Large)
|
||||
->icon('tabler-photo-up')
|
||||
->modal()
|
||||
->modalHeading('')
|
||||
->modalSubmitActionLabel(trans('admin/egg.import.import_image'))
|
||||
->schema([
|
||||
Tabs::make()
|
||||
->contained(false)
|
||||
->tabs([
|
||||
Tab::make(trans('admin/egg.import.url'))
|
||||
->schema([
|
||||
Hidden::make('imageUrl'),
|
||||
Hidden::make('imageExtension'),
|
||||
TextInput::make('image_url')
|
||||
->label(trans('admin/egg.import.image_url'))
|
||||
->reactive()
|
||||
->autocomplete(false)
|
||||
->debounce(500)
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
if (!$state) {
|
||||
$set('image_url_error', null);
|
||||
$set('imageUrl', null);
|
||||
$set('imageExtension', null);
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!filter_var($state, FILTER_VALIDATE_URL)) {
|
||||
throw new Exception(trans('admin/egg.import.invalid_url'));
|
||||
}
|
||||
try {
|
||||
if (!filter_var($state, FILTER_VALIDATE_URL)) {
|
||||
throw new Exception(trans('admin/egg.import.invalid_url'));
|
||||
}
|
||||
|
||||
$extension = strtolower(pathinfo(parse_url($state, PHP_URL_PATH), PATHINFO_EXTENSION));
|
||||
$extension = strtolower(pathinfo(parse_url($state, PHP_URL_PATH), PATHINFO_EXTENSION));
|
||||
|
||||
if (!array_key_exists($extension, Egg::IMAGE_FORMATS)) {
|
||||
throw new Exception(trans('admin/egg.import.unsupported_format', ['format' => implode(', ', array_keys(Egg::IMAGE_FORMATS))]));
|
||||
}
|
||||
if (!array_key_exists($extension, Egg::IMAGE_FORMATS)) {
|
||||
throw new Exception(trans('admin/egg.import.unsupported_format', ['format' => implode(', ', array_keys(Egg::IMAGE_FORMATS))]));
|
||||
}
|
||||
|
||||
$host = parse_url($state, PHP_URL_HOST);
|
||||
$ip = gethostbyname($host);
|
||||
$host = parse_url($state, PHP_URL_HOST);
|
||||
$ip = gethostbyname($host);
|
||||
|
||||
if (
|
||||
filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false
|
||||
) {
|
||||
throw new Exception(trans('admin/egg.import.no_local_ip'));
|
||||
}
|
||||
if (
|
||||
filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false
|
||||
) {
|
||||
throw new Exception(trans('admin/egg.import.no_local_ip'));
|
||||
}
|
||||
|
||||
$set('imageUrl', $state);
|
||||
$set('imageExtension', $extension);
|
||||
$set('image_url_error', null);
|
||||
$set('imageUrl', $state);
|
||||
$set('imageExtension', $extension);
|
||||
$set('image_url_error', null);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$set('image_url_error', $e->getMessage());
|
||||
$set('imageUrl', null);
|
||||
$set('imageExtension', null);
|
||||
}
|
||||
}),
|
||||
TextEntry::make('image_url_error')
|
||||
->hiddenLabel()
|
||||
->visible(fn ($get) => $get('image_url_error') !== null)
|
||||
->afterStateHydrated(fn ($set, $get) => $get('image_url_error')),
|
||||
Image::make(fn (Get $get) => $get('image_url'), '')
|
||||
->imageSize(150)
|
||||
->visible(fn ($get) => $get('image_url') && !$get('image_url_error'))
|
||||
->alignCenter(),
|
||||
} catch (Exception $e) {
|
||||
$set('image_url_error', $e->getMessage());
|
||||
$set('imageUrl', null);
|
||||
$set('imageExtension', null);
|
||||
}
|
||||
}),
|
||||
TextEntry::make('image_url_error')
|
||||
->hiddenLabel()
|
||||
->visible(fn ($get) => $get('image_url_error') !== null)
|
||||
->afterStateHydrated(fn ($set, $get) => $get('image_url_error')),
|
||||
Image::make(fn (Get $get) => $get('image_url'), '')
|
||||
->imageSize(150)
|
||||
->visible(fn ($get) => $get('image_url') && !$get('image_url_error'))
|
||||
->alignCenter(),
|
||||
]),
|
||||
Tab::make(trans('admin/egg.import.file'))
|
||||
->schema([
|
||||
FileUpload::make('image')
|
||||
->hiddenLabel()
|
||||
->previewable()
|
||||
->openable(false)
|
||||
->downloadable(false)
|
||||
->maxSize(256)
|
||||
->maxFiles(1)
|
||||
->columnSpanFull()
|
||||
->alignCenter()
|
||||
->imageEditor()
|
||||
->image()
|
||||
->disk('public')
|
||||
->directory(Egg::ICON_STORAGE_PATH)
|
||||
->acceptedFileTypes([
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/webp',
|
||||
'image/svg+xml',
|
||||
])
|
||||
->getUploadedFileNameForStorageUsing(function (TemporaryUploadedFile $file, $record) {
|
||||
return $record->uuid . '.' . $file->getClientOriginalExtension();
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
Tab::make(trans('admin/egg.import.file'))
|
||||
->schema([
|
||||
FileUpload::make('image')
|
||||
->hiddenLabel()
|
||||
->previewable()
|
||||
->openable(false)
|
||||
->downloadable(false)
|
||||
->maxSize(256)
|
||||
->maxFiles(1)
|
||||
->columnSpanFull()
|
||||
->alignCenter()
|
||||
->imageEditor()
|
||||
->image()
|
||||
->disk('public')
|
||||
->directory(Egg::ICON_STORAGE_PATH)
|
||||
->acceptedFileTypes([
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/webp',
|
||||
'image/svg+xml',
|
||||
])
|
||||
->getUploadedFileNameForStorageUsing(function (TemporaryUploadedFile $file, $record) {
|
||||
return $record->uuid . '.' . $file->getClientOriginalExtension();
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
->action(function (array $data, $record): void {
|
||||
if (!empty($data['imageUrl']) && !empty($data['imageExtension'])) {
|
||||
$this->saveImageFromUrl($data['imageUrl'], $data['imageExtension'], $record);
|
||||
])
|
||||
->action(function (array $data, $record): void {
|
||||
if (!empty($data['imageUrl']) && !empty($data['imageExtension'])) {
|
||||
$this->saveImageFromUrl($data['imageUrl'], $data['imageExtension'], $record);
|
||||
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.import.image_updated'))
|
||||
->success()
|
||||
->send();
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.import.image_updated'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!empty($data['image'])) {
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.import.image_updated'))
|
||||
->success()
|
||||
->send();
|
||||
if (!empty($data['image'])) {
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.import.image_updated'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (empty($data['imageUrl']) && empty($data['image'])) {
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.import.no_image'))
|
||||
->warning()
|
||||
->send();
|
||||
}
|
||||
}),
|
||||
Action::make('delete_image')
|
||||
->visible(fn ($record) => $record->image)
|
||||
->hiddenLabel()
|
||||
->icon('tabler-trash')
|
||||
->iconButton()
|
||||
->iconSize(IconSize::Large)
|
||||
->color('danger')
|
||||
->action(function ($record) {
|
||||
foreach (array_keys(Egg::IMAGE_FORMATS) as $ext) {
|
||||
$path = Egg::ICON_STORAGE_PATH . "/$record->uuid.$ext";
|
||||
if (Storage::disk('public')->exists($path)) {
|
||||
Storage::disk('public')->delete($path);
|
||||
}
|
||||
}
|
||||
if (empty($data['imageUrl']) && empty($data['image'])) {
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.import.no_image'))
|
||||
->warning()
|
||||
->send();
|
||||
}
|
||||
}),
|
||||
Action::make('delete_image')
|
||||
->visible(fn ($record) => $record->image)
|
||||
->hiddenLabel()
|
||||
->icon('tabler-trash')
|
||||
->iconButton()
|
||||
->iconSize(IconSize::Large)
|
||||
->color('danger')
|
||||
->action(function ($record) {
|
||||
foreach (array_keys(Egg::IMAGE_FORMATS) as $ext) {
|
||||
$path = Egg::ICON_STORAGE_PATH . "/$record->uuid.$ext";
|
||||
if (Storage::disk('public')->exists($path)) {
|
||||
Storage::disk('public')->delete($path);
|
||||
}
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.import.image_deleted'))
|
||||
->success()
|
||||
->send();
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.import.image_deleted'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
$record->refresh();
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
TextInput::make('name')
|
||||
->label(trans('admin/egg.name'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 3, 'lg' => 2])
|
||||
->helperText(trans('admin/egg.name_help')),
|
||||
Textarea::make('description')
|
||||
->label(trans('admin/egg.description'))
|
||||
->rows(3)
|
||||
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 4, 'lg' => 3])
|
||||
->helperText(trans('admin/egg.description_help')),
|
||||
TextInput::make('id')
|
||||
->label(trans('admin/egg.egg_id'))
|
||||
->columnSpan(1)
|
||||
->disabled(),
|
||||
TextInput::make('uuid')
|
||||
->label(trans('admin/egg.egg_uuid'))
|
||||
->disabled()
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
|
||||
->helperText(trans('admin/egg.uuid_help')),
|
||||
TextInput::make('author')
|
||||
->label(trans('admin/egg.author'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->email()
|
||||
->disabled()
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
|
||||
->helperText(trans('admin/egg.author_help_edit')),
|
||||
Toggle::make('force_outgoing_ip')
|
||||
->inline(false)
|
||||
->label(trans('admin/egg.force_ip'))
|
||||
->columnSpan(1)
|
||||
->hintIcon('tabler-question-mark', trans('admin/egg.force_ip_help')),
|
||||
KeyValue::make('startup_commands')
|
||||
->label(trans('admin/egg.startup_commands'))
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->required()
|
||||
->addActionLabel(trans('admin/egg.add_startup'))
|
||||
->keyLabel(trans('admin/egg.startup_name'))
|
||||
->valueLabel(trans('admin/egg.startup_command'))
|
||||
->helperText(trans('admin/egg.startup_help')),
|
||||
TagsInput::make('file_denylist')
|
||||
->label(trans('admin/egg.file_denylist'))
|
||||
->placeholder('denied-file.txt')
|
||||
->helperText(trans('admin/egg.file_denylist_help'))
|
||||
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
|
||||
TextInput::make('update_url')
|
||||
->label(trans('admin/egg.update_url'))
|
||||
->url()
|
||||
->hintIcon('tabler-question-mark', trans('admin/egg.update_url_help'))
|
||||
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
|
||||
TagsInput::make('features')
|
||||
->label(trans('admin/egg.features'))
|
||||
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
|
||||
Hidden::make('script_is_privileged')
|
||||
->helperText('The docker images available to servers using this egg.'),
|
||||
TagsInput::make('tags')
|
||||
->label(trans('admin/egg.tags'))
|
||||
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
|
||||
KeyValue::make('docker_images')
|
||||
->label(trans('admin/egg.docker_images'))
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->required()
|
||||
->addActionLabel(trans('admin/egg.add_image'))
|
||||
->keyLabel(trans('admin/egg.docker_name'))
|
||||
->valueLabel(trans('admin/egg.docker_uri'))
|
||||
->helperText(trans('admin/egg.docker_help')),
|
||||
]),
|
||||
Tab::make('process_management')
|
||||
->label(trans('admin/egg.tabs.process_management'))
|
||||
->columns()
|
||||
->icon('tabler-server-cog')
|
||||
->schema([
|
||||
CopyFrom::make('copy_process_from')
|
||||
->process(),
|
||||
TextInput::make('config_stop')
|
||||
->label(trans('admin/egg.stop_command'))
|
||||
->maxLength(255)
|
||||
->helperText(trans('admin/egg.stop_command_help')),
|
||||
Textarea::make('config_startup')->rows(10)->json()
|
||||
->label(trans('admin/egg.start_config'))
|
||||
->helperText(trans('admin/egg.start_config_help')),
|
||||
Textarea::make('config_files')->rows(10)->json()
|
||||
->label(trans('admin/egg.config_files'))
|
||||
->helperText(trans('admin/egg.config_files_help')),
|
||||
Textarea::make('config_logs')->rows(10)->json()
|
||||
->label(trans('admin/egg.log_config'))
|
||||
->helperText(trans('admin/egg.log_config_help')),
|
||||
]),
|
||||
Tab::make('egg_variables')
|
||||
->label(trans('admin/egg.tabs.egg_variables'))
|
||||
->columnSpanFull()
|
||||
->icon('tabler-variable')
|
||||
->schema([
|
||||
Repeater::make('variables')
|
||||
->hiddenLabel()
|
||||
->grid()
|
||||
->relationship('variables')
|
||||
->reorderable()
|
||||
->collapsible()->collapsed()
|
||||
->orderColumn()
|
||||
->addActionLabel(trans('admin/egg.add_new_variable'))
|
||||
->itemLabel(fn (array $state) => $state['name'])
|
||||
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
|
||||
$data['default_value'] ??= '';
|
||||
$data['description'] ??= '';
|
||||
$data['rules'] ??= [];
|
||||
$data['user_viewable'] ??= '';
|
||||
$data['user_editable'] ??= '';
|
||||
|
||||
return $data;
|
||||
})
|
||||
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
|
||||
$data['default_value'] ??= '';
|
||||
$data['description'] ??= '';
|
||||
$data['rules'] ??= [];
|
||||
$data['user_viewable'] ??= '';
|
||||
$data['user_editable'] ??= '';
|
||||
|
||||
return $data;
|
||||
})
|
||||
->schema([
|
||||
$record->refresh();
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
TextInput::make('name')
|
||||
->label(trans('admin/egg.name'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 3, 'lg' => 2])
|
||||
->helperText(trans('admin/egg.name_help')),
|
||||
Textarea::make('description')
|
||||
->label(trans('admin/egg.description'))
|
||||
->rows(3)
|
||||
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 4, 'lg' => 3])
|
||||
->helperText(trans('admin/egg.description_help')),
|
||||
TextInput::make('id')
|
||||
->label(trans('admin/egg.egg_id'))
|
||||
->columnSpan(1)
|
||||
->disabled(),
|
||||
TextInput::make('uuid')
|
||||
->label(trans('admin/egg.egg_uuid'))
|
||||
->disabled()
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
|
||||
->helperText(trans('admin/egg.uuid_help')),
|
||||
TextInput::make('author')
|
||||
->label(trans('admin/egg.author'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->email()
|
||||
->disabled()
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
|
||||
->helperText(trans('admin/egg.author_help_edit')),
|
||||
Toggle::make('force_outgoing_ip')
|
||||
->inline(false)
|
||||
->label(trans('admin/egg.force_ip'))
|
||||
->columnSpan(1)
|
||||
->hintIcon('tabler-question-mark', trans('admin/egg.force_ip_help')),
|
||||
KeyValue::make('startup_commands')
|
||||
->label(trans('admin/egg.startup_commands'))
|
||||
->live()
|
||||
->debounce(750)
|
||||
->maxLength(255)
|
||||
->columnSpanFull()
|
||||
->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString()))
|
||||
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
|
||||
->validationMessages([
|
||||
'unique' => trans('admin/egg.error_unique'),
|
||||
])
|
||||
->required(),
|
||||
Textarea::make('description')->label(trans('admin/egg.description'))->columnSpanFull(),
|
||||
TextInput::make('env_variable')
|
||||
->label(trans('admin/egg.environment_variable'))
|
||||
->maxLength(255)
|
||||
->prefix('{{')
|
||||
->suffix('}}')
|
||||
->hintIcon('tabler-code', fn ($state) => "{{{$state}}}")
|
||||
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
|
||||
->rules(EggVariable::getRulesForField('env_variable'))
|
||||
->validationMessages([
|
||||
'unique' => trans('admin/egg.error_unique'),
|
||||
'required' => trans('admin/egg.error_required'),
|
||||
'*' => trans('admin/egg.error_reserved'),
|
||||
])
|
||||
->required(),
|
||||
TextInput::make('default_value')->label(trans('admin/egg.default_value')),
|
||||
Fieldset::make(trans('admin/egg.user_permissions'))
|
||||
->schema([
|
||||
Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')),
|
||||
Checkbox::make('user_editable')->label(trans('admin/egg.editable')),
|
||||
]),
|
||||
TagsInput::make('rules')
|
||||
->label(trans('admin/egg.rules'))
|
||||
->required()
|
||||
->addActionLabel(trans('admin/egg.add_startup'))
|
||||
->keyLabel(trans('admin/egg.startup_name'))
|
||||
->valueLabel(trans('admin/egg.startup_command'))
|
||||
->helperText(trans('admin/egg.startup_help')),
|
||||
TagsInput::make('file_denylist')
|
||||
->label(trans('admin/egg.file_denylist'))
|
||||
->placeholder('denied-file.txt')
|
||||
->helperText(trans('admin/egg.file_denylist_help'))
|
||||
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
|
||||
TextInput::make('update_url')
|
||||
->label(trans('admin/egg.update_url'))
|
||||
->url()
|
||||
->hintIcon('tabler-question-mark', trans('admin/egg.update_url_help'))
|
||||
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
|
||||
TagsInput::make('features')
|
||||
->label(trans('admin/egg.features'))
|
||||
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
|
||||
Hidden::make('script_is_privileged')
|
||||
->helperText('The docker images available to servers using this egg.'),
|
||||
TagsInput::make('tags')
|
||||
->label(trans('admin/egg.tags'))
|
||||
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
|
||||
KeyValue::make('docker_images')
|
||||
->label(trans('admin/egg.docker_images'))
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->required()
|
||||
->addActionLabel(trans('admin/egg.add_image'))
|
||||
->keyLabel(trans('admin/egg.docker_name'))
|
||||
->valueLabel(trans('admin/egg.docker_uri'))
|
||||
->helperText(trans('admin/egg.docker_help')),
|
||||
]),
|
||||
Tab::make('process_management')
|
||||
->label(trans('admin/egg.tabs.process_management'))
|
||||
->columns()
|
||||
->icon('tabler-server-cog')
|
||||
->schema([
|
||||
CopyFrom::make('copy_process_from')
|
||||
->process(),
|
||||
TextInput::make('config_stop')
|
||||
->label(trans('admin/egg.stop_command'))
|
||||
->maxLength(255)
|
||||
->helperText(trans('admin/egg.stop_command_help')),
|
||||
Textarea::make('config_startup')->rows(10)->json()
|
||||
->label(trans('admin/egg.start_config'))
|
||||
->helperText(trans('admin/egg.start_config_help')),
|
||||
Textarea::make('config_files')->rows(10)->json()
|
||||
->label(trans('admin/egg.config_files'))
|
||||
->helperText(trans('admin/egg.config_files_help')),
|
||||
Textarea::make('config_logs')->rows(10)->json()
|
||||
->label(trans('admin/egg.log_config'))
|
||||
->helperText(trans('admin/egg.log_config_help')),
|
||||
]),
|
||||
Tab::make('egg_variables')
|
||||
->label(trans('admin/egg.tabs.egg_variables'))
|
||||
->columnSpanFull()
|
||||
->icon('tabler-variable')
|
||||
->schema([
|
||||
Repeater::make('variables')
|
||||
->hiddenLabel()
|
||||
->grid()
|
||||
->relationship('variables')
|
||||
->reorderable()
|
||||
->suggestions([
|
||||
'required',
|
||||
'nullable',
|
||||
'string',
|
||||
'integer',
|
||||
'numeric',
|
||||
'boolean',
|
||||
'alpha',
|
||||
'alpha_dash',
|
||||
'alpha_num',
|
||||
'url',
|
||||
'email',
|
||||
'regex:',
|
||||
'min:',
|
||||
'max:',
|
||||
'between:',
|
||||
'between:1024,65535',
|
||||
'in:',
|
||||
'in:true,false',
|
||||
->collapsible()->collapsed()
|
||||
->orderColumn()
|
||||
->addActionLabel(trans('admin/egg.add_new_variable'))
|
||||
->itemLabel(fn (array $state) => $state['name'])
|
||||
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
|
||||
$data['default_value'] ??= '';
|
||||
$data['description'] ??= '';
|
||||
$data['rules'] ??= [];
|
||||
$data['user_viewable'] ??= '';
|
||||
$data['user_editable'] ??= '';
|
||||
|
||||
return $data;
|
||||
})
|
||||
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
|
||||
$data['default_value'] ??= '';
|
||||
$data['description'] ??= '';
|
||||
$data['rules'] ??= [];
|
||||
$data['user_viewable'] ??= '';
|
||||
$data['user_editable'] ??= '';
|
||||
|
||||
return $data;
|
||||
})
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label(trans('admin/egg.name'))
|
||||
->live()
|
||||
->debounce(750)
|
||||
->maxLength(255)
|
||||
->columnSpanFull()
|
||||
->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString()))
|
||||
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
|
||||
->validationMessages([
|
||||
'unique' => trans('admin/egg.error_unique'),
|
||||
])
|
||||
->required(),
|
||||
Textarea::make('description')->label(trans('admin/egg.description'))->columnSpanFull(),
|
||||
TextInput::make('env_variable')
|
||||
->label(trans('admin/egg.environment_variable'))
|
||||
->maxLength(255)
|
||||
->prefix('{{')
|
||||
->suffix('}}')
|
||||
->hintIcon('tabler-code', fn ($state) => "{{{$state}}}")
|
||||
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
|
||||
->rules(EggVariable::getRulesForField('env_variable'))
|
||||
->validationMessages([
|
||||
'unique' => trans('admin/egg.error_unique'),
|
||||
'required' => trans('admin/egg.error_required'),
|
||||
'*' => trans('admin/egg.error_reserved'),
|
||||
])
|
||||
->required(),
|
||||
TextInput::make('default_value')->label(trans('admin/egg.default_value')),
|
||||
Fieldset::make(trans('admin/egg.user_permissions'))
|
||||
->schema([
|
||||
Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')),
|
||||
Checkbox::make('user_editable')->label(trans('admin/egg.editable')),
|
||||
]),
|
||||
TagsInput::make('rules')
|
||||
->label(trans('admin/egg.rules'))
|
||||
->columnSpanFull()
|
||||
->reorderable()
|
||||
->suggestions([
|
||||
'required',
|
||||
'nullable',
|
||||
'string',
|
||||
'integer',
|
||||
'numeric',
|
||||
'boolean',
|
||||
'alpha',
|
||||
'alpha_dash',
|
||||
'alpha_num',
|
||||
'url',
|
||||
'email',
|
||||
'regex:',
|
||||
'min:',
|
||||
'max:',
|
||||
'between:',
|
||||
'between:1024,65535',
|
||||
'in:',
|
||||
'in:true,false',
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('install_script')
|
||||
->label(trans('admin/egg.tabs.install_script'))
|
||||
->columns(3)
|
||||
->icon('tabler-file-download')
|
||||
->schema([
|
||||
CopyFrom::make('copy_script_from')
|
||||
->script(),
|
||||
TextInput::make('script_container')
|
||||
->label(trans('admin/egg.script_container'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->placeholder('ghcr.io/pelican-eggs/installers:debian'),
|
||||
Select::make('script_entry')
|
||||
->label(trans('admin/egg.script_entry'))
|
||||
->selectablePlaceholder(false)
|
||||
->options([
|
||||
'bash' => 'bash',
|
||||
'ash' => 'ash',
|
||||
'/bin/bash' => '/bin/bash',
|
||||
])
|
||||
->required(),
|
||||
MonacoEditor::make('script_install')
|
||||
->hiddenLabel()
|
||||
->language(EditorLanguages::shell)
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
];
|
||||
Tab::make('install_script')
|
||||
->label(trans('admin/egg.tabs.install_script'))
|
||||
->columns(3)
|
||||
->icon('tabler-file-download')
|
||||
->schema([
|
||||
CopyFrom::make('copy_script_from')
|
||||
->script(),
|
||||
TextInput::make('script_container')
|
||||
->label(trans('admin/egg.script_container'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->placeholder('ghcr.io/pelican-eggs/installers:debian'),
|
||||
Select::make('script_entry')
|
||||
->label(trans('admin/egg.script_entry'))
|
||||
->selectablePlaceholder(false)
|
||||
->options([
|
||||
'bash' => 'bash',
|
||||
'ash' => 'ash',
|
||||
'/bin/bash' => '/bin/bash',
|
||||
])
|
||||
->required(),
|
||||
MonacoEditor::make('script_install')
|
||||
->hiddenLabel()
|
||||
->language(EditorLanguages::shell)
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
])->columnSpanFull()->persistTabInQueryString(),
|
||||
]);
|
||||
}
|
||||
|
||||
/** @return array<Action|ActionGroup> */
|
||||
@@ -449,16 +441,6 @@ class EditEgg extends EditRecord
|
||||
DeleteAction::make()
|
||||
->disabled(fn (Egg $egg): bool => $egg->servers()->count() > 0)
|
||||
->label(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? trans('filament-actions::delete.single.label') : trans('admin/egg.in_use'))
|
||||
->successNotification(fn (Egg $egg) => Notification::make()
|
||||
->success()
|
||||
->title(trans('admin/egg.delete_success'))
|
||||
->body(trans('admin/egg.deleted', ['egg' => $egg->name]))
|
||||
)
|
||||
->failureNotification(fn (Egg $egg) => Notification::make()
|
||||
->danger()
|
||||
->title(trans('admin/egg.delete_failed'))
|
||||
->body(trans('admin/egg.could_not_delete', ['egg' => $egg->name]))
|
||||
)
|
||||
->iconButton()->iconSize(IconSize::ExtraLarge),
|
||||
ExportEggAction::make(),
|
||||
ImportEggAction::make()
|
||||
|
||||
@@ -18,13 +18,11 @@ use Filament\Actions\CreateAction;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ReplicateAction;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Support\Enums\IconSize;
|
||||
use Filament\Tables\Columns\ImageColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ListEggs extends ListRecords
|
||||
@@ -90,45 +88,15 @@ class ListEggs extends ListRecords
|
||||
])
|
||||
->groupedBulkActions([
|
||||
DeleteBulkAction::make()
|
||||
->before(function (Collection &$records) {
|
||||
$eggsWithServers = $records->filter(fn (Egg $egg) => $egg->servers_count > 0);
|
||||
|
||||
if ($eggsWithServers->isNotEmpty()) {
|
||||
$eggNames = $eggsWithServers->map(fn (Egg $egg) => sprintf('%s (%d server%s)', $egg->name, $egg->servers_count, $egg->servers_count > 1 ? 's' : ''))
|
||||
->join(', ');
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title(trans('admin/egg.cannot_delete', ['count' => $eggsWithServers->count()]))
|
||||
->body(trans('admin/egg.eggs_have_servers', ['eggs' => $eggNames]))
|
||||
->send();
|
||||
}
|
||||
|
||||
$records = $records->filter(fn (Egg $egg) => $egg->servers_count <= 0);
|
||||
|
||||
if ($records->isEmpty()) {
|
||||
$this->halt();
|
||||
}
|
||||
}),
|
||||
->before(fn (&$records) => $records = $records->filter(function ($egg) {
|
||||
/** @var Egg $egg */
|
||||
return $egg->servers_count <= 0;
|
||||
})),
|
||||
UpdateEggBulkAction::make()
|
||||
->before(function (Collection &$records) {
|
||||
$eggsWithoutUpdateUrl = $records->filter(fn (Egg $egg) => $egg->update_url === null);
|
||||
|
||||
if ($eggsWithoutUpdateUrl->isNotEmpty()) {
|
||||
$eggNames = $eggsWithoutUpdateUrl->pluck('name')->join(', ');
|
||||
|
||||
Notification::make()
|
||||
->warning()
|
||||
->title(trans('admin/egg.cannot_update', ['count' => $eggsWithoutUpdateUrl->count()]))
|
||||
->body(trans('admin/egg.no_update_url', ['eggs' => $eggNames]))
|
||||
->send();
|
||||
}
|
||||
|
||||
$records = $records->filter(fn (Egg $egg) => $egg->update_url !== null);
|
||||
|
||||
if ($records->isEmpty()) {
|
||||
$this->halt();
|
||||
}
|
||||
}),
|
||||
->before(fn (&$records) => $records = $records->filter(function ($egg) {
|
||||
/** @var Egg $egg */
|
||||
return cache()->get("eggs.$egg->uuid.update", false);
|
||||
})),
|
||||
])
|
||||
->emptyStateIcon('tabler-eggs')
|
||||
->emptyStateDescription('')
|
||||
|
||||
@@ -21,6 +21,7 @@ use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Resources\Pages\PageRegistration;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Components\Group;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\StateCasts\BooleanStateCast;
|
||||
use Filament\Schemas\Schema;
|
||||
@@ -150,32 +151,30 @@ class MountResource extends Resource
|
||||
->label(trans('admin/mount.description'))
|
||||
->helperText(trans('admin/mount.description_help'))
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'lg' => 2,
|
||||
])
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'xl' => 2,
|
||||
])->columnSpan(1)->columns([
|
||||
'default' => 1,
|
||||
'lg' => 2,
|
||||
]),
|
||||
Group::make()->schema([
|
||||
Section::make()->schema([
|
||||
Select::make('eggs')->multiple()
|
||||
->label(trans('admin/mount.eggs'))
|
||||
// Selecting only non-json fields to prevent Postgres from choking on DISTINCT JSON columns
|
||||
->relationship('eggs', 'name', fn (Builder $query) => $query->select(['eggs.id', 'eggs.name']))
|
||||
->preload(),
|
||||
Select::make('nodes')->multiple()
|
||||
->label(trans('admin/mount.nodes'))
|
||||
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', user()?->accessibleNodes()->pluck('id')))
|
||||
->searchable(['name', 'fqdn'])
|
||||
->preload(),
|
||||
]),
|
||||
Section::make()->schema([
|
||||
Select::make('eggs')
|
||||
->multiple()
|
||||
->label(trans('admin/mount.eggs'))
|
||||
// Selecting only non-json fields to prevent Postgres from choking on DISTINCT JSON columns
|
||||
->relationship('eggs', 'name', fn (Builder $query) => $query->select(['eggs.id', 'eggs.name']))
|
||||
->preload(),
|
||||
Select::make('nodes')
|
||||
->multiple()
|
||||
->label(trans('admin/mount.nodes'))
|
||||
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', user()?->accessibleNodes()->pluck('id')))
|
||||
->searchable(['name', 'fqdn'])
|
||||
->preload(),
|
||||
])->columns([
|
||||
'default' => 1,
|
||||
'lg' => 2,
|
||||
]),
|
||||
])->columns([
|
||||
'default' => 1,
|
||||
'lg' => 3,
|
||||
'lg' => 2,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use App\Filament\Admin\Resources\Nodes\NodeResource;
|
||||
use App\Models\Node;
|
||||
use App\Traits\Filament\CanCustomizeHeaderActions;
|
||||
use App\Traits\Filament\CanCustomizeHeaderWidgets;
|
||||
use App\Traits\Filament\CanCustomizeSteps;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
@@ -27,397 +27,394 @@ class CreateNode extends CreateRecord
|
||||
{
|
||||
use CanCustomizeHeaderActions;
|
||||
use CanCustomizeHeaderWidgets;
|
||||
use CanCustomizeSteps;
|
||||
|
||||
protected static string $resource = NodeResource::class;
|
||||
|
||||
protected static bool $canCreateAnother = false;
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Wizard::make($this->getSteps())
|
||||
->columnSpanFull()
|
||||
->nextAction(fn (Action $action) => $action->iconButton()->iconSize(IconSize::ExtraLarge)->icon('tabler-arrow-right'))
|
||||
Wizard::make([
|
||||
Step::make('basic')
|
||||
->label(trans('admin/node.tabs.basic_settings'))
|
||||
->icon('tabler-server')
|
||||
->columnSpanFull()
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'lg' => 4,
|
||||
])
|
||||
->schema([
|
||||
TextInput::make('fqdn')
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
->autofocus()
|
||||
->live(debounce: 1500)
|
||||
->rules(Node::getRulesForField('fqdn'))
|
||||
->prohibited(fn ($state) => is_ip($state) && request()->isSecure())
|
||||
->label(fn ($state) => is_ip($state) ? trans('admin/node.ip_address') : trans('admin/node.domain'))
|
||||
->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
|
||||
->helperText(function ($state) {
|
||||
if (is_ip($state)) {
|
||||
if (request()->isSecure()) {
|
||||
return trans('admin/node.fqdn_help');
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
return trans('admin/node.error');
|
||||
})
|
||||
->hintColor('danger')
|
||||
->hint(function ($state) {
|
||||
if (is_ip($state) && request()->isSecure()) {
|
||||
return trans('admin/node.ssl_ip');
|
||||
}
|
||||
|
||||
return '';
|
||||
})
|
||||
->afterStateUpdated(function (Set $set, ?string $state) {
|
||||
$set('dns', null);
|
||||
$set('ip', null);
|
||||
|
||||
[$subdomain] = str($state)->explode('.', 2);
|
||||
if (!is_numeric($subdomain)) {
|
||||
$set('name', $subdomain);
|
||||
}
|
||||
|
||||
if (!$state || is_ip($state)) {
|
||||
$set('dns', null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$ip = get_ip_from_hostname($state);
|
||||
if ($ip) {
|
||||
$set('dns', true);
|
||||
|
||||
$set('ip', $ip);
|
||||
} else {
|
||||
$set('dns', false);
|
||||
}
|
||||
})
|
||||
->maxLength(255),
|
||||
|
||||
TextInput::make('ip')
|
||||
->disabled()
|
||||
->hidden(),
|
||||
|
||||
ToggleButtons::make('dns')
|
||||
->label(trans('admin/node.dns'))
|
||||
->helperText(trans('admin/node.dns_help'))
|
||||
->disabled()
|
||||
->inline()
|
||||
->default(null)
|
||||
->hint(fn (Get $get) => $get('ip'))
|
||||
->hintColor('success')
|
||||
->options([
|
||||
true => trans('admin/node.valid'),
|
||||
false => trans('admin/node.invalid'),
|
||||
])
|
||||
->colors([
|
||||
true => 'success',
|
||||
false => 'danger',
|
||||
])
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
]),
|
||||
|
||||
TextInput::make('daemon_connect')
|
||||
->columnSpan(1)
|
||||
->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port'))
|
||||
->helperText(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port_help') : trans('admin/node.port_help'))
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(8080)
|
||||
->required()
|
||||
->integer(),
|
||||
|
||||
TextInput::make('name')
|
||||
->label(trans('admin/node.display_name'))
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 2,
|
||||
])
|
||||
->required()
|
||||
->maxLength(100),
|
||||
|
||||
Hidden::make('scheme')
|
||||
->default(fn () => request()->isSecure() ? 'https' : 'http'),
|
||||
|
||||
Hidden::make('behind_proxy')
|
||||
->default(false),
|
||||
|
||||
ToggleButtons::make('connection')
|
||||
->label(trans('admin/node.ssl'))
|
||||
->columnSpan(1)
|
||||
->inline()
|
||||
->helperText(function (Get $get) {
|
||||
if (request()->isSecure()) {
|
||||
return new HtmlString(trans('admin/node.panel_on_ssl'));
|
||||
}
|
||||
|
||||
if (is_ip($get('fqdn'))) {
|
||||
return trans('admin/node.ssl_help');
|
||||
}
|
||||
|
||||
return '';
|
||||
})
|
||||
->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure())
|
||||
->options([
|
||||
'http' => 'HTTP',
|
||||
'https' => 'HTTPS (SSL)',
|
||||
'https_proxy' => 'HTTPS with (reverse) proxy',
|
||||
])
|
||||
->colors([
|
||||
'http' => 'warning',
|
||||
'https' => 'success',
|
||||
'https_proxy' => 'success',
|
||||
])
|
||||
->icons([
|
||||
'http' => 'tabler-lock-open-off',
|
||||
'https' => 'tabler-lock',
|
||||
'https_proxy' => 'tabler-shield-lock',
|
||||
])
|
||||
->default(fn () => request()->isSecure() ? 'https' : 'http')
|
||||
->live()
|
||||
->dehydrated(false)
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
$set('scheme', $state === 'http' ? 'http' : 'https');
|
||||
$set('behind_proxy', $state === 'https_proxy');
|
||||
|
||||
$set('daemon_connect', $state === 'https_proxy' ? 443 : 8080);
|
||||
$set('daemon_listen', 8080);
|
||||
}),
|
||||
|
||||
TextInput::make('daemon_listen')
|
||||
->columnSpan(1)
|
||||
->label(trans('admin/node.listen_port'))
|
||||
->helperText(trans('admin/node.listen_port_help'))
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(8080)
|
||||
->required()
|
||||
->integer()
|
||||
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
|
||||
]),
|
||||
Step::make('advanced')
|
||||
->label(trans('admin/node.tabs.advanced_settings'))
|
||||
->icon('tabler-server-cog')
|
||||
->columnSpanFull()
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'lg' => 4,
|
||||
])
|
||||
->schema([
|
||||
ToggleButtons::make('maintenance_mode')
|
||||
->label(trans('admin/node.maintenance_mode'))->inline()
|
||||
->columnSpan(1)
|
||||
->default(false)
|
||||
->hintIcon('tabler-question-mark', trans('admin/node.maintenance_mode_help'))
|
||||
->options([
|
||||
true => trans('admin/node.enabled'),
|
||||
false => trans('admin/node.disabled'),
|
||||
])
|
||||
->colors([
|
||||
true => 'danger',
|
||||
false => 'success',
|
||||
]),
|
||||
ToggleButtons::make('public')
|
||||
->default(true)
|
||||
->columnSpan(1)
|
||||
->label(trans('admin/node.use_for_deploy'))->inline()
|
||||
->options([
|
||||
true => trans('admin/node.yes'),
|
||||
false => trans('admin/node.no'),
|
||||
])
|
||||
->colors([
|
||||
true => 'success',
|
||||
false => 'danger',
|
||||
]),
|
||||
TagsInput::make('tags')
|
||||
->label(trans('admin/node.tags'))
|
||||
->columnSpan(2),
|
||||
TextInput::make('upload_size')
|
||||
->label(trans('admin/node.upload_limit'))
|
||||
->helperText(trans('admin/node.upload_limit_help.0'))
|
||||
->hintIcon('tabler-question-mark', trans('admin/node.upload_limit_help.1'))
|
||||
->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(trans('admin/node.sftp_port'))
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(2022)
|
||||
->required()
|
||||
->integer(),
|
||||
TextInput::make('daemon_sftp_alias')
|
||||
->columnSpan(2)
|
||||
->label(trans('admin/node.sftp_alias'))
|
||||
->helperText(trans('admin/node.sftp_alias_help')),
|
||||
Grid::make()
|
||||
->columns(6)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
ToggleButtons::make('unlimited_mem')
|
||||
->dehydrated()
|
||||
->label(trans('admin/node.memory'))->inlineLabel()->inline()
|
||||
->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 => trans('admin/node.unlimited'),
|
||||
false => trans('admin/node.limited'),
|
||||
])
|
||||
->colors([
|
||||
true => 'primary',
|
||||
false => 'warning',
|
||||
])
|
||||
->columnSpan(2),
|
||||
TextInput::make('memory')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Get $get) => $get('unlimited_mem'))
|
||||
->label(trans('admin/node.memory_limit'))->inlineLabel()
|
||||
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->default(0)
|
||||
->required(),
|
||||
TextInput::make('memory_overallocate')
|
||||
->dehydratedWhenHidden()
|
||||
->label(trans('admin/node.overallocate'))->inlineLabel()
|
||||
->hidden(fn (Get $get) => $get('unlimited_mem'))
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->minValue(-1)
|
||||
->maxValue(100)
|
||||
->default(0)
|
||||
->suffix('%')
|
||||
->required(),
|
||||
]),
|
||||
Grid::make()
|
||||
->columns(6)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
ToggleButtons::make('unlimited_disk')
|
||||
->dehydrated()
|
||||
->label(trans('admin/node.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 => trans('admin/node.unlimited'),
|
||||
false => trans('admin/node.limited'),
|
||||
])
|
||||
->colors([
|
||||
true => 'primary',
|
||||
false => 'warning',
|
||||
])
|
||||
->columnSpan(2),
|
||||
TextInput::make('disk')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Get $get) => $get('unlimited_disk'))
|
||||
->label(trans('admin/node.disk_limit'))->inlineLabel()
|
||||
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->default(0)
|
||||
->required(),
|
||||
TextInput::make('disk_overallocate')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Get $get) => $get('unlimited_disk'))
|
||||
->label(trans('admin/node.overallocate'))->inlineLabel()
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->minValue(-1)
|
||||
->maxValue(100)
|
||||
->default(0)
|
||||
->suffix('%')
|
||||
->required(),
|
||||
]),
|
||||
Grid::make()
|
||||
->columns(6)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
ToggleButtons::make('unlimited_cpu')
|
||||
->dehydrated()
|
||||
->label(trans('admin/node.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 => trans('admin/node.unlimited'),
|
||||
false => trans('admin/node.limited'),
|
||||
])
|
||||
->colors([
|
||||
true => 'primary',
|
||||
false => 'warning',
|
||||
])
|
||||
->columnSpan(2),
|
||||
TextInput::make('cpu')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Get $get) => $get('unlimited_cpu'))
|
||||
->label(trans('admin/node.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(trans('admin/node.overallocate'))->inlineLabel()
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->default(0)
|
||||
->minValue(-1)
|
||||
->maxValue(100)
|
||||
->suffix('%')
|
||||
->required(),
|
||||
]),
|
||||
]),
|
||||
])->columnSpanFull()
|
||||
->nextAction(fn (Action $action) => $action->label(trans('admin/node.next_step'))->iconButton()->iconSize(IconSize::ExtraLarge)->icon('tabler-arrow-right'))
|
||||
->previousAction(fn (Action $action) => $action->iconButton()->iconSize(IconSize::ExtraLarge)->icon('tabler-arrow-left'))
|
||||
->submitAction(new HtmlString(Blade::render(<<<'BLADE'
|
||||
<x-filament::icon-button
|
||||
type="submit"
|
||||
iconSize="xl"
|
||||
icon="tabler-file-plus"
|
||||
>
|
||||
{{ trans('admin/node.create') }}
|
||||
</x-filament::icon-button>
|
||||
BLADE))),
|
||||
<x-filament::icon-button
|
||||
type="submit"
|
||||
iconSize="xl"
|
||||
icon="tabler-file-plus"
|
||||
>
|
||||
{{ trans('admin/node.create') }}
|
||||
</x-filament::icon-button>
|
||||
BLADE))),
|
||||
]);
|
||||
}
|
||||
|
||||
/** @return Step[] */
|
||||
protected function getDefaultSteps(): array
|
||||
{
|
||||
return [
|
||||
Step::make('basic')
|
||||
->label(trans('admin/node.tabs.basic_settings'))
|
||||
->icon('tabler-server')
|
||||
->columnSpanFull()
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'lg' => 4,
|
||||
])
|
||||
->schema([
|
||||
TextInput::make('fqdn')
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
->autofocus()
|
||||
->live(debounce: 1500)
|
||||
->rules(Node::getRulesForField('fqdn'))
|
||||
->prohibited(fn ($state) => is_ip($state) && request()->isSecure())
|
||||
->label(fn ($state) => is_ip($state) ? trans('admin/node.ip_address') : trans('admin/node.domain'))
|
||||
->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
|
||||
->helperText(function ($state) {
|
||||
if (is_ip($state)) {
|
||||
if (request()->isSecure()) {
|
||||
return trans('admin/node.fqdn_help');
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
return trans('admin/node.error');
|
||||
})
|
||||
->hintColor('danger')
|
||||
->hint(function ($state) {
|
||||
if (is_ip($state) && request()->isSecure()) {
|
||||
return trans('admin/node.ssl_ip');
|
||||
}
|
||||
|
||||
return '';
|
||||
})
|
||||
->afterStateUpdated(function (Set $set, ?string $state) {
|
||||
$set('dns', null);
|
||||
$set('ip', null);
|
||||
|
||||
[$subdomain] = str($state)->explode('.', 2);
|
||||
if (!is_numeric($subdomain)) {
|
||||
$set('name', $subdomain);
|
||||
}
|
||||
|
||||
if (!$state || is_ip($state)) {
|
||||
$set('dns', null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$ip = get_ip_from_hostname($state);
|
||||
if ($ip) {
|
||||
$set('dns', true);
|
||||
|
||||
$set('ip', $ip);
|
||||
} else {
|
||||
$set('dns', false);
|
||||
}
|
||||
})
|
||||
->maxLength(255),
|
||||
|
||||
TextInput::make('ip')
|
||||
->disabled()
|
||||
->hidden(),
|
||||
|
||||
ToggleButtons::make('dns')
|
||||
->label(trans('admin/node.dns'))
|
||||
->helperText(trans('admin/node.dns_help'))
|
||||
->disabled()
|
||||
->inline()
|
||||
->default(null)
|
||||
->hint(fn (Get $get) => $get('ip'))
|
||||
->hintColor('success')
|
||||
->options([
|
||||
true => trans('admin/node.valid'),
|
||||
false => trans('admin/node.invalid'),
|
||||
])
|
||||
->colors([
|
||||
true => 'success',
|
||||
false => 'danger',
|
||||
])
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
]),
|
||||
|
||||
TextInput::make('daemon_connect')
|
||||
->columnSpan(1)
|
||||
->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port'))
|
||||
->helperText(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port_help') : trans('admin/node.port_help'))
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(8080)
|
||||
->required()
|
||||
->integer(),
|
||||
|
||||
TextInput::make('name')
|
||||
->label(trans('admin/node.display_name'))
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 2,
|
||||
])
|
||||
->required()
|
||||
->maxLength(100),
|
||||
|
||||
Hidden::make('scheme')
|
||||
->default(fn () => request()->isSecure() ? 'https' : 'http'),
|
||||
|
||||
Hidden::make('behind_proxy')
|
||||
->default(false),
|
||||
|
||||
ToggleButtons::make('connection')
|
||||
->label(trans('admin/node.ssl'))
|
||||
->columnSpan(1)
|
||||
->inline()
|
||||
->helperText(function (Get $get) {
|
||||
if (request()->isSecure()) {
|
||||
return new HtmlString(trans('admin/node.panel_on_ssl'));
|
||||
}
|
||||
|
||||
if (is_ip($get('fqdn'))) {
|
||||
return trans('admin/node.ssl_help');
|
||||
}
|
||||
|
||||
return '';
|
||||
})
|
||||
->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure())
|
||||
->options([
|
||||
'http' => 'HTTP',
|
||||
'https' => 'HTTPS (SSL)',
|
||||
'https_proxy' => 'HTTPS with (reverse) proxy',
|
||||
])
|
||||
->colors([
|
||||
'http' => 'warning',
|
||||
'https' => 'success',
|
||||
'https_proxy' => 'success',
|
||||
])
|
||||
->icons([
|
||||
'http' => 'tabler-lock-open-off',
|
||||
'https' => 'tabler-lock',
|
||||
'https_proxy' => 'tabler-shield-lock',
|
||||
])
|
||||
->default(fn () => request()->isSecure() ? 'https' : 'http')
|
||||
->live()
|
||||
->dehydrated(false)
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
$set('scheme', $state === 'http' ? 'http' : 'https');
|
||||
$set('behind_proxy', $state === 'https_proxy');
|
||||
|
||||
$set('daemon_connect', $state === 'https_proxy' ? 443 : 8080);
|
||||
$set('daemon_listen', 8080);
|
||||
}),
|
||||
|
||||
TextInput::make('daemon_listen')
|
||||
->columnSpan(1)
|
||||
->label(trans('admin/node.listen_port'))
|
||||
->helperText(trans('admin/node.listen_port_help'))
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(8080)
|
||||
->required()
|
||||
->integer()
|
||||
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
|
||||
]),
|
||||
Step::make('advanced')
|
||||
->label(trans('admin/node.tabs.advanced_settings'))
|
||||
->icon('tabler-server-cog')
|
||||
->columnSpanFull()
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'lg' => 4,
|
||||
])
|
||||
->schema([
|
||||
ToggleButtons::make('maintenance_mode')
|
||||
->label(trans('admin/node.maintenance_mode'))->inline()
|
||||
->columnSpan(1)
|
||||
->default(false)
|
||||
->hintIcon('tabler-question-mark', trans('admin/node.maintenance_mode_help'))
|
||||
->options([
|
||||
true => trans('admin/node.enabled'),
|
||||
false => trans('admin/node.disabled'),
|
||||
])
|
||||
->colors([
|
||||
true => 'danger',
|
||||
false => 'success',
|
||||
]),
|
||||
ToggleButtons::make('public')
|
||||
->default(true)
|
||||
->columnSpan(1)
|
||||
->label(trans('admin/node.use_for_deploy'))->inline()
|
||||
->options([
|
||||
true => trans('admin/node.yes'),
|
||||
false => trans('admin/node.no'),
|
||||
])
|
||||
->colors([
|
||||
true => 'success',
|
||||
false => 'danger',
|
||||
]),
|
||||
TagsInput::make('tags')
|
||||
->label(trans('admin/node.tags'))
|
||||
->columnSpan(2),
|
||||
TextInput::make('upload_size')
|
||||
->label(trans('admin/node.upload_limit'))
|
||||
->hintIcon('tabler-question-mark', trans('admin/node.upload_limit_help'))
|
||||
->columnSpan(1)
|
||||
->numeric()->required()
|
||||
->default(256)
|
||||
->minValue(1)
|
||||
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'),
|
||||
TextInput::make('daemon_sftp')
|
||||
->columnSpan(1)
|
||||
->label(trans('admin/node.sftp_port'))
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(2022)
|
||||
->required()
|
||||
->integer(),
|
||||
TextInput::make('daemon_sftp_alias')
|
||||
->columnSpan(2)
|
||||
->label(trans('admin/node.sftp_alias'))
|
||||
->helperText(trans('admin/node.sftp_alias_help')),
|
||||
Grid::make()
|
||||
->columns(6)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
ToggleButtons::make('unlimited_mem')
|
||||
->dehydrated()
|
||||
->label(trans('admin/node.memory'))->inlineLabel()->inline()
|
||||
->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 => trans('admin/node.unlimited'),
|
||||
false => trans('admin/node.limited'),
|
||||
])
|
||||
->colors([
|
||||
true => 'primary',
|
||||
false => 'warning',
|
||||
])
|
||||
->columnSpan(2),
|
||||
TextInput::make('memory')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Get $get) => $get('unlimited_mem'))
|
||||
->label(trans('admin/node.memory_limit'))->inlineLabel()
|
||||
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->default(0)
|
||||
->required(),
|
||||
TextInput::make('memory_overallocate')
|
||||
->dehydratedWhenHidden()
|
||||
->label(trans('admin/node.overallocate'))->inlineLabel()
|
||||
->hidden(fn (Get $get) => $get('unlimited_mem'))
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->minValue(-1)
|
||||
->maxValue(100)
|
||||
->default(0)
|
||||
->suffix('%')
|
||||
->required(),
|
||||
]),
|
||||
Grid::make()
|
||||
->columns(6)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
ToggleButtons::make('unlimited_disk')
|
||||
->dehydrated()
|
||||
->label(trans('admin/node.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 => trans('admin/node.unlimited'),
|
||||
false => trans('admin/node.limited'),
|
||||
])
|
||||
->colors([
|
||||
true => 'primary',
|
||||
false => 'warning',
|
||||
])
|
||||
->columnSpan(2),
|
||||
TextInput::make('disk')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Get $get) => $get('unlimited_disk'))
|
||||
->label(trans('admin/node.disk_limit'))->inlineLabel()
|
||||
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->default(0)
|
||||
->required(),
|
||||
TextInput::make('disk_overallocate')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Get $get) => $get('unlimited_disk'))
|
||||
->label(trans('admin/node.overallocate'))->inlineLabel()
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->minValue(-1)
|
||||
->maxValue(100)
|
||||
->default(0)
|
||||
->suffix('%')
|
||||
->required(),
|
||||
]),
|
||||
Grid::make()
|
||||
->columns(6)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
ToggleButtons::make('unlimited_cpu')
|
||||
->dehydrated()
|
||||
->label(trans('admin/node.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 => trans('admin/node.unlimited'),
|
||||
false => trans('admin/node.limited'),
|
||||
])
|
||||
->colors([
|
||||
true => 'primary',
|
||||
false => 'warning',
|
||||
])
|
||||
->columnSpan(2),
|
||||
TextInput::make('cpu')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Get $get) => $get('unlimited_cpu'))
|
||||
->label(trans('admin/node.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(trans('admin/node.overallocate'))->inlineLabel()
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->default(0)
|
||||
->minValue(-1)
|
||||
->maxValue(100)
|
||||
->suffix('%')
|
||||
->required(),
|
||||
]),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getRedirectUrlParameters(): array
|
||||
{
|
||||
return [
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -119,22 +119,14 @@ class PluginResource extends Resource
|
||||
->color('success')
|
||||
->hidden(fn (Plugin $plugin) => $plugin->status !== PluginStatus::NotInstalled)
|
||||
->action(function (Plugin $plugin, $livewire, PluginService $pluginService) {
|
||||
try {
|
||||
$pluginService->installPlugin($plugin, !$plugin->isTheme() || !$pluginService->hasThemePluginEnabled());
|
||||
$pluginService->installPlugin($plugin, !$plugin->isTheme() || !$pluginService->hasThemePluginEnabled());
|
||||
|
||||
redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab]));
|
||||
redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab]));
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title(trans('admin/plugin.notifications.installed'))
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title(trans('admin/plugin.notifications.install_error'))
|
||||
->body($exception->getMessage())
|
||||
->send();
|
||||
}
|
||||
Notification::make()
|
||||
->success()
|
||||
->title(trans('admin/plugin.notifications.installed'))
|
||||
->send();
|
||||
}),
|
||||
Action::make('update')
|
||||
->label(trans('admin/plugin.update'))
|
||||
@@ -143,22 +135,14 @@ class PluginResource extends Resource
|
||||
->color('success')
|
||||
->visible(fn (Plugin $plugin) => $plugin->status !== PluginStatus::NotInstalled && $plugin->isUpdateAvailable())
|
||||
->action(function (Plugin $plugin, $livewire, PluginService $pluginService) {
|
||||
try {
|
||||
$pluginService->updatePlugin($plugin);
|
||||
$pluginService->updatePlugin($plugin);
|
||||
|
||||
redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab]));
|
||||
redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab]));
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title(trans('admin/plugin.notifications.updated'))
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title(trans('admin/plugin.notifications.update_error'))
|
||||
->body($exception->getMessage())
|
||||
->send();
|
||||
}
|
||||
Notification::make()
|
||||
->success()
|
||||
->title(trans('admin/plugin.notifications.updated'))
|
||||
->send();
|
||||
}),
|
||||
Action::make('enable')
|
||||
->label(trans('admin/plugin.enable'))
|
||||
@@ -176,7 +160,7 @@ class PluginResource extends Resource
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title(trans('admin/plugin.notifications.enabled'))
|
||||
->title(trans('admin/plugin.notifications.updated'))
|
||||
->send();
|
||||
}),
|
||||
Action::make('disable')
|
||||
@@ -218,24 +202,16 @@ class PluginResource extends Resource
|
||||
->icon('tabler-terminal')
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->hidden(fn (Plugin $plugin) => $plugin->status === PluginStatus::NotInstalled || $plugin->status === PluginStatus::Errored)
|
||||
->hidden(fn (Plugin $plugin) => $plugin->status === PluginStatus::NotInstalled)
|
||||
->action(function (Plugin $plugin, $livewire, PluginService $pluginService) {
|
||||
try {
|
||||
$pluginService->uninstallPlugin($plugin);
|
||||
$pluginService->uninstallPlugin($plugin);
|
||||
|
||||
redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab]));
|
||||
redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab]));
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title(trans('admin/plugin.notifications.uninstalled'))
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title(trans('admin/plugin.notifications.uninstall_error'))
|
||||
->body($exception->getMessage())
|
||||
->send();
|
||||
}
|
||||
Notification::make()
|
||||
->success()
|
||||
->title(trans('admin/plugin.notifications.uninstalled'))
|
||||
->send();
|
||||
}),
|
||||
]),
|
||||
])
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -49,7 +49,7 @@ class DatabasesRelationManager extends RelationManager
|
||||
->formatStateUsing(fn (Database $record) => $record->remote === '%' ? trans('admin/databasehost.anywhere'). ' ( % )' : $record->remote),
|
||||
TextInput::make('max_connections')
|
||||
->label(trans('admin/databasehost.table.max_connections'))
|
||||
->formatStateUsing(fn (Database $record) => $record->max_connections ?: trans('admin/databasehost.unlimited')),
|
||||
->formatStateUsing(fn (Database $record) => $record->max_connections === 0 ? trans('admin/databasehost.unlimited') : $record->max_connections),
|
||||
TextInput::make('jdbc')
|
||||
->label(trans('admin/databasehost.table.connection_string'))
|
||||
->columnSpanFull()
|
||||
@@ -75,7 +75,7 @@ class DatabasesRelationManager extends RelationManager
|
||||
->url(fn (Database $database) => route('filament.admin.resources.servers.edit', ['record' => $database->server_id])),
|
||||
TextColumn::make('max_connections')
|
||||
->label(trans('admin/databasehost.table.max_connections'))
|
||||
->formatStateUsing(fn ($record) => $record->max_connections ?: trans('admin/databasehost.unlimited')),
|
||||
->formatStateUsing(fn ($record) => $record->max_connections === 0 ? trans('admin/databasehost.unlimited') : $record->max_connections),
|
||||
DateTimeColumn::make('created_at')
|
||||
->label(trans('admin/databasehost.table.created_at')),
|
||||
])
|
||||
|
||||
@@ -7,7 +7,6 @@ use App\Filament\Admin\Resources\Servers\Pages\CreateServer;
|
||||
use App\Filament\Admin\Resources\Servers\Pages\EditServer;
|
||||
use App\Filament\Admin\Resources\Servers\Pages\ListServers;
|
||||
use App\Filament\Admin\Resources\Servers\RelationManagers\AllocationsRelationManager;
|
||||
use App\Filament\Admin\Resources\Servers\RelationManagers\DatabasesRelationManager;
|
||||
use App\Models\Mount;
|
||||
use App\Models\Server;
|
||||
use App\Traits\Filament\CanCustomizePages;
|
||||
@@ -87,7 +86,6 @@ class ServerResource extends Resource
|
||||
{
|
||||
return [
|
||||
AllocationsRelationManager::class,
|
||||
DatabasesRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ use App\Models\UserSSHKey;
|
||||
use App\Services\Helpers\LanguageService;
|
||||
use App\Traits\Filament\CanCustomizePages;
|
||||
use App\Traits\Filament\CanCustomizeRelations;
|
||||
use App\Traits\Filament\CanCustomizeStaticTabs;
|
||||
use App\Traits\Filament\CanModifyForm;
|
||||
use App\Traits\Filament\CanModifyTable;
|
||||
use DateTimeZone;
|
||||
@@ -60,7 +59,6 @@ class UserResource extends Resource
|
||||
{
|
||||
use CanCustomizePages;
|
||||
use CanCustomizeRelations;
|
||||
use CanCustomizeStaticTabs;
|
||||
use CanModifyForm;
|
||||
use CanModifyTable;
|
||||
|
||||
@@ -148,339 +146,328 @@ class UserResource extends Resource
|
||||
->columns(['default' => 1, 'lg' => 3, 'md' => 2])
|
||||
->components([
|
||||
Tabs::make()
|
||||
->schema(static::getTabs())
|
||||
->columnSpanFull(),
|
||||
]);
|
||||
}
|
||||
->schema([
|
||||
Tab::make('account')
|
||||
->label(trans('profile.tabs.account'))
|
||||
->icon('tabler-user-cog')
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'md' => 3,
|
||||
'lg' => 3,
|
||||
])
|
||||
->schema([
|
||||
TextInput::make('username')
|
||||
->label(trans('admin/user.username'))
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->required()
|
||||
->unique()
|
||||
->maxLength(255),
|
||||
TextInput::make('email')
|
||||
->label(trans('admin/user.email'))
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->email()
|
||||
->required()
|
||||
->unique()
|
||||
->maxLength(255),
|
||||
TextInput::make('password')
|
||||
->label(trans('admin/user.password'))
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->hintIcon(fn ($operation) => $operation === 'create' ? 'tabler-question-mark' : null, fn ($operation) => $operation === 'create' ? trans('admin/user.password_help') : null)
|
||||
->password()
|
||||
->hintAction(
|
||||
Action::make('password_reset')
|
||||
->label(trans('admin/user.password_reset'))
|
||||
->hidden(fn (string $operation) => $operation === 'create' || config('mail.default', 'log') === 'log')
|
||||
->icon('tabler-send')
|
||||
->action(function (User $user) {
|
||||
$status = Password::broker(Filament::getPanel('app')->getAuthPasswordBroker())->sendResetLink([
|
||||
'email' => $user->email,
|
||||
],
|
||||
function (User $user, string $token) {
|
||||
$notification = new ResetPassword($token);
|
||||
$notification->url = Filament::getPanel('app')->getResetPasswordUrl($token, $user);
|
||||
|
||||
/** @return Tab[] */
|
||||
protected static function getDefaultTabs(): array
|
||||
{
|
||||
return [
|
||||
Tab::make('account')
|
||||
->label(trans('profile.tabs.account'))
|
||||
->icon('tabler-user-cog')
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'md' => 3,
|
||||
'lg' => 3,
|
||||
])
|
||||
->schema([
|
||||
TextInput::make('username')
|
||||
->label(trans('admin/user.username'))
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->required()
|
||||
->unique()
|
||||
->maxLength(255),
|
||||
TextInput::make('email')
|
||||
->label(trans('admin/user.email'))
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->email()
|
||||
->required()
|
||||
->unique()
|
||||
->maxLength(255),
|
||||
TextInput::make('password')
|
||||
->label(trans('admin/user.password'))
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->hintIcon(fn ($operation) => $operation === 'create' ? 'tabler-question-mark' : null, fn ($operation) => $operation === 'create' ? trans('admin/user.password_help') : null)
|
||||
->password()
|
||||
->hintAction(
|
||||
Action::make('password_reset')
|
||||
->label(trans('admin/user.password_reset'))
|
||||
->hidden(fn (string $operation) => $operation === 'create' || config('mail.default', 'log') === 'log')
|
||||
->icon('tabler-send')
|
||||
->action(function (User $user) {
|
||||
$status = Password::broker(Filament::getPanel('app')->getAuthPasswordBroker())->sendResetLink([
|
||||
'email' => $user->email,
|
||||
],
|
||||
function (User $user, string $token) {
|
||||
$notification = new ResetPassword($token);
|
||||
$notification->url = Filament::getPanel('app')->getResetPasswordUrl($token, $user);
|
||||
$user->notify($notification);
|
||||
|
||||
$user->notify($notification);
|
||||
event(new PasswordResetLinkSent($user));
|
||||
},
|
||||
);
|
||||
|
||||
event(new PasswordResetLinkSent($user));
|
||||
},
|
||||
);
|
||||
if ($status === Password::RESET_LINK_SENT) {
|
||||
Notification::make()
|
||||
->title(trans('admin/user.password_reset_sent'))
|
||||
->success()
|
||||
->send();
|
||||
} else {
|
||||
Notification::make()
|
||||
->title(trans('admin/user.password_reset_failed'))
|
||||
->body($status)
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
})),
|
||||
TextInput::make('external_id')
|
||||
->label(trans('admin/user.external_id'))
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
]),
|
||||
Toggle::make('is_managed_externally')
|
||||
->label(trans('admin/user.is_managed_externally'))
|
||||
->hintIcon('tabler-question-mark', trans('admin/user.is_managed_externally_helper'))
|
||||
->inline(false)
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
]),
|
||||
Section::make(trans('profile.tabs.customization'))
|
||||
->collapsible()
|
||||
->columnSpanFull()
|
||||
->columns(2)
|
||||
->schema([
|
||||
Select::make('timezone')
|
||||
->label(trans('profile.timezone'))
|
||||
->required()
|
||||
->prefixIcon('tabler-clock-pin')
|
||||
->default(fn () => config('app.timezone', 'UTC'))
|
||||
->selectablePlaceholder(false)
|
||||
->options(fn () => collect(DateTimeZone::listIdentifiers())->mapWithKeys(fn ($tz) => [$tz => $tz]))
|
||||
->searchable(),
|
||||
Select::make('language')
|
||||
->label(trans('profile.language'))
|
||||
->required()
|
||||
->prefixIcon('tabler-flag')
|
||||
->live()
|
||||
->default('en')
|
||||
->searchable()
|
||||
->selectablePlaceholder(false)
|
||||
->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages()),
|
||||
FileUpload::make('avatar')
|
||||
->visible(fn (?User $user, FileUpload $fileUpload) => $user ? $fileUpload->getDisk()->exists($fileUpload->getDirectory() . '/' . $user->id . '.png') : false)
|
||||
->columnSpanFull()
|
||||
->avatar()
|
||||
->directory('avatars')
|
||||
->disk('public')
|
||||
->formatStateUsing(function (FileUpload $fileUpload, ?User $user) {
|
||||
if (!$user) {
|
||||
return null;
|
||||
}
|
||||
$path = $fileUpload->getDirectory() . '/' . $user->id . '.png';
|
||||
if ($fileUpload->getDisk()->exists($path)) {
|
||||
return $path;
|
||||
}
|
||||
})
|
||||
->deleteUploadedFileUsing(function (FileUpload $fileUpload, $file) {
|
||||
if ($file instanceof TemporaryUploadedFile) {
|
||||
return $file->delete();
|
||||
}
|
||||
|
||||
if ($status === Password::RESET_LINK_SENT) {
|
||||
Notification::make()
|
||||
->title(trans('admin/user.password_reset_sent'))
|
||||
->success()
|
||||
->send();
|
||||
} else {
|
||||
Notification::make()
|
||||
->title(trans('admin/user.password_reset_failed'))
|
||||
->body($status)
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
})),
|
||||
TextInput::make('external_id')
|
||||
->label(trans('admin/user.external_id'))
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
]),
|
||||
Toggle::make('is_managed_externally')
|
||||
->label(trans('admin/user.is_managed_externally'))
|
||||
->hintIcon('tabler-question-mark', trans('admin/user.is_managed_externally_helper'))
|
||||
->inline(false)
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
]),
|
||||
Section::make(trans('profile.tabs.customization'))
|
||||
->collapsible()
|
||||
->columnSpanFull()
|
||||
->columns(2)
|
||||
->schema([
|
||||
Select::make('timezone')
|
||||
->label(trans('profile.timezone'))
|
||||
->required()
|
||||
->prefixIcon('tabler-clock-pin')
|
||||
->default(fn () => config('app.timezone', 'UTC'))
|
||||
->selectablePlaceholder(false)
|
||||
->options(fn () => collect(DateTimeZone::listIdentifiers())->mapWithKeys(fn ($tz) => [$tz => $tz]))
|
||||
->searchable(),
|
||||
Select::make('language')
|
||||
->label(trans('profile.language'))
|
||||
->required()
|
||||
->prefixIcon('tabler-flag')
|
||||
->live()
|
||||
->default('en')
|
||||
->searchable()
|
||||
->selectablePlaceholder(false)
|
||||
->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages()),
|
||||
FileUpload::make('avatar')
|
||||
->visible(fn (?User $user, FileUpload $fileUpload) => $user ? $fileUpload->getDisk()->exists($fileUpload->getDirectory() . '/' . $user->id . '.png') : false)
|
||||
->columnSpanFull()
|
||||
->avatar()
|
||||
->directory('avatars')
|
||||
->disk('public')
|
||||
->formatStateUsing(function (FileUpload $fileUpload, ?User $user) {
|
||||
if (!$user) {
|
||||
return null;
|
||||
}
|
||||
$path = $fileUpload->getDirectory() . '/' . $user->id . '.png';
|
||||
if ($fileUpload->getDisk()->exists($path)) {
|
||||
return $path;
|
||||
}
|
||||
})
|
||||
->deleteUploadedFileUsing(function (FileUpload $fileUpload, $file) {
|
||||
if ($file instanceof TemporaryUploadedFile) {
|
||||
return $file->delete();
|
||||
}
|
||||
if ($fileUpload->getDisk()->exists($file)) {
|
||||
return $fileUpload->getDisk()->delete($file);
|
||||
}
|
||||
}),
|
||||
]),
|
||||
Section::make(trans('profile.tabs.oauth'))
|
||||
->visible(fn (?User $user) => $user)
|
||||
->collapsible()
|
||||
->columnSpanFull()
|
||||
->schema(function (OAuthService $oauthService, ?User $user) {
|
||||
if (!$user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($fileUpload->getDisk()->exists($file)) {
|
||||
return $fileUpload->getDisk()->delete($file);
|
||||
}
|
||||
}),
|
||||
]),
|
||||
Section::make(trans('profile.tabs.oauth'))
|
||||
->visible(fn (?User $user) => $user)
|
||||
->collapsible()
|
||||
->columnSpanFull()
|
||||
->schema(function (OAuthService $oauthService, ?User $user) {
|
||||
if (!$user) {
|
||||
return;
|
||||
}
|
||||
|
||||
$actions = [];
|
||||
foreach ($user->oauth ?? [] as $schema => $_) {
|
||||
$schema = $oauthService->get($schema);
|
||||
if (!$schema) {
|
||||
return;
|
||||
}
|
||||
|
||||
$id = $schema->getId();
|
||||
$name = $schema->getName();
|
||||
|
||||
$color = $schema->getHexColor();
|
||||
$color = is_string($color) ? Color::hex($color) : null;
|
||||
|
||||
$actions[] = Action::make("oauth_$id")
|
||||
->label(trans('profile.unlink', ['name' => $name]))
|
||||
->icon('tabler-unlink')
|
||||
->requiresConfirmation()
|
||||
->color($color)
|
||||
->action(function ($livewire) use ($oauthService, $user, $name, $schema) {
|
||||
$oauthService->unlinkUser($user, $schema);
|
||||
$livewire->form->fill($user->attributesToArray());
|
||||
Notification::make()
|
||||
->title(trans('profile.unlinked', ['name' => $name]))
|
||||
->success()
|
||||
->send();
|
||||
});
|
||||
}
|
||||
|
||||
if (!$actions) {
|
||||
return [
|
||||
TextEntry::make('no_oauth')
|
||||
->state(trans('profile.no_oauth'))
|
||||
->hiddenLabel(),
|
||||
];
|
||||
}
|
||||
|
||||
return [Actions::make($actions)];
|
||||
}),
|
||||
]),
|
||||
Tab::make('roles')
|
||||
->label(trans('admin/user.roles'))
|
||||
->icon('tabler-users-group')
|
||||
->schema([
|
||||
CheckboxList::make('roles')
|
||||
->hidden(fn (?User $user) => $user && $user->isRootAdmin())
|
||||
->relationship('roles', 'name', fn (Builder $query) => $query->whereNot('id', Role::getRootAdmin()->id))
|
||||
->saveRelationshipsUsing(fn (User $user, array $state) => $user->syncRoles(collect($state)->map(fn ($role) => Role::findById($role))))
|
||||
->dehydrated()
|
||||
->label(trans('admin/user.admin_roles'))
|
||||
->columnSpanFull()
|
||||
->bulkToggleable(false),
|
||||
CheckboxList::make('root_admin_role')
|
||||
->visible(fn (?User $user) => $user && $user->isRootAdmin())
|
||||
->disabled()
|
||||
->options([
|
||||
'root_admin' => Role::ROOT_ADMIN,
|
||||
])
|
||||
->descriptions([
|
||||
'root_admin' => trans('admin/role.root_admin', ['role' => Role::ROOT_ADMIN]),
|
||||
])
|
||||
->formatStateUsing(fn () => ['root_admin'])
|
||||
->dehydrated(false)
|
||||
->label(trans('admin/user.admin_roles'))
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
Tab::make('keys')
|
||||
->visible(fn (?User $user) => $user)
|
||||
->label(trans('profile.tabs.keys'))
|
||||
->icon('tabler-key')
|
||||
->schema([
|
||||
Section::make(trans('profile.api_keys'))
|
||||
->columnSpan(2)
|
||||
->schema([
|
||||
Repeater::make('api_keys')
|
||||
->hiddenLabel()
|
||||
->inlineLabel(false)
|
||||
->relationship('apiKeys')
|
||||
->addable(false)
|
||||
->itemLabel(fn ($state) => $state['identifier'])
|
||||
->deleteAction(function (Action $action) {
|
||||
$action->requiresConfirmation()->action(function (array $arguments, Repeater $component, ?User $user) {
|
||||
$items = $component->getState();
|
||||
$key = $items[$arguments['item']] ?? null;
|
||||
|
||||
if ($key) {
|
||||
$apiKey = ApiKey::find($key['id']);
|
||||
if ($apiKey?->exists()) {
|
||||
$apiKey->delete();
|
||||
|
||||
Activity::event('user:api-key.delete')
|
||||
->actor(user())
|
||||
->subject($user)
|
||||
->subject($apiKey)
|
||||
->property('identifier', $apiKey->identifier)
|
||||
->log();
|
||||
$actions = [];
|
||||
foreach ($user->oauth ?? [] as $schema => $_) {
|
||||
$schema = $oauthService->get($schema);
|
||||
if (!$schema) {
|
||||
return;
|
||||
}
|
||||
|
||||
unset($items[$arguments['item']]);
|
||||
$component->state($items);
|
||||
$component->callAfterStateUpdated();
|
||||
}
|
||||
});
|
||||
})
|
||||
->schema([
|
||||
TextEntry::make('memo')
|
||||
->hiddenLabel()
|
||||
->state(fn (ApiKey $key) => $key->memo),
|
||||
])
|
||||
->visible(fn (User $user) => $user->apiKeys()->exists()),
|
||||
|
||||
TextEntry::make('no_api_keys')
|
||||
->state(trans('profile.no_api_keys'))
|
||||
->hiddenLabel()
|
||||
->visible(fn (User $user) => !$user->apiKeys()->exists()),
|
||||
]),
|
||||
Section::make(trans('profile.ssh_keys'))->columnSpan(2)
|
||||
->schema([
|
||||
Repeater::make('ssh_keys')
|
||||
->hiddenLabel()
|
||||
->inlineLabel(false)
|
||||
->relationship('sshKeys')
|
||||
->addable(false)
|
||||
->itemLabel(fn ($state) => $state['name'])
|
||||
->deleteAction(function (Action $action) {
|
||||
$action->requiresConfirmation()->action(function (array $arguments, Repeater $component, User $user) {
|
||||
$items = $component->getState();
|
||||
$key = $items[$arguments['item']];
|
||||
|
||||
$sshKey = UserSSHKey::find($key['id'] ?? null);
|
||||
if ($sshKey->exists()) {
|
||||
$sshKey->delete();
|
||||
|
||||
Activity::event('user:ssh-key.delete')
|
||||
->actor(user())
|
||||
->subject($user)
|
||||
->subject($sshKey)
|
||||
->property('fingerprint', $sshKey->fingerprint)
|
||||
->log();
|
||||
$id = $schema->getId();
|
||||
$name = $schema->getName();
|
||||
$actions[] = Action::make("oauth_$id")
|
||||
->label(trans('profile.unlink', ['name' => $name]))
|
||||
->icon('tabler-unlink')
|
||||
->requiresConfirmation()
|
||||
->color(Color::hex($schema->getHexColor()))
|
||||
->action(function ($livewire) use ($oauthService, $user, $name, $schema) {
|
||||
$oauthService->unlinkUser($user, $schema);
|
||||
$livewire->form->fill($user->attributesToArray());
|
||||
Notification::make()
|
||||
->title(trans('profile.unlinked', ['name' => $name]))
|
||||
->success()
|
||||
->send();
|
||||
});
|
||||
}
|
||||
|
||||
unset($items[$arguments['item']]);
|
||||
if (!$actions) {
|
||||
return [
|
||||
TextEntry::make('no_oauth')
|
||||
->state(trans('profile.no_oauth'))
|
||||
->hiddenLabel(),
|
||||
];
|
||||
}
|
||||
|
||||
$component->state($items);
|
||||
return [Actions::make($actions)];
|
||||
}),
|
||||
]),
|
||||
Tab::make('roles')
|
||||
->label(trans('admin/user.roles'))
|
||||
->icon('tabler-users-group')
|
||||
->components([
|
||||
CheckboxList::make('roles')
|
||||
->hidden(fn (?User $user) => $user && $user->isRootAdmin())
|
||||
->relationship('roles', 'name', fn (Builder $query) => $query->whereNot('id', Role::getRootAdmin()->id))
|
||||
->saveRelationshipsUsing(fn (User $user, array $state) => $user->syncRoles(collect($state)->map(fn ($role) => Role::findById($role))))
|
||||
->dehydrated()
|
||||
->label(trans('admin/user.admin_roles'))
|
||||
->columnSpanFull()
|
||||
->bulkToggleable(false),
|
||||
CheckboxList::make('root_admin_role')
|
||||
->visible(fn (?User $user) => $user && $user->isRootAdmin())
|
||||
->disabled()
|
||||
->options([
|
||||
'root_admin' => Role::ROOT_ADMIN,
|
||||
])
|
||||
->descriptions([
|
||||
'root_admin' => trans('admin/role.root_admin', ['role' => Role::ROOT_ADMIN]),
|
||||
])
|
||||
->formatStateUsing(fn () => ['root_admin'])
|
||||
->dehydrated(false)
|
||||
->label(trans('admin/user.admin_roles'))
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
Tab::make('keys')
|
||||
->visible(fn (?User $user) => $user)
|
||||
->label(trans('profile.tabs.keys'))
|
||||
->icon('tabler-key')
|
||||
->schema([
|
||||
Section::make(trans('profile.api_keys'))
|
||||
->columnSpan(2)
|
||||
->schema([
|
||||
Repeater::make('api_keys')
|
||||
->hiddenLabel()
|
||||
->inlineLabel(false)
|
||||
->relationship('apiKeys')
|
||||
->addable(false)
|
||||
->itemLabel(fn ($state) => $state['identifier'])
|
||||
->deleteAction(function (Action $action) {
|
||||
$action->requiresConfirmation()->action(function (array $arguments, Repeater $component, ?User $user) {
|
||||
$items = $component->getState();
|
||||
$key = $items[$arguments['item']] ?? null;
|
||||
|
||||
$component->callAfterStateUpdated();
|
||||
});
|
||||
})
|
||||
->schema(fn () => [
|
||||
TextEntry::make('fingerprint')
|
||||
->hiddenLabel()
|
||||
->state(fn (UserSSHKey $key) => "SHA256:{$key->fingerprint}"),
|
||||
])
|
||||
->visible(fn (User $user) => $user->sshKeys()->exists()),
|
||||
if ($key) {
|
||||
$apiKey = ApiKey::find($key['id']);
|
||||
if ($apiKey?->exists()) {
|
||||
$apiKey->delete();
|
||||
|
||||
TextEntry::make('no_ssh_keys')
|
||||
->state(trans('profile.no_ssh_keys'))
|
||||
->hiddenLabel()
|
||||
->visible(fn (User $user) => !$user->sshKeys()->exists()),
|
||||
]),
|
||||
]),
|
||||
Tab::make('activity')
|
||||
->visible(fn (?User $user) => $user)
|
||||
->disabledOn('create')
|
||||
->label(trans('profile.tabs.activity'))
|
||||
->icon('tabler-history')
|
||||
->schema([
|
||||
Repeater::make('activity')
|
||||
->hiddenLabel()
|
||||
->inlineLabel(false)
|
||||
->deletable(false)
|
||||
->addable(false)
|
||||
->relationship(null, function (Builder $query) {
|
||||
$query->orderBy('timestamp', 'desc');
|
||||
})
|
||||
->schema([
|
||||
TextEntry::make('log')
|
||||
->hiddenLabel()
|
||||
->state(fn (ActivityLog $log) => new HtmlString($log->htmlable())),
|
||||
]),
|
||||
]),
|
||||
];
|
||||
Activity::event('user:api-key.delete')
|
||||
->actor(user())
|
||||
->subject($user)
|
||||
->subject($apiKey)
|
||||
->property('identifier', $apiKey->identifier)
|
||||
->log();
|
||||
}
|
||||
|
||||
unset($items[$arguments['item']]);
|
||||
$component->state($items);
|
||||
$component->callAfterStateUpdated();
|
||||
}
|
||||
});
|
||||
})
|
||||
->schema([
|
||||
TextEntry::make('memo')
|
||||
->hiddenLabel()
|
||||
->state(fn (ApiKey $key) => $key->memo),
|
||||
])
|
||||
->visible(fn (User $user) => $user->apiKeys()->exists()),
|
||||
|
||||
TextEntry::make('no_api_keys')
|
||||
->state(trans('profile.no_api_keys'))
|
||||
->hiddenLabel()
|
||||
->visible(fn (User $user) => !$user->apiKeys()->exists()),
|
||||
]),
|
||||
Section::make(trans('profile.ssh_keys'))->columnSpan(2)
|
||||
->schema([
|
||||
Repeater::make('ssh_keys')
|
||||
->hiddenLabel()
|
||||
->inlineLabel(false)
|
||||
->relationship('sshKeys')
|
||||
->addable(false)
|
||||
->itemLabel(fn ($state) => $state['name'])
|
||||
->deleteAction(function (Action $action) {
|
||||
$action->requiresConfirmation()->action(function (array $arguments, Repeater $component, User $user) {
|
||||
$items = $component->getState();
|
||||
$key = $items[$arguments['item']];
|
||||
|
||||
$sshKey = UserSSHKey::find($key['id'] ?? null);
|
||||
if ($sshKey->exists()) {
|
||||
$sshKey->delete();
|
||||
|
||||
Activity::event('user:ssh-key.delete')
|
||||
->actor(user())
|
||||
->subject($user)
|
||||
->subject($sshKey)
|
||||
->property('fingerprint', $sshKey->fingerprint)
|
||||
->log();
|
||||
}
|
||||
|
||||
unset($items[$arguments['item']]);
|
||||
|
||||
$component->state($items);
|
||||
|
||||
$component->callAfterStateUpdated();
|
||||
});
|
||||
})
|
||||
->schema(fn () => [
|
||||
TextEntry::make('fingerprint')
|
||||
->hiddenLabel()
|
||||
->state(fn (UserSSHKey $key) => "SHA256:{$key->fingerprint}"),
|
||||
])
|
||||
->visible(fn (User $user) => $user->sshKeys()->exists()),
|
||||
|
||||
TextEntry::make('no_ssh_keys')
|
||||
->state(trans('profile.no_ssh_keys'))
|
||||
->hiddenLabel()
|
||||
->visible(fn (User $user) => !$user->sshKeys()->exists()),
|
||||
]),
|
||||
]),
|
||||
Tab::make('activity')
|
||||
->visible(fn (?User $user) => $user)
|
||||
->disabledOn('create')
|
||||
->label(trans('profile.tabs.activity'))
|
||||
->icon('tabler-history')
|
||||
->schema([
|
||||
Repeater::make('activity')
|
||||
->hiddenLabel()
|
||||
->inlineLabel(false)
|
||||
->deletable(false)
|
||||
->addable(false)
|
||||
->relationship(null, function (Builder $query) {
|
||||
$query->orderBy('timestamp', 'desc');
|
||||
})
|
||||
->schema([
|
||||
TextEntry::make('log')
|
||||
->hiddenLabel()
|
||||
->state(fn (ActivityLog $log) => new HtmlString($log->htmlable())),
|
||||
]),
|
||||
]),
|
||||
])->columnSpanFull(),
|
||||
]);
|
||||
}
|
||||
|
||||
/** @return class-string<RelationManager>[] */
|
||||
|
||||
@@ -3,24 +3,24 @@
|
||||
namespace App\Filament\Components\Actions;
|
||||
|
||||
use App\Console\Commands\Egg\UpdateEggIndexCommand;
|
||||
use App\Jobs\InstallEgg;
|
||||
use App\Models\Egg;
|
||||
use App\Services\Eggs\Sharing\EggImporterService;
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Schemas\Components\Tabs;
|
||||
use Filament\Schemas\Components\Tabs\Tab;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
use Filament\Support\Enums\IconSize;
|
||||
use Filament\Support\Enums\Width;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
|
||||
class ImportEggAction extends Action
|
||||
@@ -42,31 +42,10 @@ class ImportEggAction extends Action
|
||||
|
||||
$this->iconSize(IconSize::ExtraLarge);
|
||||
|
||||
$this->modalWidth(Width::ScreenExtraLarge);
|
||||
|
||||
$this->authorize(fn () => user()?->can('import egg'));
|
||||
|
||||
$this->action(function (array $data, EggImporterService $eggImportService): void {
|
||||
|
||||
$gitHubEggs = array_get($this->data, 'eggs', []);
|
||||
$eggs = array_merge(collect($data['urls'])->flatten()->whereNotNull()->unique()->all(), Arr::wrap($data['files']));
|
||||
|
||||
if ($gitHubEggs) {
|
||||
foreach ($gitHubEggs as $category => $sortedEggs) {
|
||||
foreach ($sortedEggs as $downloadUrl) {
|
||||
InstallEgg::dispatch($downloadUrl);
|
||||
}
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title(trans('installer.egg.background_install_started'))
|
||||
->body(trans('installer.egg.background_install_description', ['count' => array_sum(array_map('count', $gitHubEggs))]))
|
||||
->success()
|
||||
->persistent()
|
||||
->send();
|
||||
|
||||
}
|
||||
|
||||
if (empty($eggs)) {
|
||||
return;
|
||||
}
|
||||
@@ -110,20 +89,18 @@ class ImportEggAction extends Action
|
||||
}
|
||||
}
|
||||
|
||||
$bodyParts = collect([
|
||||
$success->isNotEmpty() ? trans('admin/egg.import.imported_eggs', ['eggs' => $success->join(', ')]) : null,
|
||||
$failed->isNotEmpty() ? trans('admin/egg.import.failed_import_eggs', ['eggs' => $failed->join(', ')]) : null,
|
||||
])->filter();
|
||||
|
||||
if ($bodyParts->isNotEmpty()) {
|
||||
if ($failed->count() > 0) {
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.import.import_result', [
|
||||
'success' => $success->count(),
|
||||
'failed' => $failed->count(),
|
||||
'total' => $success->count() + $failed->count(),
|
||||
]))
|
||||
->body($bodyParts->join(' | '))
|
||||
->status($failed->isEmpty() ? 'success' : ($success->isEmpty() ? 'danger' : 'warning'))
|
||||
->title(trans('admin/egg.import.import_failed'))
|
||||
->body($failed->join(', '))
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
if ($success->count() > 0) {
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.import.import_success'))
|
||||
->body($success->join(', '))
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
});
|
||||
@@ -136,7 +113,6 @@ class ImportEggAction extends Action
|
||||
Tabs::make('Tabs')
|
||||
->contained(false)
|
||||
->tabs([
|
||||
$this->importEggsFromGitHub(),
|
||||
Tab::make('file')
|
||||
->label(trans('admin/egg.import.file'))
|
||||
->icon('tabler-file-upload')
|
||||
@@ -154,6 +130,30 @@ class ImportEggAction extends Action
|
||||
->label(trans('admin/egg.import.url'))
|
||||
->icon('tabler-world-upload')
|
||||
->schema([
|
||||
Select::make('github')
|
||||
->label(trans('admin/egg.import.github'))
|
||||
->options(fn () => cache('eggs.index'))
|
||||
->selectablePlaceholder(false)
|
||||
->searchable()
|
||||
->preload()
|
||||
->live()
|
||||
->hintAction(
|
||||
Action::make('refresh')
|
||||
->iconButton()
|
||||
->icon('tabler-refresh')
|
||||
->tooltip(trans('admin/egg.import.refresh'))
|
||||
->action(function () {
|
||||
Artisan::call(UpdateEggIndexCommand::class);
|
||||
})
|
||||
)
|
||||
->afterStateUpdated(function ($state, Set $set, Get $get) use ($isMultiple) {
|
||||
if ($state) {
|
||||
$urls = $isMultiple ? $get('urls') : [];
|
||||
$urls[Str::uuid()->toString()] = ['url' => $state];
|
||||
$set('urls', $urls);
|
||||
$set('github', null);
|
||||
}
|
||||
}),
|
||||
Repeater::make('urls')
|
||||
->hiddenLabel()
|
||||
->itemLabel(fn (array $state) => str($state['url'])->afterLast('/egg-')->beforeLast('.')->headline())
|
||||
@@ -179,49 +179,4 @@ class ImportEggAction extends Action
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function importEggsFromGitHub(): Tab
|
||||
{
|
||||
if (!cache()->get('eggs.index')) {
|
||||
Artisan::call(UpdateEggIndexCommand::class);
|
||||
}
|
||||
|
||||
$eggs = cache()->get('eggs.index', []);
|
||||
$categories = array_keys($eggs);
|
||||
$tabs = array_map(function (string $label) use ($eggs) {
|
||||
$id = str_slug($label, '_');
|
||||
$eggCount = count($eggs[$label]);
|
||||
|
||||
return Tab::make($id)
|
||||
->label($label)
|
||||
->badge($eggCount)
|
||||
->schema([
|
||||
CheckboxList::make("eggs.$id")
|
||||
->hiddenLabel()
|
||||
->options(fn () => array_sort($eggs[$label]))
|
||||
->searchable($eggCount > 0)
|
||||
->bulkToggleable($eggCount > 0)
|
||||
->columns(4),
|
||||
]);
|
||||
}, $categories);
|
||||
|
||||
if (empty($tabs)) {
|
||||
$tabs[] = Tab::make('no_eggs')
|
||||
->label(trans('installer.egg.no_eggs'))
|
||||
->schema([
|
||||
TextEntry::make('no_eggs')
|
||||
->hiddenLabel()
|
||||
->state(trans('installer.egg.exceptions.no_eggs')),
|
||||
]);
|
||||
}
|
||||
|
||||
return Tab::make('github')
|
||||
->label(trans('admin/egg.import.github'))
|
||||
->icon('tabler-brand-github')
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Tabs::make('egg_tabs')
|
||||
->tabs($tabs),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +47,8 @@ class UpdateEggAction extends Action
|
||||
cache()->forget("eggs.$egg->uuid.update");
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.update_failed', ['egg' => $egg->name]))
|
||||
->body(trans('admin/egg.update_error', ['error' => $exception->getMessage()]))
|
||||
->title(trans('admin/egg.update_failed'))
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
@@ -58,8 +58,8 @@ class UpdateEggAction extends Action
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.update_success', ['egg' => $egg->name]))
|
||||
->body(trans('admin/egg.updated_from', ['url' => $egg->update_url]))
|
||||
->title(trans_choice('admin/egg.updated', 1))
|
||||
->body($egg->name)
|
||||
->success()
|
||||
->send();
|
||||
});
|
||||
|
||||
@@ -47,40 +47,39 @@ class UpdateEggBulkAction extends BulkAction
|
||||
return;
|
||||
}
|
||||
|
||||
$successEggs = collect();
|
||||
$failedEggs = collect();
|
||||
$skippedEggs = collect();
|
||||
$success = 0;
|
||||
$failed = 0;
|
||||
$skipped = 0;
|
||||
|
||||
/** @var Egg $egg */
|
||||
foreach ($records as $egg) {
|
||||
if ($egg->update_url === null) {
|
||||
$skippedEggs->push($egg->name);
|
||||
$skipped++;
|
||||
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$eggImporterService->fromUrl($egg->update_url, $egg);
|
||||
|
||||
$successEggs->push($egg->name);
|
||||
$success++;
|
||||
|
||||
cache()->forget("eggs.$egg->uuid.update");
|
||||
} catch (Exception $exception) {
|
||||
$failedEggs->push($egg->name);
|
||||
$failed++;
|
||||
|
||||
report($exception);
|
||||
}
|
||||
}
|
||||
|
||||
$bodyParts = collect([
|
||||
$successEggs->isNotEmpty() ? trans('admin/egg.updated_eggs', ['eggs' => $successEggs->join(', ')]) : null,
|
||||
$failedEggs->isNotEmpty() ? trans('admin/egg.failed_eggs', ['eggs' => $failedEggs->join(', ')]) : null,
|
||||
$skippedEggs->isNotEmpty() ? trans('admin/egg.skipped_eggs', ['eggs' => $skippedEggs->join(', ')]) : null,
|
||||
])->filter();
|
||||
|
||||
Notification::make()
|
||||
->title(trans_choice('admin/egg.updated', 2, ['count' => $successEggs->count(), 'total' => $records->count()]))
|
||||
->body($bodyParts->join(' | '))
|
||||
->status($failedEggs->isNotEmpty() ? 'warning' : 'success')
|
||||
->title(trans_choice('admin/egg.updated', 2, ['count' => $success, 'total' => $records->count()]))
|
||||
->body(
|
||||
collect([
|
||||
$failed > 0 ? trans('admin/egg.updated_failed', ['count' => $failed]) : null,
|
||||
$skipped > 0 ? trans('admin/egg.updated_skipped', ['count' => $skipped]) : null,
|
||||
])->filter()->join(' ')
|
||||
)
|
||||
->status($failed > 0 ? 'warning' : 'success')
|
||||
->persistent()
|
||||
->send();
|
||||
});
|
||||
|
||||
@@ -83,8 +83,6 @@ class StartupVariable extends Field
|
||||
$this->variableDefault(fn (Get $get) => $get('default_value'));
|
||||
$this->variableRules(fn (Get $get) => $get('rules'));
|
||||
|
||||
$this->disabled(fn (Get $get) => !$get('user_editable'));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -96,8 +94,6 @@ class StartupVariable extends Field
|
||||
$this->variableDefault(fn (?ServerVariable $record) => $record?->variable->default_value);
|
||||
$this->variableRules(fn (?ServerVariable $record) => $record?->variable->rules);
|
||||
|
||||
$this->disabled(fn (?ServerVariable $record) => !$record?->variable->user_editable);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ use App\Services\Ssh\KeyCreationService;
|
||||
use App\Services\Users\UserUpdateService;
|
||||
use App\Traits\Filament\CanCustomizeHeaderActions;
|
||||
use App\Traits\Filament\CanCustomizeHeaderWidgets;
|
||||
use App\Traits\Filament\CanCustomizeTabs;
|
||||
use DateTimeZone;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
@@ -57,7 +56,6 @@ class EditProfile extends BaseEditProfile
|
||||
{
|
||||
use CanCustomizeHeaderActions;
|
||||
use CanCustomizeHeaderWidgets;
|
||||
use CanCustomizeTabs;
|
||||
|
||||
protected OAuthService $oauthService;
|
||||
|
||||
@@ -83,433 +81,418 @@ class EditProfile extends BaseEditProfile
|
||||
* @throws Exception
|
||||
*/
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Tabs::make()
|
||||
->persistTabInQueryString()
|
||||
->tabs($this->getTabs()),
|
||||
])
|
||||
->operation('edit')
|
||||
->model($this->getUser())
|
||||
->statePath('data')
|
||||
->inlineLabel(!static::isSimple());
|
||||
}
|
||||
|
||||
/** @return Tab[] */
|
||||
protected function getDefaultTabs(): array
|
||||
{
|
||||
$oauthSchemas = $this->oauthService->getEnabled();
|
||||
|
||||
return [
|
||||
Tab::make('account')
|
||||
->label(trans('profile.tabs.account'))
|
||||
->icon('tabler-user-cog')
|
||||
->schema([
|
||||
TextInput::make('username')
|
||||
->disabled(fn (User $user) => $user->is_managed_externally)
|
||||
->prefixIcon('tabler-user')
|
||||
->label(trans('profile.username'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->unique(),
|
||||
TextInput::make('email')
|
||||
->disabled(fn (User $user) => $user->is_managed_externally)
|
||||
->prefixIcon('tabler-mail')
|
||||
->label(trans('profile.email'))
|
||||
->email()
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->unique(),
|
||||
TextInput::make('password')
|
||||
->hidden(fn (User $user) => $user->is_managed_externally)
|
||||
->label(trans('profile.password'))
|
||||
->password()
|
||||
->prefixIcon('tabler-password')
|
||||
->revealable(filament()->arePasswordsRevealable())
|
||||
->rule(Password::default())
|
||||
->autocomplete('new-password')
|
||||
->dehydrated(fn ($state) => filled($state))
|
||||
->dehydrateStateUsing(fn ($state) => Hash::make($state))
|
||||
->live(debounce: 500)
|
||||
->same('passwordConfirmation'),
|
||||
TextInput::make('passwordConfirmation')
|
||||
->label(trans('profile.password_confirmation'))
|
||||
->password()
|
||||
->prefixIcon('tabler-password-fingerprint')
|
||||
->revealable(filament()->arePasswordsRevealable())
|
||||
->required()
|
||||
->visible(fn (Get $get) => filled($get('password')))
|
||||
->dehydrated(false),
|
||||
Select::make('timezone')
|
||||
->label(trans('profile.timezone'))
|
||||
->required()
|
||||
->prefixIcon('tabler-clock-pin')
|
||||
->default(config('app.timezone', 'UTC'))
|
||||
->selectablePlaceholder(false)
|
||||
->options(fn () => collect(DateTimeZone::listIdentifiers())->mapWithKeys(fn ($tz) => [$tz => $tz]))
|
||||
->searchable(),
|
||||
Select::make('language')
|
||||
->label(trans('profile.language'))
|
||||
->required()
|
||||
->prefixIcon('tabler-flag')
|
||||
->live()
|
||||
->default('en')
|
||||
->selectablePlaceholder(false)
|
||||
->helperText(fn ($state, LanguageService $languageService) => new HtmlString($languageService->isLanguageTranslated($state) ? ''
|
||||
: trans('profile.language_help', ['state' => $state]) . ' <u><a href="https://crowdin.com/project/pelican-dev/">Update On Crowdin</a></u>'))
|
||||
->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages()),
|
||||
FileUpload::make('avatar')
|
||||
->visible(fn () => config('panel.filament.uploadable-avatars'))
|
||||
->avatar()
|
||||
->imageEditor()
|
||||
->acceptedFileTypes(['image/png'])
|
||||
->directory('avatars')
|
||||
->disk('public')
|
||||
->getUploadedFileNameForStorageUsing(fn () => $this->getUser()->id . '.png')
|
||||
->formatStateUsing(function (FileUpload $fileUpload) {
|
||||
$path = $fileUpload->getDirectory() . '/' . $this->getUser()->id . '.png';
|
||||
if ($fileUpload->getDisk()->exists($path)) {
|
||||
return $path;
|
||||
}
|
||||
})
|
||||
->deleteUploadedFileUsing(function (FileUpload $fileUpload, $file) {
|
||||
if ($file instanceof TemporaryUploadedFile) {
|
||||
return $file->delete();
|
||||
}
|
||||
|
||||
if ($fileUpload->getDisk()->exists($file)) {
|
||||
return $fileUpload->getDisk()->delete($file);
|
||||
}
|
||||
}),
|
||||
]),
|
||||
Tab::make('oauth')
|
||||
->label(trans('profile.tabs.oauth'))
|
||||
->icon('tabler-brand-oauth')
|
||||
->visible(count($oauthSchemas) > 0)
|
||||
->schema(function () use ($oauthSchemas) {
|
||||
$actions = [];
|
||||
|
||||
foreach ($oauthSchemas as $schema) {
|
||||
|
||||
$id = $schema->getId();
|
||||
$name = $schema->getName();
|
||||
|
||||
$color = $schema->getHexColor();
|
||||
$color = is_string($color) ? Color::hex($color) : null;
|
||||
|
||||
$unlink = array_key_exists($id, $this->getUser()->oauth ?? []);
|
||||
|
||||
$actions[] = Action::make("oauth_$id")
|
||||
->label(trans('profile.' . ($unlink ? 'unlink' : 'link'), ['name' => $name]))
|
||||
->icon($unlink ? 'tabler-unlink' : 'tabler-link')
|
||||
->color($color)
|
||||
->action(function (UserUpdateService $updateService) use ($id, $name, $unlink) {
|
||||
if ($unlink) {
|
||||
$oauth = user()?->oauth;
|
||||
unset($oauth[$id]);
|
||||
|
||||
$updateService->handle(user(), ['oauth' => $oauth]);
|
||||
|
||||
$this->fillForm();
|
||||
|
||||
Notification::make()
|
||||
->title(trans('profile.unlinked', ['name' => $name]))
|
||||
->success()
|
||||
->send();
|
||||
} else {
|
||||
redirect(Socialite::with($id)->redirect()->getTargetUrl());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return [Actions::make($actions)];
|
||||
}),
|
||||
Tab::make('2fa')
|
||||
->label(trans('profile.tabs.2fa'))
|
||||
->icon('tabler-shield-lock')
|
||||
->visible(fn () => Filament::hasMultiFactorAuthentication())
|
||||
->schema(collect(Filament::getMultiFactorAuthenticationProviders())
|
||||
->sort(fn (MultiFactorAuthenticationProvider $multiFactorAuthenticationProvider) => $multiFactorAuthenticationProvider->isEnabled(Filament::auth()->user()) ? 0 : 1)
|
||||
->map(fn (MultiFactorAuthenticationProvider $multiFactorAuthenticationProvider) => Group::make($multiFactorAuthenticationProvider->getManagementSchemaComponents())
|
||||
->statePath($multiFactorAuthenticationProvider->getId()))
|
||||
->all()),
|
||||
Tab::make('api_keys')
|
||||
->label(trans('profile.tabs.api_keys'))
|
||||
->icon('tabler-key')
|
||||
->schema([
|
||||
Grid::make(5)
|
||||
->schema([
|
||||
Section::make(trans('profile.create_api_key'))->columnSpan(3)
|
||||
->schema([
|
||||
TextInput::make('description')
|
||||
->label(trans('profile.description'))
|
||||
->live(),
|
||||
TagsInput::make('allowed_ips')
|
||||
->label(trans('profile.allowed_ips'))
|
||||
->live()
|
||||
->splitKeys([',', ' ', 'Tab'])
|
||||
->placeholder('127.0.0.1 or 192.168.1.1')
|
||||
->helperText(trans('profile.allowed_ips_help'))
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->headerActions([
|
||||
Action::make('create_api_key')
|
||||
->label(trans('filament-actions::create.single.modal.actions.create.label'))
|
||||
->disabled(fn (Get $get) => empty($get('description')))
|
||||
->successRedirectUrl(self::getUrl(['tab' => 'api-keys::data::tab'], panel: 'app'))
|
||||
->action(function (Get $get, Action $action, User $user) {
|
||||
$token = $user->createToken(
|
||||
$get('description'),
|
||||
$get('allowed_ips'),
|
||||
);
|
||||
|
||||
Activity::event('user:api-key.create')
|
||||
->actor($user)
|
||||
->subject($user)
|
||||
->subject($token->accessToken)
|
||||
->property('identifier', $token->accessToken->identifier)
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title(trans('profile.api_key_created'))
|
||||
->body($token->accessToken->identifier . $token->plainTextToken)
|
||||
->persistent()
|
||||
->success()
|
||||
->send();
|
||||
|
||||
$action->success();
|
||||
}),
|
||||
]),
|
||||
Section::make(trans('profile.api_keys'))->columnSpan(2)
|
||||
->schema([
|
||||
Repeater::make('api_keys')
|
||||
->hiddenLabel()
|
||||
->inlineLabel(false)
|
||||
->relationship('apiKeys')
|
||||
->addable(false)
|
||||
->itemLabel(fn ($state) => $state['identifier'])
|
||||
->deleteAction(function (Action $action) {
|
||||
$action->requiresConfirmation()->action(function (array $arguments, Repeater $component, User $user) {
|
||||
$items = $component->getState();
|
||||
$key = $items[$arguments['item']];
|
||||
|
||||
$apiKey = ApiKey::find($key['id'] ?? null);
|
||||
if ($apiKey->exists()) {
|
||||
$apiKey->delete();
|
||||
|
||||
Activity::event('user:api-key.delete')
|
||||
->actor($user)
|
||||
->subject($user)
|
||||
->subject($apiKey)
|
||||
->property('identifier', $apiKey->identifier)
|
||||
->log();
|
||||
}
|
||||
|
||||
unset($items[$arguments['item']]);
|
||||
|
||||
$component->state($items);
|
||||
|
||||
$component->callAfterStateUpdated();
|
||||
});
|
||||
})
|
||||
->schema(fn () => [
|
||||
TextEntry::make('memo')
|
||||
->hiddenLabel()
|
||||
->state(fn (ApiKey $key) => $key->memo),
|
||||
])
|
||||
->visible(fn (User $user) => $user->apiKeys()->exists()),
|
||||
|
||||
TextEntry::make('no_api_keys')
|
||||
->state(trans('profile.no_api_keys'))
|
||||
->hiddenLabel()
|
||||
->visible(fn (User $user) => !$user->apiKeys()->exists()),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('ssh_keys')
|
||||
->label(trans('profile.tabs.ssh_keys'))
|
||||
->icon('tabler-lock-code')
|
||||
->schema([
|
||||
Grid::make(5)->schema([
|
||||
Section::make(trans('profile.create_ssh_key'))->columnSpan(3)
|
||||
return $schema
|
||||
->components([
|
||||
Tabs::make()->persistTabInQueryString()
|
||||
->schema([
|
||||
Tab::make('account')
|
||||
->label(trans('profile.tabs.account'))
|
||||
->icon('tabler-user-cog')
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label(trans('profile.name'))
|
||||
->live(),
|
||||
Textarea::make('public_key')
|
||||
->label(trans('profile.public_key'))
|
||||
->autosize()
|
||||
->live(),
|
||||
])
|
||||
->headerActions([
|
||||
Action::make('create_ssh_key')
|
||||
->label(trans('filament-actions::create.single.modal.actions.create.label'))
|
||||
->disabled(fn (Get $get) => empty($get('name')) || empty($get('public_key')))
|
||||
->successRedirectUrl(self::getUrl(['tab' => 'ssh-keys::data::tab'], panel: 'app'))
|
||||
->action(function (Get $get, Action $action, User $user, KeyCreationService $service) {
|
||||
try {
|
||||
$sshKey = $service->handle($user, $get('name'), $get('public_key'));
|
||||
TextInput::make('username')
|
||||
->disabled(fn (User $user) => $user->is_managed_externally)
|
||||
->prefixIcon('tabler-user')
|
||||
->label(trans('profile.username'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->unique(),
|
||||
TextInput::make('email')
|
||||
->disabled(fn (User $user) => $user->is_managed_externally)
|
||||
->prefixIcon('tabler-mail')
|
||||
->label(trans('profile.email'))
|
||||
->email()
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->unique(),
|
||||
TextInput::make('password')
|
||||
->hidden(fn (User $user) => $user->is_managed_externally)
|
||||
->label(trans('profile.password'))
|
||||
->password()
|
||||
->prefixIcon('tabler-password')
|
||||
->revealable(filament()->arePasswordsRevealable())
|
||||
->rule(Password::default())
|
||||
->autocomplete('new-password')
|
||||
->dehydrated(fn ($state) => filled($state))
|
||||
->dehydrateStateUsing(fn ($state) => Hash::make($state))
|
||||
->live(debounce: 500)
|
||||
->same('passwordConfirmation'),
|
||||
TextInput::make('passwordConfirmation')
|
||||
->label(trans('profile.password_confirmation'))
|
||||
->password()
|
||||
->prefixIcon('tabler-password-fingerprint')
|
||||
->revealable(filament()->arePasswordsRevealable())
|
||||
->required()
|
||||
->visible(fn (Get $get) => filled($get('password')))
|
||||
->dehydrated(false),
|
||||
Select::make('timezone')
|
||||
->label(trans('profile.timezone'))
|
||||
->required()
|
||||
->prefixIcon('tabler-clock-pin')
|
||||
->default(config('app.timezone', 'UTC'))
|
||||
->selectablePlaceholder(false)
|
||||
->options(fn () => collect(DateTimeZone::listIdentifiers())->mapWithKeys(fn ($tz) => [$tz => $tz]))
|
||||
->searchable(),
|
||||
Select::make('language')
|
||||
->label(trans('profile.language'))
|
||||
->required()
|
||||
->prefixIcon('tabler-flag')
|
||||
->live()
|
||||
->default('en')
|
||||
->selectablePlaceholder(false)
|
||||
->helperText(fn ($state, LanguageService $languageService) => new HtmlString($languageService->isLanguageTranslated($state) ? ''
|
||||
: trans('profile.language_help', ['state' => $state]) . ' <u><a href="https://crowdin.com/project/pelican-dev/">Update On Crowdin</a></u>'))
|
||||
->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages()),
|
||||
FileUpload::make('avatar')
|
||||
->visible(fn () => config('panel.filament.uploadable-avatars'))
|
||||
->avatar()
|
||||
->imageEditor()
|
||||
->acceptedFileTypes(['image/png'])
|
||||
->directory('avatars')
|
||||
->disk('public')
|
||||
->getUploadedFileNameForStorageUsing(fn () => $this->getUser()->id . '.png')
|
||||
->formatStateUsing(function (FileUpload $fileUpload) {
|
||||
$path = $fileUpload->getDirectory() . '/' . $this->getUser()->id . '.png';
|
||||
if ($fileUpload->getDisk()->exists($path)) {
|
||||
return $path;
|
||||
}
|
||||
})
|
||||
->deleteUploadedFileUsing(function (FileUpload $fileUpload, $file) {
|
||||
if ($file instanceof TemporaryUploadedFile) {
|
||||
return $file->delete();
|
||||
}
|
||||
|
||||
Activity::event('user:ssh-key.create')
|
||||
->actor($user)
|
||||
->subject($user)
|
||||
->subject($sshKey)
|
||||
->property('fingerprint', $sshKey->fingerprint)
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title(trans('profile.ssh_key_created'))
|
||||
->body("SHA256:{$sshKey->fingerprint}")
|
||||
->success()
|
||||
->send();
|
||||
|
||||
$action->success();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title(trans('profile.could_not_create_ssh_key'))
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
$action->failure();
|
||||
if ($fileUpload->getDisk()->exists($file)) {
|
||||
return $fileUpload->getDisk()->delete($file);
|
||||
}
|
||||
}),
|
||||
]),
|
||||
Section::make(trans('profile.ssh_keys'))->columnSpan(2)
|
||||
Tab::make('oauth')
|
||||
->label(trans('profile.tabs.oauth'))
|
||||
->icon('tabler-brand-oauth')
|
||||
->visible(count($oauthSchemas) > 0)
|
||||
->schema(function () use ($oauthSchemas) {
|
||||
$actions = [];
|
||||
|
||||
foreach ($oauthSchemas as $schema) {
|
||||
|
||||
$id = $schema->getId();
|
||||
$name = $schema->getName();
|
||||
|
||||
$unlink = array_key_exists($id, $this->getUser()->oauth ?? []);
|
||||
|
||||
$actions[] = Action::make("oauth_$id")
|
||||
->label(trans('profile.' . ($unlink ? 'unlink' : 'link'), ['name' => $name]))
|
||||
->icon($unlink ? 'tabler-unlink' : 'tabler-link')
|
||||
->color(Color::hex($schema->getHexColor()))
|
||||
->action(function (UserUpdateService $updateService) use ($id, $name, $unlink) {
|
||||
if ($unlink) {
|
||||
$oauth = user()?->oauth;
|
||||
unset($oauth[$id]);
|
||||
|
||||
$updateService->handle(user(), ['oauth' => $oauth]);
|
||||
|
||||
$this->fillForm();
|
||||
|
||||
Notification::make()
|
||||
->title(trans('profile.unlinked', ['name' => $name]))
|
||||
->success()
|
||||
->send();
|
||||
} else {
|
||||
redirect(Socialite::with($id)->redirect()->getTargetUrl());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return [Actions::make($actions)];
|
||||
}),
|
||||
Tab::make('2fa')
|
||||
->label(trans('profile.tabs.2fa'))
|
||||
->icon('tabler-shield-lock')
|
||||
->visible(fn () => Filament::hasMultiFactorAuthentication())
|
||||
->schema(collect(Filament::getMultiFactorAuthenticationProviders())
|
||||
->sort(fn (MultiFactorAuthenticationProvider $multiFactorAuthenticationProvider) => $multiFactorAuthenticationProvider->isEnabled(Filament::auth()->user()) ? 0 : 1)
|
||||
->map(fn (MultiFactorAuthenticationProvider $multiFactorAuthenticationProvider) => Group::make($multiFactorAuthenticationProvider->getManagementSchemaComponents())
|
||||
->statePath($multiFactorAuthenticationProvider->getId()))
|
||||
->all()),
|
||||
Tab::make('api_keys')
|
||||
->label(trans('profile.tabs.api_keys'))
|
||||
->icon('tabler-key')
|
||||
->schema([
|
||||
Repeater::make('ssh_keys')
|
||||
Grid::make(5)
|
||||
->schema([
|
||||
Section::make(trans('profile.create_api_key'))->columnSpan(3)
|
||||
->schema([
|
||||
TextInput::make('description')
|
||||
->label(trans('profile.description'))
|
||||
->live(),
|
||||
TagsInput::make('allowed_ips')
|
||||
->label(trans('profile.allowed_ips'))
|
||||
->live()
|
||||
->splitKeys([',', ' ', 'Tab'])
|
||||
->placeholder('127.0.0.1 or 192.168.1.1')
|
||||
->helperText(trans('profile.allowed_ips_help'))
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->headerActions([
|
||||
Action::make('create_api_key')
|
||||
->label(trans('filament-actions::create.single.modal.actions.create.label'))
|
||||
->disabled(fn (Get $get) => empty($get('description')))
|
||||
->successRedirectUrl(self::getUrl(['tab' => 'api-keys::data::tab'], panel: 'app'))
|
||||
->action(function (Get $get, Action $action, User $user) {
|
||||
$token = $user->createToken(
|
||||
$get('description'),
|
||||
$get('allowed_ips'),
|
||||
);
|
||||
|
||||
Activity::event('user:api-key.create')
|
||||
->actor($user)
|
||||
->subject($user)
|
||||
->subject($token->accessToken)
|
||||
->property('identifier', $token->accessToken->identifier)
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title(trans('profile.api_key_created'))
|
||||
->body($token->accessToken->identifier . $token->plainTextToken)
|
||||
->persistent()
|
||||
->success()
|
||||
->send();
|
||||
|
||||
$action->success();
|
||||
}),
|
||||
]),
|
||||
Section::make(trans('profile.api_keys'))->columnSpan(2)
|
||||
->schema([
|
||||
Repeater::make('api_keys')
|
||||
->hiddenLabel()
|
||||
->inlineLabel(false)
|
||||
->relationship('apiKeys')
|
||||
->addable(false)
|
||||
->itemLabel(fn ($state) => $state['identifier'])
|
||||
->deleteAction(function (Action $action) {
|
||||
$action->requiresConfirmation()->action(function (array $arguments, Repeater $component, User $user) {
|
||||
$items = $component->getState();
|
||||
$key = $items[$arguments['item']];
|
||||
|
||||
$apiKey = ApiKey::find($key['id'] ?? null);
|
||||
if ($apiKey->exists()) {
|
||||
$apiKey->delete();
|
||||
|
||||
Activity::event('user:api-key.delete')
|
||||
->actor($user)
|
||||
->subject($user)
|
||||
->subject($apiKey)
|
||||
->property('identifier', $apiKey->identifier)
|
||||
->log();
|
||||
}
|
||||
|
||||
unset($items[$arguments['item']]);
|
||||
|
||||
$component->state($items);
|
||||
|
||||
$component->callAfterStateUpdated();
|
||||
});
|
||||
})
|
||||
->schema(fn () => [
|
||||
TextEntry::make('memo')
|
||||
->hiddenLabel()
|
||||
->state(fn (ApiKey $key) => $key->memo),
|
||||
])
|
||||
->visible(fn (User $user) => $user->apiKeys()->exists()),
|
||||
|
||||
TextEntry::make('no_api_keys')
|
||||
->state(trans('profile.no_api_keys'))
|
||||
->hiddenLabel()
|
||||
->visible(fn (User $user) => !$user->apiKeys()->exists()),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('ssh_keys')
|
||||
->label(trans('profile.tabs.ssh_keys'))
|
||||
->icon('tabler-lock-code')
|
||||
->schema([
|
||||
Grid::make(5)->schema([
|
||||
Section::make(trans('profile.create_ssh_key'))->columnSpan(3)
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label(trans('profile.name'))
|
||||
->live(),
|
||||
Textarea::make('public_key')
|
||||
->label(trans('profile.public_key'))
|
||||
->autosize()
|
||||
->live(),
|
||||
])
|
||||
->headerActions([
|
||||
Action::make('create_ssh_key')
|
||||
->label(trans('filament-actions::create.single.modal.actions.create.label'))
|
||||
->disabled(fn (Get $get) => empty($get('name')) || empty($get('public_key')))
|
||||
->successRedirectUrl(self::getUrl(['tab' => 'ssh-keys::data::tab'], panel: 'app'))
|
||||
->action(function (Get $get, Action $action, User $user, KeyCreationService $service) {
|
||||
try {
|
||||
$sshKey = $service->handle($user, $get('name'), $get('public_key'));
|
||||
|
||||
Activity::event('user:ssh-key.create')
|
||||
->actor($user)
|
||||
->subject($user)
|
||||
->subject($sshKey)
|
||||
->property('fingerprint', $sshKey->fingerprint)
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title(trans('profile.ssh_key_created'))
|
||||
->body("SHA256:{$sshKey->fingerprint}")
|
||||
->success()
|
||||
->send();
|
||||
|
||||
$action->success();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title(trans('profile.could_not_create_ssh_key'))
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
$action->failure();
|
||||
}
|
||||
}),
|
||||
]),
|
||||
Section::make(trans('profile.ssh_keys'))->columnSpan(2)
|
||||
->schema([
|
||||
Repeater::make('ssh_keys')
|
||||
->hiddenLabel()
|
||||
->inlineLabel(false)
|
||||
->relationship('sshKeys')
|
||||
->addable(false)
|
||||
->itemLabel(fn ($state) => $state['name'])
|
||||
->deleteAction(function (Action $action) {
|
||||
$action->requiresConfirmation()->action(function (array $arguments, Repeater $component, User $user) {
|
||||
$items = $component->getState();
|
||||
$key = $items[$arguments['item']];
|
||||
|
||||
$sshKey = UserSSHKey::find($key['id'] ?? null);
|
||||
if ($sshKey->exists()) {
|
||||
$sshKey->delete();
|
||||
|
||||
Activity::event('user:ssh-key.delete')
|
||||
->actor($user)
|
||||
->subject($user)
|
||||
->subject($sshKey)
|
||||
->property('fingerprint', $sshKey->fingerprint)
|
||||
->log();
|
||||
}
|
||||
|
||||
unset($items[$arguments['item']]);
|
||||
|
||||
$component->state($items);
|
||||
|
||||
$component->callAfterStateUpdated();
|
||||
});
|
||||
})
|
||||
->schema(fn () => [
|
||||
TextEntry::make('fingerprint')
|
||||
->hiddenLabel()
|
||||
->state(fn (UserSSHKey $key) => "SHA256:{$key->fingerprint}"),
|
||||
])
|
||||
->visible(fn (User $user) => $user->sshKeys()->exists()),
|
||||
|
||||
TextEntry::make('no_ssh_keys')
|
||||
->state(trans('profile.no_ssh_keys'))
|
||||
->hiddenLabel()
|
||||
->visible(fn (User $user) => !$user->sshKeys()->exists()),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('activity')
|
||||
->label(trans('profile.tabs.activity'))
|
||||
->icon('tabler-history')
|
||||
->schema([
|
||||
Repeater::make('activity')
|
||||
->hiddenLabel()
|
||||
->inlineLabel(false)
|
||||
->relationship('sshKeys')
|
||||
->deletable(false)
|
||||
->addable(false)
|
||||
->itemLabel(fn ($state) => $state['name'])
|
||||
->deleteAction(function (Action $action) {
|
||||
$action->requiresConfirmation()->action(function (array $arguments, Repeater $component, User $user) {
|
||||
$items = $component->getState();
|
||||
$key = $items[$arguments['item']];
|
||||
|
||||
$sshKey = UserSSHKey::find($key['id'] ?? null);
|
||||
if ($sshKey->exists()) {
|
||||
$sshKey->delete();
|
||||
|
||||
Activity::event('user:ssh-key.delete')
|
||||
->actor($user)
|
||||
->subject($user)
|
||||
->subject($sshKey)
|
||||
->property('fingerprint', $sshKey->fingerprint)
|
||||
->log();
|
||||
}
|
||||
|
||||
unset($items[$arguments['item']]);
|
||||
|
||||
$component->state($items);
|
||||
|
||||
$component->callAfterStateUpdated();
|
||||
});
|
||||
->relationship(null, function (Builder $query) {
|
||||
$query->orderBy('timestamp', 'desc');
|
||||
})
|
||||
->schema(fn () => [
|
||||
TextEntry::make('fingerprint')
|
||||
->schema([
|
||||
TextEntry::make('log')
|
||||
->hiddenLabel()
|
||||
->state(fn (UserSSHKey $key) => "SHA256:{$key->fingerprint}"),
|
||||
])
|
||||
->visible(fn (User $user) => $user->sshKeys()->exists()),
|
||||
|
||||
TextEntry::make('no_ssh_keys')
|
||||
->state(trans('profile.no_ssh_keys'))
|
||||
->hiddenLabel()
|
||||
->visible(fn (User $user) => !$user->sshKeys()->exists()),
|
||||
->state(fn (ActivityLog $log) => new HtmlString($log->htmlable())),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('activity')
|
||||
->label(trans('profile.tabs.activity'))
|
||||
->icon('tabler-history')
|
||||
->schema([
|
||||
Repeater::make('activity')
|
||||
->hiddenLabel()
|
||||
->inlineLabel(false)
|
||||
->deletable(false)
|
||||
->addable(false)
|
||||
->relationship(null, function (Builder $query) {
|
||||
$query->orderBy('timestamp', 'desc');
|
||||
})
|
||||
->schema([
|
||||
TextEntry::make('log')
|
||||
->hiddenLabel()
|
||||
->state(fn (ActivityLog $log) => new HtmlString($log->htmlable())),
|
||||
]),
|
||||
]),
|
||||
Tab::make('customization')
|
||||
->label(trans('profile.tabs.customization'))
|
||||
->icon('tabler-adjustments')
|
||||
->schema([
|
||||
Section::make(trans('profile.dashboard'))
|
||||
->collapsible()
|
||||
->icon('tabler-dashboard')
|
||||
->schema([
|
||||
ToggleButtons::make('dashboard_layout')
|
||||
->label(trans('profile.dashboard_layout'))
|
||||
->inline()
|
||||
->required()
|
||||
->options([
|
||||
'grid' => trans('profile.grid'),
|
||||
'table' => trans('profile.table'),
|
||||
]),
|
||||
ToggleButtons::make('top_navigation')
|
||||
->label(trans('profile.navigation'))
|
||||
->inline()
|
||||
->options([
|
||||
'sidebar' => trans('profile.sidebar'),
|
||||
'topbar' => trans('profile.topbar'),
|
||||
'mixed' => trans('profile.mixed'),
|
||||
]),
|
||||
]),
|
||||
Section::make(trans('profile.console'))
|
||||
->collapsible()
|
||||
->icon('tabler-brand-tabler')
|
||||
->columns(4)
|
||||
->schema([
|
||||
TextInput::make('console_font_size')
|
||||
->label(trans('profile.font_size'))
|
||||
->columnSpan(1)
|
||||
->minValue(1)
|
||||
->numeric()
|
||||
->required()
|
||||
->live()
|
||||
->default(14),
|
||||
Select::make('console_font')
|
||||
->label(trans('profile.font'))
|
||||
->required()
|
||||
->options(function () {
|
||||
$fonts = [
|
||||
'monospace' => 'monospace', //default
|
||||
];
|
||||
Tab::make('customization')
|
||||
->label(trans('profile.tabs.customization'))
|
||||
->icon('tabler-adjustments')
|
||||
->schema([
|
||||
Section::make(trans('profile.dashboard'))
|
||||
->collapsible()
|
||||
->icon('tabler-dashboard')
|
||||
->schema([
|
||||
ToggleButtons::make('dashboard_layout')
|
||||
->label(trans('profile.dashboard_layout'))
|
||||
->inline()
|
||||
->required()
|
||||
->options([
|
||||
'grid' => trans('profile.grid'),
|
||||
'table' => trans('profile.table'),
|
||||
]),
|
||||
ToggleButtons::make('top_navigation')
|
||||
->label(trans('profile.navigation'))
|
||||
->inline()
|
||||
->options([
|
||||
'sidebar' => trans('profile.sidebar'),
|
||||
'topbar' => trans('profile.topbar'),
|
||||
'mixed' => trans('profile.mixed'),
|
||||
]),
|
||||
]),
|
||||
Section::make(trans('profile.console'))
|
||||
->collapsible()
|
||||
->icon('tabler-brand-tabler')
|
||||
->columns(4)
|
||||
->schema([
|
||||
TextInput::make('console_font_size')
|
||||
->label(trans('profile.font_size'))
|
||||
->columnSpan(1)
|
||||
->minValue(1)
|
||||
->numeric()
|
||||
->required()
|
||||
->live()
|
||||
->default(14),
|
||||
Select::make('console_font')
|
||||
->label(trans('profile.font'))
|
||||
->required()
|
||||
->options(function () {
|
||||
$fonts = [
|
||||
'monospace' => 'monospace', //default
|
||||
];
|
||||
|
||||
if (!Storage::disk('public')->exists('fonts')) {
|
||||
Storage::disk('public')->makeDirectory('fonts');
|
||||
$this->fillForm();
|
||||
}
|
||||
if (!Storage::disk('public')->exists('fonts')) {
|
||||
Storage::disk('public')->makeDirectory('fonts');
|
||||
$this->fillForm();
|
||||
}
|
||||
|
||||
foreach (Storage::disk('public')->allFiles('fonts') as $file) {
|
||||
$fileInfo = pathinfo($file);
|
||||
foreach (Storage::disk('public')->allFiles('fonts') as $file) {
|
||||
$fileInfo = pathinfo($file);
|
||||
|
||||
if ($fileInfo['extension'] === 'ttf') {
|
||||
$fonts[$fileInfo['filename']] = $fileInfo['filename'];
|
||||
}
|
||||
}
|
||||
if ($fileInfo['extension'] === 'ttf') {
|
||||
$fonts[$fileInfo['filename']] = $fileInfo['filename'];
|
||||
}
|
||||
}
|
||||
|
||||
return $fonts;
|
||||
})
|
||||
->live()
|
||||
->default('monospace'),
|
||||
TextEntry::make('font_preview')
|
||||
->label(trans('profile.font_preview'))
|
||||
->columnSpan(2)
|
||||
->state(function (Get $get) {
|
||||
$fontName = $get('console_font') ?? 'monospace';
|
||||
$fontSize = $get('console_font_size') . 'px';
|
||||
$style = <<<CSS
|
||||
return $fonts;
|
||||
})
|
||||
->live()
|
||||
->default('monospace'),
|
||||
TextEntry::make('font_preview')
|
||||
->label(trans('profile.font_preview'))
|
||||
->columnSpan(2)
|
||||
->state(function (Get $get) {
|
||||
$fontName = $get('console_font') ?? 'monospace';
|
||||
$fontSize = $get('console_font_size') . 'px';
|
||||
$style = <<<CSS
|
||||
.preview-text {
|
||||
font-family: $fontName;
|
||||
font-size: $fontSize;
|
||||
@@ -517,44 +500,49 @@ class EditProfile extends BaseEditProfile
|
||||
display: block;
|
||||
}
|
||||
CSS;
|
||||
if ($fontName !== 'monospace') {
|
||||
$fontUrl = asset("storage/fonts/$fontName.ttf");
|
||||
$style = <<<CSS
|
||||
if ($fontName !== 'monospace') {
|
||||
$fontUrl = asset("storage/fonts/$fontName.ttf");
|
||||
$style = <<<CSS
|
||||
@font-face {
|
||||
font-family: $fontName;
|
||||
src: url("$fontUrl");
|
||||
}
|
||||
$style
|
||||
CSS;
|
||||
}
|
||||
}
|
||||
|
||||
return new HtmlString(<<<HTML
|
||||
return new HtmlString(<<<HTML
|
||||
<style>
|
||||
{$style}
|
||||
</style>
|
||||
<span class="preview-text">The quick blue pelican jumps over the lazy pterodactyl. :)</span>
|
||||
HTML);
|
||||
}),
|
||||
TextInput::make('console_graph_period')
|
||||
->label(trans('profile.graph_period'))
|
||||
->suffix(trans('profile.seconds'))
|
||||
->hintIcon('tabler-question-mark', trans('profile.graph_period_helper'))
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->default(30)
|
||||
->minValue(10)
|
||||
->maxValue(120)
|
||||
->required(),
|
||||
TextInput::make('console_rows')
|
||||
->label(trans('profile.rows'))
|
||||
->minValue(1)
|
||||
->numeric()
|
||||
->required()
|
||||
->columnSpan(2)
|
||||
->default(30),
|
||||
]),
|
||||
]),
|
||||
];
|
||||
}),
|
||||
TextInput::make('console_graph_period')
|
||||
->label(trans('profile.graph_period'))
|
||||
->suffix(trans('profile.seconds'))
|
||||
->hintIcon('tabler-question-mark', trans('profile.graph_period_helper'))
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->default(30)
|
||||
->minValue(10)
|
||||
->maxValue(120)
|
||||
->required(),
|
||||
TextInput::make('console_rows')
|
||||
->label(trans('profile.rows'))
|
||||
->minValue(1)
|
||||
->numeric()
|
||||
->required()
|
||||
->columnSpan(2)
|
||||
->default(30),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
->operation('edit')
|
||||
->model($this->getUser())
|
||||
->statePath('data')
|
||||
->inlineLabel(!static::isSimple());
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
|
||||
@@ -76,13 +76,10 @@ class Login extends BaseLogin
|
||||
|
||||
$id = $schema->getId();
|
||||
|
||||
$color = $schema->getHexColor();
|
||||
$color = is_string($color) ? Color::hex($color) : null;
|
||||
|
||||
$actions[] = Action::make("oauth_$id")
|
||||
->label($schema->getName())
|
||||
->icon($schema->getIcon())
|
||||
->color($color)
|
||||
->color(Color::hex($schema->getHexColor()))
|
||||
->url(route('auth.oauth.redirect', ['driver' => $id], false));
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ abstract class ServerFormPage extends Page
|
||||
|
||||
protected function authorizeAccess(): void {}
|
||||
|
||||
protected function fillForm(): void
|
||||
protected function fillform(): void
|
||||
{
|
||||
$data = $this->getRecord()->attributesToArray();
|
||||
|
||||
|
||||
@@ -147,15 +147,12 @@ class Startup extends ServerFormPage
|
||||
return parent::canAccess() && user()?->can(SubuserPermission::StartupRead, Filament::getTenant());
|
||||
}
|
||||
|
||||
public function update(?string $state, ServerVariable $serverVariable): void
|
||||
public function update(?string $state, ServerVariable $serverVariable): null
|
||||
{
|
||||
if (!$serverVariable->variable->user_editable) {
|
||||
return;
|
||||
}
|
||||
|
||||
$original = $serverVariable->variable_value;
|
||||
|
||||
try {
|
||||
|
||||
$validator = Validator::make(
|
||||
['variable_value' => $state],
|
||||
['variable_value' => $serverVariable->variable->rules]
|
||||
@@ -168,7 +165,7 @@ class Startup extends ServerFormPage
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
ServerVariable::query()->updateOrCreate([
|
||||
@@ -187,7 +184,6 @@ class Startup extends ServerFormPage
|
||||
])
|
||||
->log();
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title(trans('server/startup.update', ['variable' => $serverVariable->variable->name]))
|
||||
->body(fn () => $original . ' -> ' . $state)
|
||||
@@ -200,6 +196,8 @@ class Startup extends ServerFormPage
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
|
||||
@@ -98,7 +98,7 @@ class DatabaseResource extends Resource
|
||||
->label(trans('server/database.remote')),
|
||||
TextInput::make('max_connections')
|
||||
->label(trans('server/database.max_connections'))
|
||||
->formatStateUsing(fn (Database $database) => $database->max_connections ?: trans('server/database.unlimited')),
|
||||
->formatStateUsing(fn (Database $database) => $database->max_connections === 0 ? $database->max_connections : 'Unlimited'),
|
||||
TextInput::make('jdbc')
|
||||
->label(trans('server/database.jdbc'))
|
||||
->password()->revealable()
|
||||
|
||||
@@ -83,21 +83,15 @@ class BackupController extends ClientApiController
|
||||
// how best to allow a user to create a backup that is locked without also preventing
|
||||
// them from just filling up a server with backups that can never be deleted?
|
||||
if ($request->user()->can(SubuserPermission::BackupDelete, $server)) {
|
||||
$action->setIsLocked($request->boolean('is_locked'));
|
||||
$action->setIsLocked((bool) $request->input('is_locked'));
|
||||
}
|
||||
|
||||
$backup = Activity::event('server:backup.start')->transaction(function ($log) use ($action, $server, $request) {
|
||||
$server->backups()->lockForUpdate();
|
||||
$backup = $action->handle($server, $request->input('name'));
|
||||
|
||||
$backup = $action->handle($server, $request->input('name'));
|
||||
|
||||
$log->subject($backup)->property([
|
||||
'name' => $backup->name,
|
||||
'locked' => $request->boolean('is_locked'),
|
||||
]);
|
||||
|
||||
return $backup;
|
||||
});
|
||||
Activity::event('server:backup.start')
|
||||
->subject($backup)
|
||||
->property(['name' => $backup->name, 'locked' => (bool) $request->input('is_locked')])
|
||||
->log();
|
||||
|
||||
return $this->fractal->item($backup)
|
||||
->transformWith($this->getTransformer(BackupTransformer::class))
|
||||
|
||||
@@ -59,15 +59,12 @@ class DatabaseController extends ClientApiController
|
||||
*/
|
||||
public function store(StoreDatabaseRequest $request, Server $server): array
|
||||
{
|
||||
$database = Activity::event('server:database.create')->transaction(function ($log) use ($request, $server) {
|
||||
$server->databases()->lockForUpdate();
|
||||
$database = $this->deployDatabaseService->handle($server, $request->validated());
|
||||
|
||||
$database = $this->deployDatabaseService->handle($server, $request->validated());
|
||||
|
||||
$log->subject($database)->property('name', $database->database);
|
||||
|
||||
return $database;
|
||||
});
|
||||
Activity::event('server:database.create')
|
||||
->subject($database)
|
||||
->property('name', $database->database)
|
||||
->log();
|
||||
|
||||
return $this->fractal->item($database)
|
||||
->parseIncludes(['password'])
|
||||
|
||||
@@ -107,19 +107,16 @@ class NetworkAllocationController extends ClientApiController
|
||||
*/
|
||||
public function store(NewAllocationRequest $request, Server $server): array
|
||||
{
|
||||
$allocation = Activity::event('server:allocation.create')->transaction(function ($log) use ($server) {
|
||||
$server->allocations()->lockForUpdate();
|
||||
if ($server->allocations()->count() >= $server->allocation_limit) {
|
||||
throw new DisplayException('Cannot assign additional allocations to this server: limit has been reached.');
|
||||
}
|
||||
|
||||
if ($server->allocations->count() >= $server->allocation_limit) {
|
||||
throw new DisplayException('Cannot assign additional allocations to this server: limit has been reached.');
|
||||
}
|
||||
$allocation = $this->assignableAllocationService->handle($server);
|
||||
|
||||
$allocation = $this->assignableAllocationService->handle($server);
|
||||
|
||||
$log->subject($allocation)->property('allocation', $allocation->address);
|
||||
|
||||
return $allocation;
|
||||
});
|
||||
Activity::event('server:allocation.create')
|
||||
->subject($allocation)
|
||||
->property('allocation', $allocation->address)
|
||||
->log();
|
||||
|
||||
return $this->fractal->item($allocation)
|
||||
->transformWith($this->getTransformer(AllocationTransformer::class))
|
||||
|
||||
@@ -131,7 +131,7 @@ class BackupRemoteUploadController extends Controller
|
||||
*/
|
||||
private function getConfiguredMaxPartSize(): int
|
||||
{
|
||||
$maxPartSize = config('backups.max_part_size', self::DEFAULT_MAX_PART_SIZE);
|
||||
$maxPartSize = (int) config('backups.max_part_size', self::DEFAULT_MAX_PART_SIZE);
|
||||
if ($maxPartSize <= 0) {
|
||||
$maxPartSize = self::DEFAULT_MAX_PART_SIZE;
|
||||
}
|
||||
|
||||
@@ -79,13 +79,13 @@ class OAuthController extends Controller
|
||||
|
||||
$user = User::whereEmail($email)->first();
|
||||
if ($user) {
|
||||
if (!$driver->shouldLinkMissingUser($user, $oauthUser)) {
|
||||
if (!$driver->shouldLinkMissingUsers()) {
|
||||
return $this->errorRedirect();
|
||||
}
|
||||
|
||||
$user = $this->oauthService->linkUser($user, $driver, $oauthUser);
|
||||
} else {
|
||||
if (!$driver->shouldCreateMissingUser($oauthUser)) {
|
||||
if (!$driver->shouldCreateMissingUsers()) {
|
||||
return $this->errorRedirect();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Services\Eggs\Sharing\EggImporterService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Throwable;
|
||||
|
||||
class InstallEgg implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public int $timeout = 15;
|
||||
|
||||
public function __construct(public string $downloadUrl) {}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function handle(EggImporterService $eggImporterService): void
|
||||
{
|
||||
try {
|
||||
$eggImporterService->fromUrl($this->downloadUrl);
|
||||
} catch (Throwable $e) {
|
||||
Log::error('Failed to install egg from URL: ' . $this->downloadUrl, ['exception' => $e]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,8 @@
|
||||
|
||||
namespace App\Livewire\Installer;
|
||||
|
||||
use App\Jobs\InstallEgg;
|
||||
use App\Livewire\Installer\Steps\CacheStep;
|
||||
use App\Livewire\Installer\Steps\DatabaseStep;
|
||||
use App\Livewire\Installer\Steps\EggSelectionStep;
|
||||
use App\Livewire\Installer\Steps\EnvironmentStep;
|
||||
use App\Livewire\Installer\Steps\QueueStep;
|
||||
use App\Livewire\Installer\Steps\RequirementsStep;
|
||||
@@ -15,7 +13,6 @@ use App\Services\Helpers\LanguageService;
|
||||
use App\Services\Users\UserCreationService;
|
||||
use App\Traits\CheckMigrationsTrait;
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
use App\Traits\Filament\CanCustomizeSteps;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Facades\Filament;
|
||||
@@ -27,7 +24,6 @@ use Filament\Pages\SimplePage;
|
||||
use Filament\Schemas\Components\Component;
|
||||
use Filament\Schemas\Components\Grid;
|
||||
use Filament\Schemas\Components\Wizard;
|
||||
use Filament\Schemas\Components\Wizard\Step;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Enums\Width;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
@@ -41,7 +37,6 @@ use Illuminate\Support\HtmlString;
|
||||
*/
|
||||
class PanelInstaller extends SimplePage implements HasForms
|
||||
{
|
||||
use CanCustomizeSteps;
|
||||
use CheckMigrationsTrait;
|
||||
use EnvironmentWriterTrait;
|
||||
use InteractsWithForms;
|
||||
@@ -58,7 +53,7 @@ class PanelInstaller extends SimplePage implements HasForms
|
||||
|
||||
public function getMaxContentWidth(): Width|string
|
||||
{
|
||||
return Width::ScreenTwoExtraLarge;
|
||||
return Width::SevenExtraLarge;
|
||||
}
|
||||
|
||||
public static function isInstalled(): bool
|
||||
@@ -73,7 +68,9 @@ class PanelInstaller extends SimplePage implements HasForms
|
||||
$this->form->fill();
|
||||
}
|
||||
|
||||
/** @return Component[] */
|
||||
/** @return Component[]
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function getFormSchema(): array
|
||||
{
|
||||
return [
|
||||
@@ -81,9 +78,20 @@ class PanelInstaller extends SimplePage implements HasForms
|
||||
->schema([
|
||||
$this->getLanguageComponent(),
|
||||
]),
|
||||
Wizard::make($this->getSteps())
|
||||
Wizard::make([
|
||||
RequirementsStep::make(),
|
||||
EnvironmentStep::make($this),
|
||||
DatabaseStep::make($this),
|
||||
CacheStep::make($this),
|
||||
QueueStep::make($this),
|
||||
SessionStep::make(),
|
||||
])
|
||||
->persistStepInQueryString()
|
||||
->nextAction(fn (Action $action) => $action->keyBindings('enter'))
|
||||
->nextAction(function (Action $action) {
|
||||
$action
|
||||
->label(trans('installer.next_step'))
|
||||
->keyBindings('enter');
|
||||
})
|
||||
->submitAction(new HtmlString(Blade::render(<<<'BLADE'
|
||||
<x-filament::button
|
||||
type="submit"
|
||||
@@ -91,30 +99,12 @@ class PanelInstaller extends SimplePage implements HasForms
|
||||
wire:loading.attr="disabled"
|
||||
>
|
||||
{{ trans('installer.finish') }}
|
||||
<x-filament::loading-indicator wire:loading class="h-4 w-4" />
|
||||
<span wire:loading><x-filament::loading-indicator class="h-4 w-4" /></span>
|
||||
</x-filament::button>
|
||||
BLADE))),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Step[]
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function getDefaultSteps(): array
|
||||
{
|
||||
return [
|
||||
RequirementsStep::make(),
|
||||
EnvironmentStep::make($this),
|
||||
DatabaseStep::make($this),
|
||||
EggSelectionStep::make(),
|
||||
CacheStep::make($this),
|
||||
QueueStep::make($this),
|
||||
SessionStep::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getLanguageComponent(): Component
|
||||
{
|
||||
return Select::make('language')
|
||||
@@ -151,9 +141,6 @@ class PanelInstaller extends SimplePage implements HasForms
|
||||
// Write session data at the very end to avoid "page expired" errors
|
||||
$this->writeToEnv('env_session');
|
||||
|
||||
// Install selected eggs
|
||||
$this->installEggs();
|
||||
|
||||
// Redirect to admin panel
|
||||
$this->redirect(Filament::getPanel('admin')->getUrl());
|
||||
} catch (Halt) {
|
||||
@@ -178,6 +165,8 @@ class PanelInstaller extends SimplePage implements HasForms
|
||||
|
||||
throw new Halt(trans('installer.exceptions.write_env'));
|
||||
}
|
||||
|
||||
Artisan::call('config:clear');
|
||||
}
|
||||
|
||||
public function runMigrations(): void
|
||||
@@ -231,36 +220,4 @@ class PanelInstaller extends SimplePage implements HasForms
|
||||
throw new Halt(trans('installer.exceptions.create_user'));
|
||||
}
|
||||
}
|
||||
|
||||
public function installEggs(): void
|
||||
{
|
||||
try {
|
||||
$selectedEggs = array_get($this->data, 'eggs', []);
|
||||
if (!$selectedEggs) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($selectedEggs as $category => $eggs) {
|
||||
foreach ($eggs as $downloadUrl) {
|
||||
InstallEgg::dispatch($downloadUrl);
|
||||
}
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title(trans('installer.egg.background_install_started'))
|
||||
->body(trans('installer.egg.background_install_description', ['count' => array_sum(array_map('count', $selectedEggs))]))
|
||||
->success()
|
||||
->persistent()
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
report($exception);
|
||||
|
||||
Notification::make()
|
||||
->title(trans('installer.egg.exceptions.installation_failed'))
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->persistent()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Installer\Steps;
|
||||
|
||||
use App\Console\Commands\Egg\UpdateEggIndexCommand;
|
||||
use Exception;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Schemas\Components\Tabs;
|
||||
use Filament\Schemas\Components\Tabs\Tab;
|
||||
use Filament\Schemas\Components\Wizard\Step;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
class EggSelectionStep
|
||||
{
|
||||
public static function make(): Step
|
||||
{
|
||||
try {
|
||||
Artisan::call(UpdateEggIndexCommand::class);
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title(trans('installer.egg.exceptions.failed_to_update'))
|
||||
->icon('tabler-egg')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->persistent()
|
||||
->send();
|
||||
}
|
||||
|
||||
$eggs = cache()->get('eggs.index', []);
|
||||
|
||||
$categories = array_keys($eggs);
|
||||
|
||||
$tabs = array_map(function (string $label) use ($eggs) {
|
||||
$id = str_slug($label, '_');
|
||||
$eggCount = count($eggs[$label]);
|
||||
|
||||
return Tab::make($id)
|
||||
->label($label)
|
||||
->badge($eggCount)
|
||||
->schema([
|
||||
CheckboxList::make("eggs.$id")
|
||||
->hiddenLabel()
|
||||
->options(fn () => array_sort($eggs[$label]))
|
||||
->searchable($eggCount > 0)
|
||||
->bulkToggleable($eggCount > 0)
|
||||
->columns(4),
|
||||
]);
|
||||
}, $categories);
|
||||
|
||||
if (empty($tabs)) {
|
||||
$tabs[] = Tab::make('no_eggs')
|
||||
->label(trans('installer.egg.no_eggs'))
|
||||
->schema([
|
||||
TextEntry::make('no_eggs')
|
||||
->hiddenLabel()
|
||||
->state(trans('installer.egg.exceptions.no_eggs')),
|
||||
]);
|
||||
}
|
||||
|
||||
return Step::make('egg')
|
||||
->label(trans('installer.egg.title'))
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Tabs::make('egg_tabs')
|
||||
->tabs($tabs),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ use PDOException;
|
||||
* @property string $username
|
||||
* @property string $remote
|
||||
* @property string $password
|
||||
* @property ?int $max_connections
|
||||
* @property int $max_connections
|
||||
* @property string $jdbc
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
|
||||
@@ -110,7 +110,7 @@ class Node extends Model implements Validatable
|
||||
'daemon_listen' => ['required', 'numeric', 'between:1,65535'],
|
||||
'daemon_connect' => ['required', 'numeric', 'between:1,65535'],
|
||||
'maintenance_mode' => ['boolean'],
|
||||
'upload_size' => ['int', 'min:1'],
|
||||
'upload_size' => ['int', 'between:1,1024'],
|
||||
'tags' => ['array'],
|
||||
];
|
||||
|
||||
|
||||
@@ -205,7 +205,7 @@ class Plugin extends Model implements HasPluginSettings
|
||||
|
||||
public function canDisable(): bool
|
||||
{
|
||||
return $this->status === PluginStatus::Enabled || $this->status === PluginStatus::Incompatible;
|
||||
return $this->status !== PluginStatus::Disabled && $this->status !== PluginStatus::NotInstalled && $this->isCompatible();
|
||||
}
|
||||
|
||||
public function isCompatible(): bool
|
||||
@@ -306,10 +306,11 @@ class Plugin extends Model implements HasPluginSettings
|
||||
public function hasSettings(): bool
|
||||
{
|
||||
try {
|
||||
$pluginObject = new ($this->fullClass());
|
||||
$pluginObject = filament($this->id);
|
||||
|
||||
return $pluginObject instanceof HasPluginSettings;
|
||||
} catch (Exception) {
|
||||
// Plugin is not loaded on the current panel, so no settings
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -319,12 +320,13 @@ class Plugin extends Model implements HasPluginSettings
|
||||
public function getSettingsForm(): array
|
||||
{
|
||||
try {
|
||||
$pluginObject = new ($this->fullClass());
|
||||
$pluginObject = filament($this->id);
|
||||
|
||||
if ($pluginObject instanceof HasPluginSettings) {
|
||||
return $pluginObject->getSettingsForm();
|
||||
}
|
||||
} catch (Exception) {
|
||||
// Plugin is not loaded on the current panel, so no settings
|
||||
}
|
||||
|
||||
return [];
|
||||
@@ -334,12 +336,13 @@ class Plugin extends Model implements HasPluginSettings
|
||||
public function saveSettings(array $data): void
|
||||
{
|
||||
try {
|
||||
$pluginObject = new ($this->fullClass());
|
||||
$pluginObject = filament($this->id);
|
||||
|
||||
if ($pluginObject instanceof HasPluginSettings) {
|
||||
$pluginObject->saveSettings($data);
|
||||
}
|
||||
} catch (Exception) {
|
||||
// Plugin is not loaded on the current panel, so no settings
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Context;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rules\In;
|
||||
use ResourceBundle;
|
||||
@@ -216,7 +215,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
{
|
||||
$rules = self::getValidationRules();
|
||||
|
||||
$rules['language'][] = new In(ResourceBundle::getLocales(''));
|
||||
$rules['language'][] = new In(array_values(array_filter(ResourceBundle::getLocales(''), fn ($lang) => preg_match('/^[a-z]{2}$/', $lang))));
|
||||
$rules['timezone'][] = new In(DateTimeZone::listIdentifiers());
|
||||
|
||||
return $rules;
|
||||
@@ -334,8 +333,12 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
return !$key ? $customization : $customization[$key->value];
|
||||
}
|
||||
|
||||
protected function hasPermission(Server $server, string $permission = ''): bool
|
||||
protected function checkPermission(Server $server, string|SubuserPermission $permission = ''): bool
|
||||
{
|
||||
if ($permission instanceof SubuserPermission) {
|
||||
$permission = $permission->value;
|
||||
}
|
||||
|
||||
if ($this->canned('update', $server) || $server->owner_id === $this->id) {
|
||||
return true;
|
||||
}
|
||||
@@ -353,17 +356,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
return in_array($permission, $subuser->permissions);
|
||||
}
|
||||
|
||||
protected function checkPermission(Server $server, string|SubuserPermission $permission = ''): bool
|
||||
{
|
||||
if ($permission instanceof SubuserPermission) {
|
||||
$permission = $permission->value;
|
||||
}
|
||||
|
||||
$contextKey = "users.$this->id.servers.$server->id.$permission";
|
||||
|
||||
return Context::remember($contextKey, fn () => $this->hasPermission($server, $permission));
|
||||
}
|
||||
|
||||
/**
|
||||
* Laravel's policies strictly check for the existence of a real method,
|
||||
* this checks if the ability is one of our permissions and then checks if the user can do it or not
|
||||
|
||||
@@ -3,69 +3,34 @@
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Enums\SubuserPermission;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AllocationPolicy
|
||||
{
|
||||
use DefaultAdminPolicies {
|
||||
viewAny as adminViewAny;
|
||||
view as adminView;
|
||||
create as adminCreate;
|
||||
update as adminUpdate;
|
||||
delete as adminDelete;
|
||||
deleteAny as adminDeleteAny;
|
||||
}
|
||||
|
||||
protected string $modelName = 'allocation';
|
||||
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
/** @var ?Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $server ? $user->can(SubuserPermission::AllocationRead, $server) : $this->adminViewAny($user);
|
||||
return $user->can(SubuserPermission::AllocationRead, Filament::getTenant());
|
||||
}
|
||||
|
||||
public function view(User $user, Model $model): bool
|
||||
public function view(User $user, Model $record): bool
|
||||
{
|
||||
/** @var ?Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $server ? $user->can(SubuserPermission::AllocationRead, $server) : $this->adminView($user, $model);
|
||||
return $user->can(SubuserPermission::AllocationRead, Filament::getTenant());
|
||||
}
|
||||
|
||||
public function create(User $user): bool
|
||||
{
|
||||
/** @var ?Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $server ? $user->can(SubuserPermission::AllocationCreate, $server) : $this->adminCreate($user);
|
||||
return $user->can(SubuserPermission::AllocationCreate, Filament::getTenant());
|
||||
}
|
||||
|
||||
public function update(User $user, Model $model): bool
|
||||
public function edit(User $user, Model $record): bool
|
||||
{
|
||||
/** @var ?Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $server ? $user->can(SubuserPermission::AllocationUpdate, $server) : $this->adminUpdate($user, $model);
|
||||
return $user->can(SubuserPermission::AllocationUpdate, Filament::getTenant());
|
||||
}
|
||||
|
||||
public function delete(User $user, Model $model): bool
|
||||
public function delete(User $user, Model $record): bool
|
||||
{
|
||||
/** @var ?Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $server ? $user->can(SubuserPermission::AllocationDelete, $server) : $this->adminDelete($user, $model);
|
||||
}
|
||||
|
||||
public function deleteAny(User $user): bool
|
||||
{
|
||||
/** @var ?Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $server ? $user->can(SubuserPermission::AllocationDelete, $server) : $this->adminDeleteAny($user);
|
||||
return $user->can(SubuserPermission::AllocationDelete, Filament::getTenant());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class BackupPolicy
|
||||
return $user->can(SubuserPermission::BackupRead, Filament::getTenant());
|
||||
}
|
||||
|
||||
public function view(User $user, Model $model): bool
|
||||
public function view(User $user, Model $record): bool
|
||||
{
|
||||
return $user->can(SubuserPermission::BackupRead, Filament::getTenant());
|
||||
}
|
||||
@@ -24,7 +24,7 @@ class BackupPolicy
|
||||
return $user->can(SubuserPermission::BackupCreate, Filament::getTenant());
|
||||
}
|
||||
|
||||
public function delete(User $user, Model $model): bool
|
||||
public function delete(User $user, Model $record): bool
|
||||
{
|
||||
return $user->can(SubuserPermission::BackupDelete, Filament::getTenant());
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class DatabasePolicy
|
||||
return $user->can(SubuserPermission::DatabaseRead, Filament::getTenant());
|
||||
}
|
||||
|
||||
public function view(User $user, Model $model): bool
|
||||
public function view(User $user, Model $record): bool
|
||||
{
|
||||
return $user->can(SubuserPermission::DatabaseRead, Filament::getTenant());
|
||||
}
|
||||
@@ -24,12 +24,12 @@ class DatabasePolicy
|
||||
return $user->can(SubuserPermission::DatabaseCreate, Filament::getTenant());
|
||||
}
|
||||
|
||||
public function update(User $user, Model $model): bool
|
||||
public function edit(User $user, Model $record): bool
|
||||
{
|
||||
return $user->can(SubuserPermission::DatabaseUpdate, Filament::getTenant());
|
||||
}
|
||||
|
||||
public function delete(User $user, Model $model): bool
|
||||
public function delete(User $user, Model $record): bool
|
||||
{
|
||||
return $user->can(SubuserPermission::DatabaseDelete, Filament::getTenant());
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class FilePolicy
|
||||
return $user->can(SubuserPermission::FileRead, Filament::getTenant());
|
||||
}
|
||||
|
||||
public function view(User $user, Model $model): bool
|
||||
public function view(User $user, Model $record): bool
|
||||
{
|
||||
return $user->can(SubuserPermission::FileReadContent, Filament::getTenant());
|
||||
}
|
||||
@@ -24,12 +24,12 @@ class FilePolicy
|
||||
return $user->can(SubuserPermission::FileCreate, Filament::getTenant());
|
||||
}
|
||||
|
||||
public function update(User $user, Model $model): bool
|
||||
public function edit(User $user, Model $record): bool
|
||||
{
|
||||
return $user->can(SubuserPermission::FileUpdate, Filament::getTenant());
|
||||
}
|
||||
|
||||
public function delete(User $user, Model $model): bool
|
||||
public function delete(User $user, Model $record): bool
|
||||
{
|
||||
return $user->can(SubuserPermission::FileDelete, Filament::getTenant());
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class SchedulePolicy
|
||||
return $user->can(SubuserPermission::ScheduleRead, Filament::getTenant());
|
||||
}
|
||||
|
||||
public function view(User $user, Model $model): bool
|
||||
public function view(User $user, Model $record): bool
|
||||
{
|
||||
return $user->can(SubuserPermission::ScheduleRead, Filament::getTenant());
|
||||
}
|
||||
@@ -24,12 +24,12 @@ class SchedulePolicy
|
||||
return $user->can(SubuserPermission::ScheduleCreate, Filament::getTenant());
|
||||
}
|
||||
|
||||
public function update(User $user, Model $model): bool
|
||||
public function edit(User $user, Model $record): bool
|
||||
{
|
||||
return $user->can(SubuserPermission::ScheduleUpdate, Filament::getTenant());
|
||||
}
|
||||
|
||||
public function delete(User $user, Model $model): bool
|
||||
public function delete(User $user, Model $record): bool
|
||||
{
|
||||
return $user->can(SubuserPermission::ScheduleDelete, Filament::getTenant());
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class SubuserPolicy
|
||||
return $user->can(SubuserPermission::UserRead, Filament::getTenant());
|
||||
}
|
||||
|
||||
public function view(User $user, Model $model): bool
|
||||
public function view(User $user, Model $record): bool
|
||||
{
|
||||
return $user->can(SubuserPermission::UserRead, Filament::getTenant());
|
||||
}
|
||||
@@ -24,12 +24,12 @@ class SubuserPolicy
|
||||
return $user->can(SubuserPermission::UserCreate, Filament::getTenant());
|
||||
}
|
||||
|
||||
public function update(User $user, Model $model): bool
|
||||
public function edit(User $user, Model $record): bool
|
||||
{
|
||||
return $user->can(SubuserPermission::UserUpdate, Filament::getTenant());
|
||||
}
|
||||
|
||||
public function delete(User $user, Model $model): bool
|
||||
public function delete(User $user, Model $record): bool
|
||||
{
|
||||
return $user->can(SubuserPermission::UserDelete, Filament::getTenant());
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Enums\ResourceLimit;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Foundation\Http\Middleware\TrimStrings;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
@@ -99,7 +98,5 @@ class RouteServiceProvider extends ServiceProvider
|
||||
config('http.rate_limit.application')
|
||||
)->by($key);
|
||||
});
|
||||
|
||||
ResourceLimit::boot();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,11 +31,11 @@ class DaemonFileRepository extends DaemonRepository
|
||||
throw new FileSizeTooLargeException();
|
||||
}
|
||||
|
||||
if ($response->status() === 400) {
|
||||
if ($response->getStatusCode() === 400) {
|
||||
throw new FileNotEditableException();
|
||||
}
|
||||
|
||||
if ($response->status() === 404) {
|
||||
if ($response->getStatusCode() === 404) {
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ class DaemonFileRepository extends DaemonRepository
|
||||
->withBody($content)
|
||||
->post("/api/servers/{$this->server->uuid}/files/write");
|
||||
|
||||
if ($response->status() === 400) {
|
||||
if ($response->getStatusCode() === 400) {
|
||||
throw new FileExistsException();
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ class DaemonFileRepository extends DaemonRepository
|
||||
]
|
||||
);
|
||||
|
||||
if ($response->status() === 400) {
|
||||
if ($response->getStatusCode() === 400) {
|
||||
throw new FileExistsException();
|
||||
}
|
||||
|
||||
|
||||
@@ -133,9 +133,6 @@ class DaemonServerRepository extends DaemonRepository
|
||||
* make it easier to revoke tokens on the fly. This ensures that the JTI key is formatted
|
||||
* correctly and avoids any costly mistakes in the codebase.
|
||||
*
|
||||
* @deprecated
|
||||
* @see self::deauthorize()
|
||||
*
|
||||
* @throws ConnectionException
|
||||
*/
|
||||
public function revokeUserJTI(int $id): void
|
||||
@@ -146,21 +143,6 @@ class DaemonServerRepository extends DaemonRepository
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deauthorizes a user (disconnects websockets and SFTP) on the Wings instance for the server.
|
||||
*
|
||||
* @throws ConnectionException
|
||||
*/
|
||||
public function deauthorize(string $user): void
|
||||
{
|
||||
$this->getHttpClient()->post('/api/deauthorize-user', [
|
||||
'json' => [
|
||||
'user' => $user,
|
||||
'servers' => [$this->server->uuid],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function getInstallLogs(): string
|
||||
{
|
||||
return $this->getHttpClient()
|
||||
|
||||
@@ -7,21 +7,17 @@ use App\Exceptions\Service\InvalidFileUploadException;
|
||||
use App\Models\Plugin;
|
||||
use Composer\Autoload\ClassLoader;
|
||||
use Exception;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Panel;
|
||||
use Illuminate\Console\Application as ConsoleApplication;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Migrations\Migrator;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Str;
|
||||
use JsonException;
|
||||
use Spatie\TemporaryDirectory\TemporaryDirectory;
|
||||
use ZipArchive;
|
||||
|
||||
@@ -161,8 +157,6 @@ class PluginService
|
||||
/**
|
||||
* @param null|array<string, string> $newPackages
|
||||
* @param null|array<string, string> $oldPackages
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function manageComposerPackages(?array $newPackages = [], ?array $oldPackages = null): void
|
||||
{
|
||||
@@ -214,55 +208,48 @@ class PluginService
|
||||
}
|
||||
}
|
||||
|
||||
/** @throws Exception */
|
||||
public function runPluginMigrations(Plugin $plugin): void
|
||||
{
|
||||
$migrations = plugin_path($plugin->id, 'database', 'migrations');
|
||||
if (file_exists($migrations)) {
|
||||
try {
|
||||
$migrator = $this->app->make(Migrator::class);
|
||||
$migrator->run($migrations);
|
||||
} catch (Exception $exception) {
|
||||
throw new Exception("Could not run migrations': " . $exception->getMessage());
|
||||
$success = Artisan::call('migrate', ['--realpath' => true, '--path' => $migrations, '--force' => true]) === 0;
|
||||
|
||||
if (!$success) {
|
||||
throw new Exception("Could not run migrations for plugin '{$plugin->id}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @throws Exception */
|
||||
public function rollbackPluginMigrations(Plugin $plugin): void
|
||||
{
|
||||
$migrations = plugin_path($plugin->id, 'database', 'migrations');
|
||||
if (file_exists($migrations)) {
|
||||
try {
|
||||
$migrator = $this->app->make(Migrator::class);
|
||||
$migrator->reset($migrations);
|
||||
} catch (Exception $exception) {
|
||||
throw new Exception("Could not rollback migrations': " . $exception->getMessage());
|
||||
$success = Artisan::call('migrate:rollback', ['--realpath' => true, '--path' => $migrations, '--force' => true]) === 0;
|
||||
|
||||
if (!$success) {
|
||||
throw new Exception("Could not rollback migrations for plugin '{$plugin->id}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @throws Exception */
|
||||
public function runPluginSeeder(Plugin $plugin): void
|
||||
{
|
||||
$seeder = $plugin->getSeeder();
|
||||
if ($seeder) {
|
||||
try {
|
||||
$seederObject = $this->app->make($seeder)->setContainer($this->app);
|
||||
$success = Artisan::call('db:seed', ['--class' => $seeder, '--force' => true]) === 0;
|
||||
|
||||
Model::unguarded(fn () => $seederObject->__invoke());
|
||||
} catch (Exception $exception) {
|
||||
throw new Exception('Could not run seeder: ' . $exception->getMessage());
|
||||
if (!$success) {
|
||||
throw new Exception("Could not run seeder for plugin '{$plugin->id}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function buildAssets(bool $throw = false): bool
|
||||
public function buildAssets(): bool
|
||||
{
|
||||
try {
|
||||
$result = Process::path(base_path())->timeout(300)->run('yarn install');
|
||||
if ($result->failed()) {
|
||||
throw new Exception('Could not install yarn dependencies: ' . $result->errorOutput());
|
||||
throw new Exception('Could not install dependencies: ' . $result->errorOutput());
|
||||
}
|
||||
|
||||
$result = Process::path(base_path())->timeout(600)->run('yarn build');
|
||||
@@ -272,17 +259,16 @@ class PluginService
|
||||
|
||||
return true;
|
||||
} catch (Exception $exception) {
|
||||
if ($throw || $this->isDevModeActive()) {
|
||||
if ($this->isDevModeActive()) {
|
||||
throw ($exception);
|
||||
}
|
||||
|
||||
Log::warning($exception->getMessage(), ['exception' => $exception]);
|
||||
report($exception);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @throws Exception */
|
||||
public function installPlugin(Plugin $plugin, bool $enable = true): void
|
||||
{
|
||||
try {
|
||||
@@ -296,40 +282,32 @@ class PluginService
|
||||
}
|
||||
}
|
||||
|
||||
$this->buildAssets($plugin->isTheme());
|
||||
$this->buildAssets();
|
||||
|
||||
$this->runPluginMigrations($plugin);
|
||||
|
||||
$this->runPluginSeeder($plugin);
|
||||
|
||||
foreach (Filament::getPanels() as $panel) {
|
||||
$panel->clearCachedComponents();
|
||||
}
|
||||
} catch (Exception $exception) {
|
||||
$this->handlePluginException($plugin, $exception, true);
|
||||
$this->handlePluginException($plugin, $exception);
|
||||
}
|
||||
}
|
||||
|
||||
/** @throws Exception */
|
||||
public function updatePlugin(Plugin $plugin): void
|
||||
{
|
||||
try {
|
||||
$downloadUrl = $plugin->getDownloadUrlForUpdate();
|
||||
if (!$downloadUrl) {
|
||||
throw new Exception('No download url found.');
|
||||
if ($downloadUrl) {
|
||||
$this->downloadPluginFromUrl($downloadUrl, true);
|
||||
|
||||
$this->installPlugin($plugin, false);
|
||||
|
||||
cache()->forget("plugins.$plugin->id.update");
|
||||
}
|
||||
|
||||
$this->downloadPluginFromUrl($downloadUrl, true);
|
||||
|
||||
$this->installPlugin($plugin, false);
|
||||
|
||||
cache()->forget("plugins.$plugin->id.update");
|
||||
} catch (Exception $exception) {
|
||||
$this->handlePluginException($plugin, $exception, true);
|
||||
$this->handlePluginException($plugin, $exception);
|
||||
}
|
||||
}
|
||||
|
||||
/** @throws Exception */
|
||||
public function uninstallPlugin(Plugin $plugin, bool $deleteFiles = false): void
|
||||
{
|
||||
try {
|
||||
@@ -346,17 +324,11 @@ class PluginService
|
||||
$this->buildAssets();
|
||||
|
||||
$this->manageComposerPackages(oldPackages: $pluginPackages);
|
||||
|
||||
// This throws an error when not called with qualifier
|
||||
foreach (\Filament\Facades\Filament::getPanels() as $panel) {
|
||||
$panel->clearCachedComponents();
|
||||
}
|
||||
} catch (Exception $exception) {
|
||||
$this->handlePluginException($plugin, $exception, true);
|
||||
$this->handlePluginException($plugin, $exception);
|
||||
}
|
||||
}
|
||||
|
||||
/** @throws Exception */
|
||||
public function downloadPluginFromFile(UploadedFile $file, bool $cleanDownload = false): void
|
||||
{
|
||||
// Validate file size to prevent zip bombs
|
||||
@@ -396,7 +368,6 @@ class PluginService
|
||||
$zip->close();
|
||||
}
|
||||
|
||||
/** @throws Exception */
|
||||
public function downloadPluginFromUrl(string $url, bool $cleanDownload = false): void
|
||||
{
|
||||
$info = pathinfo($url);
|
||||
@@ -458,11 +429,7 @@ class PluginService
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $order
|
||||
*
|
||||
* @throws JsonException
|
||||
*/
|
||||
/** @param array<int, string> $order */
|
||||
public function updateLoadOrder(array $order): void
|
||||
{
|
||||
foreach ($order as $i => $plugin) {
|
||||
@@ -506,14 +473,14 @@ class PluginService
|
||||
return config('panel.plugin.dev_mode', false);
|
||||
}
|
||||
|
||||
private function handlePluginException(string|Plugin $plugin, Exception $exception, bool $throw = false): void
|
||||
private function handlePluginException(string|Plugin $plugin, Exception $exception): void
|
||||
{
|
||||
$this->setStatus($plugin, PluginStatus::Errored, $exception->getMessage());
|
||||
|
||||
if ($throw || $this->isDevModeActive()) {
|
||||
if ($this->isDevModeActive()) {
|
||||
throw ($exception);
|
||||
}
|
||||
|
||||
report($exception);
|
||||
|
||||
$this->setStatus($plugin, PluginStatus::Errored, $exception->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ class DetailsModificationService
|
||||
// websockets.
|
||||
if ($server->owner_id !== $owner) {
|
||||
try {
|
||||
$this->serverRepository->setServer($server)->deauthorize($server->user->uuid);
|
||||
$this->serverRepository->setServer($server)->revokeUserJTI($owner);
|
||||
} catch (ConnectionException) {
|
||||
// Do nothing. A failure here is not ideal, but it is likely to be caused by daemon
|
||||
// being offline, or in an entirely broken state. Remember, these tokens reset every
|
||||
|
||||
@@ -78,10 +78,7 @@ class ServerDeletionService
|
||||
}
|
||||
}
|
||||
|
||||
$server->allocations()->update([
|
||||
'server_id' => null,
|
||||
'notes' => null,
|
||||
]);
|
||||
$server->allocations()->update(['server_id' => null]);
|
||||
|
||||
$server->delete();
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Services\Servers;
|
||||
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Backup;
|
||||
use App\Models\Node;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServerTransfer;
|
||||
@@ -24,19 +23,11 @@ class TransferServerService
|
||||
private NodeJWTService $nodeJWTService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param string[] $backup_uuids
|
||||
*/
|
||||
private function notify(ServerTransfer $transfer, UnencryptedToken $token, array $backup_uuids = []): void
|
||||
private function notify(ServerTransfer $transfer, UnencryptedToken $token): void
|
||||
{
|
||||
$backups = [];
|
||||
if (config('backups.default') === Backup::ADAPTER_DAEMON) {
|
||||
$backups = $backup_uuids;
|
||||
}
|
||||
Http::daemon($transfer->oldNode)->post("/api/servers/{$transfer->server->uuid}/transfer", [
|
||||
'url' => $transfer->newNode->getConnectionAddress() . '/api/transfers',
|
||||
'token' => 'Bearer ' . $token->toString(),
|
||||
'backups' => $backups,
|
||||
'server' => [
|
||||
'uuid' => $transfer->server->uuid,
|
||||
'start_on_completion' => false,
|
||||
@@ -48,11 +39,10 @@ class TransferServerService
|
||||
* Starts a transfer of a server to a new node.
|
||||
*
|
||||
* @param int[] $additional_allocations
|
||||
* @param string[] $backup_uuid
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function handle(Server $server, int $node_id, ?int $allocation_id = null, ?array $additional_allocations = [], ?array $backup_uuid = []): bool
|
||||
public function handle(Server $server, int $node_id, ?int $allocation_id = null, ?array $additional_allocations = []): bool
|
||||
{
|
||||
$additional_allocations = array_map(intval(...), $additional_allocations);
|
||||
|
||||
@@ -103,7 +93,7 @@ class TransferServerService
|
||||
->handle($transfer->newNode, $server->uuid, 'sha256');
|
||||
|
||||
// Notify the source node of the pending outgoing transfer.
|
||||
$this->notify($transfer, $token, $backup_uuid);
|
||||
$this->notify($transfer, $token);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ class SubuserDeletionService
|
||||
event(new SubUserRemoved($subuser->server, $subuser->user));
|
||||
|
||||
try {
|
||||
$this->serverRepository->setServer($server)->deauthorize($subuser->user->uuid);
|
||||
$this->serverRepository->setServer($server)->revokeUserJTI($subuser->user_id);
|
||||
} catch (ConnectionException $exception) {
|
||||
// Don't block this request if we can't connect to the daemon instance.
|
||||
logger()->warning($exception, ['user_id' => $subuser->user_id, 'server_id' => $server->id]);
|
||||
|
||||
@@ -46,7 +46,7 @@ class SubuserUpdateService
|
||||
$subuser->update(['permissions' => $cleanedPermissions]);
|
||||
|
||||
try {
|
||||
$this->serverRepository->setServer($server)->deauthorize($subuser->user->uuid);
|
||||
$this->serverRepository->setServer($server)->revokeUserJTI($subuser->user_id);
|
||||
} catch (ConnectionException $exception) {
|
||||
// Don't block this request if we can't connect to the daemon instance. Chances are it is
|
||||
// offline and the token will be invalid once daemon boots back.
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Traits;
|
||||
|
||||
use Illuminate\Support\Env;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use RuntimeException;
|
||||
|
||||
trait EnvironmentWriterTrait
|
||||
@@ -18,6 +17,5 @@ trait EnvironmentWriterTrait
|
||||
public function writeToEnvironment(array $values = []): void
|
||||
{
|
||||
Env::writeVariables($values, base_path('.env'), true);
|
||||
Artisan::call('config:clear');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,6 @@ trait CanCustomizePages
|
||||
/** @return array<string, PageRegistration> */
|
||||
public static function getPages(): array
|
||||
{
|
||||
return array_unique(array_merge(static::$customPages, static::getDefaultPages()), SORT_REGULAR);
|
||||
return array_unique(array_merge(static::getDefaultPages(), static::$customPages), SORT_REGULAR);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,6 @@ trait CanCustomizeRelations
|
||||
/** @return class-string<RelationManager>[] */
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return array_unique(array_merge(static::$customRelations, static::getDefaultRelations()));
|
||||
return array_unique(array_merge(static::getDefaultRelations(), static::$customRelations));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits\Filament;
|
||||
|
||||
use App\Enums\TabPosition;
|
||||
use Filament\Schemas\Components\Tabs\Tab;
|
||||
|
||||
trait CanCustomizeStaticTabs
|
||||
{
|
||||
/** @var array<string, Tab[]> */
|
||||
protected static array $customTabs = [];
|
||||
|
||||
public static function registerCustomTabs(TabPosition $position, Tab ...$customTabs): void
|
||||
{
|
||||
static::$customTabs[$position->value] = array_merge(static::$customTabs[$position->value] ?? [], $customTabs);
|
||||
}
|
||||
|
||||
/** @return Tab[] */
|
||||
protected static function getDefaultTabs(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @return Tab[] */
|
||||
protected static function getTabs(): array
|
||||
{
|
||||
return array_merge(
|
||||
static::$customTabs[TabPosition::Before->value] ?? [],
|
||||
static::getDefaultTabs(),
|
||||
static::$customTabs[TabPosition::After->value] ?? []
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits\Filament;
|
||||
|
||||
use App\Enums\StepPosition;
|
||||
use Filament\Schemas\Components\Wizard\Step;
|
||||
|
||||
trait CanCustomizeSteps
|
||||
{
|
||||
/** @var Step[] */
|
||||
protected static array $customSteps = [];
|
||||
|
||||
public static function registerCustomSteps(StepPosition $position, Step ...$customSteps): void
|
||||
{
|
||||
static::$customSteps[$position->value] = array_merge(static::$customSteps[$position->value] ?? [], $customSteps);
|
||||
}
|
||||
|
||||
/** @return Step[] */
|
||||
protected function getDefaultSteps(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @return Step[] */
|
||||
protected function getSteps(): array
|
||||
{
|
||||
return array_merge(
|
||||
static::$customSteps[StepPosition::Before->value] ?? [],
|
||||
$this->getDefaultSteps(),
|
||||
static::$customSteps[StepPosition::After->value] ?? []
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits\Filament;
|
||||
|
||||
use App\Enums\TabPosition;
|
||||
use Filament\Schemas\Components\Tabs\Tab;
|
||||
|
||||
trait CanCustomizeTabs
|
||||
{
|
||||
/** @var array<string, Tab[]> */
|
||||
protected static array $customTabs = [];
|
||||
|
||||
public static function registerCustomTabs(TabPosition $position, Tab ...$customTabs): void
|
||||
{
|
||||
static::$customTabs[$position->value] = array_merge(static::$customTabs[$position->value] ?? [], $customTabs);
|
||||
}
|
||||
|
||||
/** @return Tab[] */
|
||||
protected function getDefaultTabs(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @return Tab[] */
|
||||
protected function getTabs(): array
|
||||
{
|
||||
return array_merge(
|
||||
static::$customTabs[TabPosition::Before->value] ?? [],
|
||||
$this->getDefaultTabs(),
|
||||
static::$customTabs[TabPosition::After->value] ?? []
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -44,10 +44,6 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
||||
'node.maintenance' => \App\Http\Middleware\MaintenanceMiddleware::class,
|
||||
]);
|
||||
|
||||
$middleware->priority([
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
]);
|
||||
})
|
||||
->withSingletons([
|
||||
\Illuminate\Contracts\Console\Kernel::class => \App\Console\Kernel::class,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# [Bounties](https://github.com/pelican-dev/panel/issues?q=state%3Aopen%20is%3Aissue%20label%3A%22%F0%9F%92%B0%20fund%22)
|
||||
# [Bounties](https://github.com/pelican-dev/panel/issues?q=is%3Aopen+is%3Aissue+label%3A%22%F0%9F%92%B5+bounty%22)
|
||||
|
||||
Get paid to improve Pelican!
|
||||
|
||||
@@ -15,6 +15,6 @@ This is still valuable work, so we'll pay out $50 for getting any bounty closed
|
||||
|
||||
## Issue bounties
|
||||
|
||||
We've tagged bounty-eligible issues across openpilot and the rest of our repos; check out all the open ones [here](https://github.com/pelican-dev/panel/issues?q=state%3Aopen%20is%3Aissue%20label%3A%22%F0%9F%92%B0%20fund%22).
|
||||
We've tagged bounty-eligible issues across openpilot and the rest of our repos; check out all the open ones [here](https://github.com/pelican-dev/panel/issues?q=is%3Aopen+is%3Aissue+label%3A%22%F0%9F%92%B5+bounty%22).
|
||||
|
||||
New bounties can be proposed in the [**#feedback**](https://discord.com/channels/1218730176297439332/1218732581797892220) channel in Discord.
|
||||
|
||||
@@ -3,26 +3,26 @@
|
||||
"description": "The free, open-source game management panel. Supporting Minecraft, Spigot, BungeeCord, and SRCDS servers.",
|
||||
"license": "AGPL-3.0-only",
|
||||
"require": {
|
||||
"php": "^8.2 || ^8.3 || ^8.4 || ^8.5",
|
||||
"php": "^8.2 || ^8.3 || ^8.4",
|
||||
"ext-intl": "*",
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-pdo": "*",
|
||||
"ext-zip": "*",
|
||||
"aws/aws-sdk-php": "^3.369",
|
||||
"aws/aws-sdk-php": "^3.356",
|
||||
"calebporzio/sushi": "^2.5",
|
||||
"dedoc/scramble": "^0.13",
|
||||
"filament/filament": "^4.5",
|
||||
"dedoc/scramble": "^0.12.10",
|
||||
"filament/filament": "~4.0",
|
||||
"gboquizosanchez/filament-log-viewer": "^2.1",
|
||||
"guzzlehttp/guzzle": "^7.10",
|
||||
"laravel/framework": "^12.47",
|
||||
"laravel/helpers": "^1.8",
|
||||
"laravel/framework": "^12.37",
|
||||
"laravel/helpers": "^1.7",
|
||||
"laravel/sanctum": "^4.2",
|
||||
"laravel/socialite": "^5.24",
|
||||
"laravel/socialite": "^5.23",
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"laravel/ui": "^4.6",
|
||||
"lcobucci/jwt": "^5.6",
|
||||
"league/flysystem-aws-s3-v3": "^3.30",
|
||||
"lcobucci/jwt": "^5.5",
|
||||
"league/flysystem-aws-s3-v3": "^3.29",
|
||||
"league/flysystem-memory": "^3.29",
|
||||
"phiki/phiki": "^2.0",
|
||||
"phpseclib/phpseclib": "~3.0.18",
|
||||
@@ -32,11 +32,11 @@
|
||||
"socialiteproviders/authentik": "^5.2",
|
||||
"socialiteproviders/discord": "^4.2",
|
||||
"socialiteproviders/steam": "^4.3",
|
||||
"spatie/laravel-data": "^4.18",
|
||||
"spatie/laravel-data": "^4.17",
|
||||
"spatie/laravel-fractal": "^6.3",
|
||||
"spatie/laravel-health": "^1.34",
|
||||
"spatie/laravel-permission": "^6.24",
|
||||
"spatie/laravel-query-builder": "^6.4",
|
||||
"spatie/laravel-permission": "^6.21",
|
||||
"spatie/laravel-query-builder": "^6.3",
|
||||
"spatie/temporary-directory": "^2.3",
|
||||
"symfony/http-client": "^7.2",
|
||||
"symfony/mailgun-mailer": "^7.2",
|
||||
@@ -98,4 +98,4 @@
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
}
|
||||
755
composer.lock
generated
755
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ return [
|
||||
'logo' => env('APP_LOGO'),
|
||||
'favicon' => env('APP_FAVICON', '/pelican.ico'),
|
||||
|
||||
'version' => 'canary',
|
||||
'version' => '1.0.0-beta30',
|
||||
|
||||
'timezone' => 'UTC',
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Api\Remote\Backups\BackupRemoteUploadController;
|
||||
use App\Models\Backup;
|
||||
|
||||
return [
|
||||
@@ -11,16 +10,16 @@ return [
|
||||
|
||||
// This value is used to determine the lifespan of UploadPart presigned urls that daemon
|
||||
// uses to upload backups to S3 storage. Value is in minutes, so this would default to an hour.
|
||||
'presigned_url_lifespan' => (int) env('BACKUP_PRESIGNED_URL_LIFESPAN', 60),
|
||||
'presigned_url_lifespan' => env('BACKUP_PRESIGNED_URL_LIFESPAN', 60),
|
||||
|
||||
// This value defines the maximal size of a single part for the S3 multipart upload during backups
|
||||
// The maximal part size must be given in bytes. The default value is 5GB.
|
||||
// Note that 5GB is the maximum for a single part when using AWS S3.
|
||||
'max_part_size' => (int) env('BACKUP_MAX_PART_SIZE', BackupRemoteUploadController::DEFAULT_MAX_PART_SIZE),
|
||||
'max_part_size' => env('BACKUP_MAX_PART_SIZE', 5 * 1024 * 1024 * 1024),
|
||||
|
||||
// The time to wait before automatically failing a backup, time is in minutes and defaults
|
||||
// to 6 hours. To disable this feature, set the value to `0`.
|
||||
'prune_age' => (int) env('BACKUP_PRUNE_AGE', 360),
|
||||
'prune_age' => env('BACKUP_PRUNE_AGE', 360),
|
||||
|
||||
// Defines the backup creation throttle limits for users. In this default example, we allow
|
||||
// a user to create two (successful or pending) backups per 10 minutes. Even if they delete
|
||||
@@ -28,8 +27,8 @@ return [
|
||||
//
|
||||
// Set the period to "0" to disable this throttle. The period is defined in seconds.
|
||||
'throttles' => [
|
||||
'limit' => (int) env('BACKUP_THROTTLE_LIMIT', 2),
|
||||
'period' => (int) env('BACKUP_THROTTLE_PERIOD', 600),
|
||||
'limit' => env('BACKUP_THROTTLE_LIMIT', 2),
|
||||
'period' => env('BACKUP_THROTTLE_PERIOD', 600),
|
||||
],
|
||||
|
||||
'disks' => [
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
$database = env('DB_DATABASE', 'database.sqlite');
|
||||
$databasePath = database_path($database);
|
||||
$datapasePath = database_path($database);
|
||||
|
||||
if (str_starts_with($database, '/') || $database === ':memory:') {
|
||||
$databasePath = $database;
|
||||
@@ -41,7 +41,7 @@ return [
|
||||
'sqlite' => [
|
||||
'driver' => 'sqlite',
|
||||
'url' => env('DB_URL'),
|
||||
'database' => $databasePath,
|
||||
'database' => $datapasePath,
|
||||
'prefix' => '',
|
||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||
'busy_timeout' => null,
|
||||
@@ -65,7 +65,7 @@ return [
|
||||
'strict' => env('DB_STRICT_MODE', false),
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
@@ -85,7 +85,7 @@ return [
|
||||
'strict' => env('DB_STRICT_MODE', false),
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ return [
|
||||
*/
|
||||
'rate_limit' => [
|
||||
'client_period' => 1,
|
||||
'client' => env('APP_API_CLIENT_RATELIMIT', 120),
|
||||
'client' => env('APP_API_CLIENT_RATELIMIT', 720),
|
||||
|
||||
'application_period' => 1,
|
||||
'application' => env('APP_API_APPLICATION_RATELIMIT', 240),
|
||||
|
||||
@@ -8,7 +8,7 @@ To start contributing to Pelican Panel, you need to have a basic understanding o
|
||||
|
||||
* [PHP](https://php.net) & [Laravel](https://laravel.com)
|
||||
* [Livewire](https://laravel-livewire.com) & [Filament](https://filamentphp.com)
|
||||
* [Git](https://git-scm.com) & [GitHub](https://github.com)
|
||||
* [Git](https://git-scm.com) & [Github](https://github.com)
|
||||
|
||||
## Dev Environment Setup
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ class DatabaseSeeder extends Seeder
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->call(EggSeeder::class);
|
||||
|
||||
Role::firstOrCreate(['name' => Role::ROOT_ADMIN]);
|
||||
|
||||
$plugins = Plugin::query()->orderBy('load_order')->get();
|
||||
|
||||
101
database/Seeders/EggSeeder.php
Normal file
101
database/Seeders/EggSeeder.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Egg;
|
||||
use App\Services\Eggs\Sharing\EggImporterService;
|
||||
use DirectoryIterator;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Throwable;
|
||||
|
||||
class EggSeeder extends Seeder
|
||||
{
|
||||
protected EggImporterService $importerService;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
public static array $imports = [
|
||||
'Minecraft',
|
||||
'Source Engine',
|
||||
'Voice Servers',
|
||||
'Rust',
|
||||
];
|
||||
|
||||
/**
|
||||
* EggSeeder constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
EggImporterService $importerService
|
||||
) {
|
||||
$this->importerService = $importerService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the egg seeder.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
foreach (static::$imports as $import) {
|
||||
$this->parseEggFiles($import);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loop through the list of egg files and import them.
|
||||
*/
|
||||
protected function parseEggFiles($name): void
|
||||
{
|
||||
$path = database_path('Seeders/eggs/' . kebab_case($name));
|
||||
$files = new DirectoryIterator($path);
|
||||
|
||||
$this->command->alert('Updating Eggs for: ' . $name);
|
||||
|
||||
/** @var DirectoryIterator $file */
|
||||
foreach ($files as $file) {
|
||||
if (!$file->isFile() || !$file->isReadable()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$extension = strtolower($file->getExtension());
|
||||
$filePath = $file->getRealPath();
|
||||
|
||||
try {
|
||||
$decoded = match ($extension) {
|
||||
'json' => json_decode(file_get_contents($filePath), true, 512, JSON_THROW_ON_ERROR),
|
||||
'yaml', 'yml' => Yaml::parseFile($filePath),
|
||||
default => null,
|
||||
};
|
||||
} catch (Throwable) {
|
||||
$this->command->warn("Failed to parse {$file->getFilename()}, skipping.");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!is_array($decoded) || !isset($decoded['name'], $decoded['author'])) {
|
||||
$this->command->warn("Invalid structure in {$file->getFilename()}, skipping.");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$uploaded = new UploadedFile($filePath, $file->getFilename());
|
||||
|
||||
$egg = Egg::query()
|
||||
->where('author', $decoded['author'])
|
||||
->where('name', $decoded['name'])
|
||||
->first();
|
||||
|
||||
if ($egg instanceof Egg) {
|
||||
$this->importerService->fromFile($uploaded, $egg);
|
||||
$this->command->info('Updated ' . $decoded['name']);
|
||||
} else {
|
||||
$this->importerService->fromFile($uploaded);
|
||||
$this->command->comment('Created ' . $decoded['name']);
|
||||
}
|
||||
}
|
||||
|
||||
$this->command->line('');
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ _comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL'
|
||||
meta:
|
||||
version: PLCN_v3
|
||||
update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/minecraft/egg-bungeecord.yaml'
|
||||
exported_at: '2025-12-28T12:14:41+00:00'
|
||||
exported_at: '2025-10-31T12:25:34+00:00'
|
||||
name: Bungeecord
|
||||
author: panel@example.com
|
||||
uuid: 9e6b409e-4028-4947-aea8-50a2c404c271
|
||||
@@ -26,7 +26,7 @@ docker_images:
|
||||
'Java 16': 'ghcr.io/pelican-eggs/yolks:java_16'
|
||||
'Java 11': 'ghcr.io/pelican-eggs/yolks:java_11'
|
||||
'Java 8': 'ghcr.io/pelican-eggs/yolks:java_8'
|
||||
file_denylist: { }
|
||||
file_denylist: { }
|
||||
startup_commands:
|
||||
Default: 'java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}'
|
||||
config:
|
||||
@@ -40,7 +40,7 @@ config:
|
||||
'regex:^(127\.0\.0\.1|localhost)(:\d{1,5})?$': '{{config.docker.interface}}$2'
|
||||
startup:
|
||||
done: 'Listening on '
|
||||
logs: { }
|
||||
logs: { }
|
||||
stop: end
|
||||
scripts:
|
||||
installation:
|
||||
@@ -56,11 +56,12 @@ scripts:
|
||||
BUNGEE_VERSION="lastStableBuild"
|
||||
fi
|
||||
|
||||
curl -sSL -o ${SERVER_JARFILE} https://ci.md-5.net/job/BungeeCord/${BUNGEE_VERSION}/artifact/bootstrap/target/BungeeCord.jar
|
||||
curl -o ${SERVER_JARFILE} https://ci.md-5.net/job/BungeeCord/${BUNGEE_VERSION}/artifact/bootstrap/target/BungeeCord.jar
|
||||
container: 'ghcr.io/pelican-eggs/installers:alpine'
|
||||
entrypoint: ash
|
||||
variables:
|
||||
- name: 'Bungeecord Jar File'
|
||||
-
|
||||
name: 'Bungeecord Jar File'
|
||||
description: 'The name of the Jarfile to use when running Bungeecord.'
|
||||
env_variable: SERVER_JARFILE
|
||||
default_value: bungeecord.jar
|
||||
@@ -70,7 +71,8 @@ variables:
|
||||
- required
|
||||
- 'regex:/^([\w\d._-]+)(\.jar)$/'
|
||||
sort: 2
|
||||
- name: 'Bungeecord Version'
|
||||
-
|
||||
name: 'Bungeecord Version'
|
||||
description: 'The version of Bungeecord to download and use.'
|
||||
env_variable: BUNGEE_VERSION
|
||||
default_value: latest
|
||||
219
database/Seeders/eggs/minecraft/egg-forge-minecraft.yaml
Normal file
219
database/Seeders/eggs/minecraft/egg-forge-minecraft.yaml
Normal file
File diff suppressed because one or more lines are too long
142
database/Seeders/eggs/minecraft/egg-paper.yaml
Normal file
142
database/Seeders/eggs/minecraft/egg-paper.yaml
Normal file
File diff suppressed because one or more lines are too long
159
database/Seeders/eggs/minecraft/egg-sponge.yaml
Normal file
159
database/Seeders/eggs/minecraft/egg-sponge.yaml
Normal file
@@ -0,0 +1,159 @@
|
||||
_comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL'
|
||||
meta:
|
||||
version: PLCN_v3
|
||||
update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/minecraft/egg-sponge.yaml'
|
||||
exported_at: '2025-10-31T12:41:03+00:00'
|
||||
name: Sponge
|
||||
author: panel@example.com
|
||||
uuid: f0d2f88f-1ff3-42a0-b03f-ac44c5571e6d
|
||||
description: 'A community-driven open source Minecraft: Java Edition modding platform.'
|
||||
image: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBmaWxsPSIjRjdDRjBEIj48cGF0aCBkPSJNMTkwIDBIMTBDNC41IDAgMCA0LjUgMCAxMHYxODBjMCA1LjUgNC41IDEwIDEwIDEwaDE2LjFjLTEuNy00NS43LS4xLTUyLjUgMy4xLTU3IDMuOS01LjYgNS41LTYuMyAxMS40LTExIDUtNCAzLjItMTAuNS0uNC0xNS4yLTIuMi0yLjktNS4zLTYuMy03LjctOS42LTEuNS0yLjIgMi4yLTE1LjEgMy42LTE5LjggMS40LTQuNyAzLjgtMjAgMjQuOC0yNC4xIDcuOS0xLjYgMjkuNi0yLjcgNDQuNS0xLjgtLjEtLjYtLjMtMS4zLS40LTItLjMtMS4yLS41LTIuNS0uOC0zLjktLjMtMS4zLS42LTIuNy0uOS00LjEtLjMtMS40LS43LTIuOC0xLTQuMy0uNC0xLjUtLjctMi45LTEuMi00LjQtLjgtMy0xLjgtNS45LTMtOC43LS42LTEuNC0xLjItMi43LTEuOS0zLjktLjctMS4xLTEuNC0yLjEtMi0yLjUtLjEtLjEtLjItLjItLjMtLjJoLS4xLjJzLjEgMCAwIDBsLS4zLS4xaC0uMmwtLjQtLjFoLS41Yy0xLjMtLjEtMi43LS4xLTQuMiAwLTIuOS4yLTYgLjgtOSAxLjVzLTUuOSAxLjYtOC43IDIuNGMtMS4yLjQtMi4zLjgtMy40IDEuMS4xLjkuMiAxLjcuMiAyLjYgMCAxMy0xMC41IDIzLjUtMjMuNSAyMy41UzIwLjYgNDcuOSAyMC42IDM0LjlzMTAuNS0yMy41IDIzLjUtMjMuNWM4LjcgMCAxNi4zIDQuNyAyMC40IDExLjggMS0uNCAyLjEtLjggMy4yLTEuMiAyLjgtMS4xIDUuOS0yLjIgOS4xLTMuMiAzLjMtMSA2LjctMiAxMC41LTIuNSAxLjktLjMgMy45LS40IDYuMS0uM2guOGMuMyAwIC42LjEuOC4xSDk1LjdsLjMuMWguMWwuMy4xcy4yIDAgLjMuMWwuNC4xYy42LjIuOS4zIDEuMy41cy43LjMgMS4xLjVjLjcuNCAxLjMuOCAxLjkgMS4yIDEuMS45IDIgMS44IDIuNyAyLjcuOC45IDEuNCAxLjggMiAyLjcgMS4yIDEuOCAyLjEgMy41IDIuOSA1LjIgMS42IDMuNCAyLjkgNi44IDMuOSAxMGwxLjUgNC44Yy41IDEuNi44IDMuMSAxLjIgNC42LjIuNy40IDEuNS41IDIuMi4yLjcuMyAxLjQuNSAyLjEuMyAxLjQuNiAyLjguOSA0LjEuNCAyIC43IDMuOSAxIDUuNiAyMi40IDIuMiAzOS41IDUuMSA0Ny4yIDEyLjggMTEuMyAxMSAyMCA2MSAxNC4zIDEyNC41aDEwYzUuNSAwIDEwLTQuNSAxMC0xMFYxMGMwLTUuNS00LjUtMTAtMTAtMTB6Ii8+PHBhdGggZD0iTTkxLjQgMTQwLjhjLTEuMyAzLjYtMi40IDQ1LjcgMTAgNDUuN3MxMi41LTQzLjIgMTIuMS00NS43Yy0uNC0yLjQtMjAuOC0zLjUtMjIuMSAwek03NSAxMDBjLTguNS0xLjItMTMuNiA0MC4yLTEuNyA0Mi42IDExLjIgMi4yIDEwLjEtNDEuNCAxLjctNDIuNnpNMTMwLjggMTAwYy04LjUtMS4yLTEzLjYgNDAuMi0xLjcgNDIuNiAxMS4yIDIuMiAxMC4yLTQxLjQgMS43LTQyLjZ6Ii8+PC9zdmc+'
|
||||
tags:
|
||||
- minecraft
|
||||
features:
|
||||
- eula
|
||||
- java_version
|
||||
- pid_limit
|
||||
docker_images:
|
||||
'Java 21': 'ghcr.io/pelican-eggs/yolks:java_21'
|
||||
'Java 17': 'ghcr.io/pelican-eggs/yolks:java_17'
|
||||
'Java 16': 'ghcr.io/pelican-eggs/yolks:java_16'
|
||||
'Java 11': 'ghcr.io/pelican-eggs/yolks:java_11'
|
||||
'Java 8': 'ghcr.io/pelican-eggs/yolks:java_8'
|
||||
file_denylist: { }
|
||||
startup_commands:
|
||||
Default: 'java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}'
|
||||
config:
|
||||
files:
|
||||
server.properties:
|
||||
parser: properties
|
||||
find:
|
||||
server-ip: ''
|
||||
server-port: '{{server.allocations.default.port}}'
|
||||
query.port: '{{server.allocations.default.port}}'
|
||||
startup:
|
||||
done: ')! For help, type '
|
||||
logs: { }
|
||||
stop: stop
|
||||
scripts:
|
||||
installation:
|
||||
script: |-
|
||||
#!/bin/ash
|
||||
# Sponge Installation Script
|
||||
#
|
||||
# Server Files: /mnt/server
|
||||
|
||||
cd /mnt/server
|
||||
|
||||
if [ $MINECRAFT_VERSION = 'latest' ] || [ -z $MINECRAFT_VERSION ]; then
|
||||
TARGET_VERSION_JSON=$(curl -sSL https://dl-api.spongepowered.org/v2/groups/org.spongepowered/artifacts/${SPONGE_TYPE}/latest?recommended=true)
|
||||
if [ -z "${TARGET_VERSION_JSON}" ]; then
|
||||
echo -e "Failed to find latest recommended version!"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "Found latest version for ${SPONGE_TYPE}"
|
||||
else
|
||||
if [ $SPONGE_TYPE = 'spongevanilla' ]; then
|
||||
VERSIONS_JSON=$(curl -sSL https://dl-api.spongepowered.org/v2/groups/org.spongepowered/artifacts/${SPONGE_TYPE}/versions?tags=,minecraft:${MINECRAFT_VERSION}&offset=0&limit=1)
|
||||
else
|
||||
FORGETAG='forge'
|
||||
if [ $SPONGE_TYPE = 'spongeneo' ]; then
|
||||
FORGETAG='neoforge'
|
||||
fi
|
||||
VERSIONS_JSON=$(curl -sSL https://dl-api.spongepowered.org/v2/groups/org.spongepowered/artifacts/${SPONGE_TYPE}/versions?tags=,minecraft:${MINECRAFT_VERSION},${FORGETAG}:${FORGE_VERSION}&offset=0&limit=1)
|
||||
fi
|
||||
|
||||
if [ -z "${VERSIONS_JSON}" ]; then
|
||||
echo -e "Failed to find recommended ${MINECRAFT_VERSION} version for ${SPONGE_TYPE} ${FORGE_VERSION}!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION_KEY=$(echo $VERSIONS_JSON | jq -r '.artifacts | to_entries[0].key')
|
||||
TARGET_VERSION_JSON=$(curl -sSL https://dl-api.spongepowered.org/v2/groups/org.spongepowered/artifacts/${SPONGE_TYPE}/versions/${VERSION_KEY})
|
||||
|
||||
if [ -z "${TARGET_VERSION_JSON}" ]; then
|
||||
echo -e "Failed to find ${VERSION_KEY} for ${SPONGE_TYPE} ${FORGE_VERSION}!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "Found ${MINECRAFT_VERSION} for ${SPONGE_TYPE}"
|
||||
fi
|
||||
|
||||
TARGET_VERSION=`echo $TARGET_VERSION_JSON | jq '.assets[] | select(.classifier == "universal")'`
|
||||
if [ -z "${TARGET_VERSION}" ]; then
|
||||
TARGET_VERSION=`echo $TARGET_VERSION_JSON | jq '.assets[] | select(.classifier == "" and .extension == "jar")'`
|
||||
fi
|
||||
|
||||
if [ -z "${TARGET_VERSION}" ]; then
|
||||
echo -e "Failed to get download url data from the selected version"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SPONGE_URL=$(echo $TARGET_VERSION | jq -r '.downloadUrl')
|
||||
CHECKSUM=$(echo $TARGET_VERSION | jq -r '.md5')
|
||||
echo -e "Found file at ${SPONGE_URL} with checksum ${CHECKSUM}"
|
||||
|
||||
echo -e "running: curl -o ${SERVER_JARFILE} ${SPONGE_URL}"
|
||||
curl -o ${SERVER_JARFILE} ${SPONGE_URL}
|
||||
|
||||
if [ $(basename $(md5sum ${SERVER_JARFILE})) = ${CHECKSUM} ] ; then
|
||||
echo "Checksum passed"
|
||||
else
|
||||
echo "Checksum failed"
|
||||
fi
|
||||
|
||||
echo -e "Install Complete"
|
||||
container: 'ghcr.io/pelican-eggs/installers:alpine'
|
||||
entrypoint: ash
|
||||
variables:
|
||||
-
|
||||
name: 'Forge/Neoforge Version'
|
||||
description: |-
|
||||
The modding api target version if set to `spongeforge` or `spongeneo`. Leave blank if using
|
||||
`spongevanilla`
|
||||
env_variable: FORGE_VERSION
|
||||
default_value: ''
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- string
|
||||
sort: 3
|
||||
-
|
||||
name: 'Minecraft Version'
|
||||
description: |-
|
||||
The version of Minecraft to target. Use "latest" to install the latest version. Go to Settings >
|
||||
Reinstall Server to apply.
|
||||
env_variable: MINECRAFT_VERSION
|
||||
default_value: latest
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- required
|
||||
- string
|
||||
- 'between:3,15'
|
||||
sort: 1
|
||||
-
|
||||
name: 'Server Jar File'
|
||||
description: 'The name of the Jarfile to use when running Sponge.'
|
||||
env_variable: SERVER_JARFILE
|
||||
default_value: server.jar
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- required
|
||||
- 'regex:/^([\w\d._-]+)(\.jar)$/'
|
||||
sort: 4
|
||||
-
|
||||
name: 'Sponge Type'
|
||||
description: |-
|
||||
SpongeVanilla if you are only using Sponge plugins.
|
||||
SpongeForge when using Forge mods and Sponge plugins.
|
||||
SpongeNeo when using NeoForge mods and Sponge plugins.
|
||||
env_variable: SPONGE_TYPE
|
||||
default_value: spongevanilla
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- required
|
||||
- 'in:spongevanilla,spongeforge,spongeneo'
|
||||
sort: 2
|
||||
@@ -24,7 +24,7 @@ docker_images:
|
||||
'Java 16': 'ghcr.io/pelican-eggs/yolks:java_16'
|
||||
'Java 11': 'ghcr.io/pelican-eggs/yolks:java_11'
|
||||
'Java 8': 'ghcr.io/pelican-eggs/yolks:java_8'
|
||||
file_denylist: { }
|
||||
file_denylist: { }
|
||||
startup_commands:
|
||||
Default: 'java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}'
|
||||
config:
|
||||
@@ -37,7 +37,7 @@ config:
|
||||
query.port: '{{server.allocations.default.port}}'
|
||||
startup:
|
||||
done: ')! For help, type '
|
||||
logs: { }
|
||||
logs: { }
|
||||
stop: stop
|
||||
scripts:
|
||||
installation:
|
||||
@@ -72,7 +72,8 @@ scripts:
|
||||
container: 'ghcr.io/pelican-eggs/installers:alpine'
|
||||
entrypoint: ash
|
||||
variables:
|
||||
- name: 'Server Jar File'
|
||||
-
|
||||
name: 'Server Jar File'
|
||||
description: 'The name of the server jarfile to run the server with.'
|
||||
env_variable: SERVER_JARFILE
|
||||
default_value: server.jar
|
||||
@@ -82,7 +83,8 @@ variables:
|
||||
- required
|
||||
- 'regex:/^([\w\d._-]+)(\.jar)$/'
|
||||
sort: 1
|
||||
- name: 'Server Version'
|
||||
-
|
||||
name: 'Server Version'
|
||||
description: |-
|
||||
The version of Minecraft Vanilla to install. Use "latest" to install the latest version, or use
|
||||
"snapshot" to install the latest snapshot. Go to Settings > Reinstall Server to apply.
|
||||
282
database/Seeders/eggs/rust/egg-rust.yaml
Normal file
282
database/Seeders/eggs/rust/egg-rust.yaml
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,155 @@
|
||||
_comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL'
|
||||
meta:
|
||||
version: PLCN_v3
|
||||
update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/source-engine/egg-custom-source-engine-game.yaml'
|
||||
exported_at: '2025-10-31T12:43:00+00:00'
|
||||
name: 'Custom Source Engine Game'
|
||||
author: panel@example.com
|
||||
uuid: 2a42d0c2-c0ba-4067-9a0a-9b95d77a3490
|
||||
description: |-
|
||||
This option allows modifying the startup arguments and other details to run a custom SRCDS based
|
||||
game on the panel.
|
||||
image: 'data:image/webp;base64,UklGRoAMAABXRUJQVlA4THMMAAAv+QATECUwbtvIkdx/2ZNnLnwjYgLYqi8KnDuYekQOBtxU1VNGcZdt8CWWtnXLbdOCTsNlE6Ix5eSRaVdUVr1HA0Dlx+NLkmTVtm3binROhla09D55Lsi/tN5reIlWJ3xHSKBt7dii9/uN8Cv8yLaxjGXbrrFt20YY2whj2zZy/bbVK8FtI0mS6JnZe9d1ZWVW7QskSZIcN1L9/zwbHDaPlYyAqtAy1NVBG0mOnCyW92E8+9FhG0mKVBkf/fjgKXVZkCSbttXXtm3btm3bfrZt27b5dW3btm3uuyBIkuSoyWLwSHjPBwr8a2Y+MUMVHtiDh/iHYtShBa1oQCUqUYv6V2h4HQO0ogXt6EbXK/Rj4BUGMfYOk5h5hwUsYRGLWMb6O2yBgGATs2jDT6RbYlG4eWMvZksTu/EbXVgF+bEwjZsQvbV53+lhjBtoBwXyw2EUAVfG8CEddaBAviBs48h9QRSXMAbyLeHAZc3HiwfHMAHyPWHLcrsqGKIT5KtCh8V1UWZ4XvNl4dBFIfbjOYYVdOE/bmEPwuEBB9jBGQFIxll8QC1mzkPXhP2h2DWzJtas8qjUohQoemVdDC7uoR8vEQrFf+dh2h+rP78/jSUCe5xHJTYOL+D8UDZVX+9qfHHqdc0oUMvrzcVgzwbm8I5xAlvumP153XRQx5mzt0Rcex7bwtelohao2/WqmPWhbl8Mo4D5JwzjBCNROXJjsSIQpQch86kIdaVohdSJ2lLPanOdv9kOiMfmGyhcsXirx25eQw8fNByDeoPhqa491cUKrpMVXp1RuFljT2M8RQ7ejD3tThy9cU4W7MPcIWj3xPpQuFpdlgVqVTEKFLvWR6GF5owics9A4zgnUxQ6aj4PRpwHdQJyotBUCMHCAbjXlSjACpN1hk9bogBDjFVhZM7w9iUKMKv+R9yPQmfBq/YsGlswaE0UsK8EuVFoLnyuQHh3IIK+PPRxcRe9ozYeiQUyAvSLNgYBGQn+R+Rv4gc0EtIb+mTjEJGRFvZFG7XxSEhwvyIv1EJgHi4XeAFH5bbr9E4vPjG+Pu912qoYa4itQlv1+GyQEE+vl85bIGYRs03i2IIDlbFrbTihPKFzaiUW4YKlUsd8wMX3XmD3f4S7Jh0e+G2E0Kgf7juobrkMvWGFmeuc1/rwcf310gkt3H5GCgyDn1lYg9rYG0aRW5RgDpmrUr9HjhYixhmi2MyYP1Yvh0QRLdkl5AZYZ27RXOvQW/ukle7agULC50llit5rfelXD5xi92jBFCp3lW7cGYrpPjKlZjRuSRuCFpaT8D89Aj0CBzGmk8sBT5eYz9VIrdjkTkU4VCiZZ72ObU9O9A475Ylz2FCpF2K8As/xTvLB94hqfecd4/J2KjAKniTBN2vlkYKzGBUbqSWZVfOGX6M8HIUDTztbnxzvdnIBAfrB7BeUhfTgJIzWbUyyKU6NmhmROgJVrObe8J3zYk1aMRTgNAQCA4vH1fDF/RV2wmb4C2ZfIPaB0zj2wHSYXg2LUSssMAm+pOBUct7w3oHzeAHXmPNCNUJhfwFJzoAHtoPz2N5sqtfjYhRiJ+CUgWVLMUk12JE7Y4ZShbvGcnfpQhPNV7KYImYaOjDGj824HaPNDAcawIiBlinXUHQB5XK++GjyJen3YiSMczKhJQE/opCks/OVZ2siytfOy4vF8B/smNzWLJIXPylW+A2GaL0dMzkYScbEC+idUyKIl3A1rtIHviF+Mi3da5xSLpQnP2k2Gg5N7C0wCC4nwC3pQTHUKQhdeJM0dHw+WZ8GW7mtGbJ1zOH0EH3bD463BAKfPPTYJ3wx6Tf5L761XQkGQhfVH7DxwlW//FqzZeNK+HcFDcDyMzSNgzlp+dH2I70CUhlteYu3PUEYTFZwqmYu2OPSX4LRRambdpl5CaLqDPMKdYVTdHo+2NJLePZmFZohuN/LODIADvSf+vxWFNijJlFmVN8jXDM/mAF2GnPhcNHT8PoBeDJ7M2vBTlnsa324rbEpgGGkD398YzOhtA3J5hQ4B78+wRyk6gwKDR5oli2L8CHMdrxZqExoGAbMvh+5yDAR9r3zoEDEO0wkxRRQ0ys4WCDH1V5H0wAOfYI3UcjilgY6Cq07fLRM4UTRmXoq5H7nzv1MLtAJYQrXc/ZAeUyEdwm53AIudFIO1HYuD8D5E9ikfYt4my97P7Sq57vIJ3J6KVo+n1I7G0S5my/v+MD43oWY/WGbWGKgX4Kx8Ij+92YoB8uJtvFvk8/mjBrW9lDx/7bSRSGdKeApTHp1WSIZNbGw829jTySF2wOfu1mYimnvPC8wDm+G/k846ZjjKY45Td3PEh5n4MPEHtKikPeO83C0TCJwRR36eae4W+BkO4RBQNrNjAmNi8jOoTwp4OuRdvbr/6dD9xbGx7mEopCo/GQczwa2F651zhyhEoW70c2MpGowPnRW5Km9gcZBV1Rt4VEUSrG0iw6cxbDcw+mg/VzXYGfCzXxMvd4IfnVm5Zl0AzMGkL8DyjKKQjH+9uLOtoVR/Qe3O21HLAx0Js55NuNugux/Z1qeyTewYADZOyiIQj00+dp8mPxxahFPrndOnX9A2Kkbe0e8mdGVIPjZWTSn4QbmDiBvB7EFbskbZrkqs9ULYvAQ3U8Od24dbTZNJyshR41gfJ1xPYH2rrNzTnaYsitXoHAWJVwHULH7KMviK3Bftr0Mu/2LaHkUsDSsRH9An4gHO34J1bKPEAy+rICusDA4gUNhfQkKrMAw02Bd4Lb8fyy06NjArSjcm01U7H36UF5AQbjkeEZ5HWEEvYDSjp53hEPxOts7A5t0YDElwSUU5m8ybh/shNzH4MLYCpvQKdk0tGvykYR3KjC7ALewijP+xM3OwwIF5IWiMiPp4OuUhGYSD9xJ6ydPegUtN/7XDSzvvoxU4j+u0c8QeIkQyyw2UWd+gQJehNXpn8jhPkQT54ISAosiO2/AgbRzNEpHmXp7pqvczRMxlpj/GYtVDsB2sxnBNXr7wMAlgvYtTREKWJAuuSycj/QCnvilx8A/otPUzOjU8EkUrbDZxed1gQI7iKFP5yVkb4Q+FFg1gJ0r9Mw5OGp0zW5K7Ak7GR/nA0kYSHjNZja6PwfkUBTU0H2DOA7eQX6o5jXrQCZ9ONXzFM7XbZc80H0yT8l9AJ9WOBeFGtM6IqFBOrPJsQ8Xi14JfT1vEUPHfHkV4WNa9Ktof8YZ8BYwnoVuHnLsz2NIwr7A+SAtNPM5nI+89VReaDCs6yGxMvZULuIQhhJcVQt3tUiWMB5ucptlKD2TdUk5L0zsHW522sGgk5eu9saHwp5ZqVmzRS6SgvcoIIU5vVewSRwOJm+1bqUm1l+qOR960Lighj8WJm/lzlCG48tzAPrYbvArCtVsTvvFN9tUSZOqyq44kZDTR/64Q6locw29i3UhDRzkLgVsyf3oKJnzk/RNvZ4UBvPylYbK2GHKPIAT62Z4lKH6DhZyoXWz6oVmZO3lzsJ4P8LowS02TieYjkpcPx+p5tVc/xE94P2iXV8honm8TuYolBOCP+fLsuvdbBwjlRGGyBFtim5RYFHcnKJLtzAFlFNebFqBBJiuqzgchQMJ9XfKS47xiRHVjG/CUzNbB1x7IEruUWCTtA5fFcrOM3T/ZtxELzCBZwvMW9JROBGGg7QTlDYsi9xNsDuTq8C2vCzaiC3MB8z2IBZQNnt7rXOy4Yr9PmN29bQAYquVfvAyz9IHvsAEZDC/wMcoHEqAfQZODNjK7VgNWprrVJkhu8/DuUyv4wDohfTLkMz4CLT5ydHewZXRdrMy2UfmfCxdk7yRczXBDhMeHlXZs9wKzIJr6+axp0MUjvWwlGKZSz7qxycmxtPnvfMWiWcxqvYVwlG9Vs/9xyUiNOyHm9bKYDkptrFirXXHb6NERAa9sk3845PnZbE9tvuj7fHxupNd74A53O6NFW6BK74aJiDG0+uVdo2chtUWQ7EdHvprlIgY118P7VCwaQWGgTxmFqiezxd9FI72J7LJ23axsbYN0ovdHoTNwGK/KRv9Sq93zKZjYrIs8FcFijX7xd42i/hiNTh0TCx0uFd64dW6ioQodBucQC3QaU+ubs0ZHjSvq0hvdxVP1kGLGfZuIQFkhcBuBzZYXuFvtwOtzdt+mIVKszjV0AfS9WFguBt8aXagiLHNoHmcU6BZeLAZTEE7Cs1C9ebn8XCNQrdQvMAGQqPQLpxaBhFR6Ne/c/Gg9gEZUeg4QwRvsQiC9rM/jCoAAA=='
|
||||
tags:
|
||||
- source
|
||||
- steamcmd
|
||||
features:
|
||||
- steam_disk_space
|
||||
docker_images:
|
||||
Source: 'ghcr.io/pelican-eggs/games:source'
|
||||
file_denylist: { }
|
||||
startup_commands:
|
||||
Default: './srcds_run -game {{SRCDS_GAME}} -console -port {{SERVER_PORT}} +map {{SRCDS_MAP}} +ip 0.0.0.0 -strictportbind -norestart'
|
||||
config:
|
||||
files: { }
|
||||
startup:
|
||||
done: 'gameserver Steam ID'
|
||||
logs: { }
|
||||
stop: quit
|
||||
scripts:
|
||||
installation:
|
||||
script: |-
|
||||
#!/bin/bash
|
||||
# steamcmd Base Installation Script
|
||||
#
|
||||
# Server Files: /mnt/server
|
||||
|
||||
##
|
||||
#
|
||||
# Variables
|
||||
# STEAM_USER, STEAM_PASS, STEAM_AUTH - Steam user setup. If a user has 2fa enabled it will most likely fail due to timeout. Leave blank for anon install.
|
||||
# WINDOWS_INSTALL - if it's a windows server you want to install set to 1
|
||||
# SRCDS_APPID - steam app id ffound here - https://developer.valvesoftware.com/wiki/Dedicated_Servers_List
|
||||
# EXTRA_FLAGS - when a server has extra glas for things like beta installs or updates.
|
||||
#
|
||||
##
|
||||
|
||||
|
||||
## just in case someone removed the defaults.
|
||||
if [ "${STEAM_USER}" == "" ]; then
|
||||
echo -e "steam user is not set.
|
||||
"
|
||||
echo -e "Using anonymous user.
|
||||
"
|
||||
STEAM_USER=anonymous
|
||||
STEAM_PASS=""
|
||||
STEAM_AUTH=""
|
||||
else
|
||||
echo -e "user set to ${STEAM_USER}"
|
||||
fi
|
||||
|
||||
## download and install steamcmd
|
||||
cd /tmp
|
||||
mkdir -p /mnt/server/steamcmd
|
||||
curl -sSL -o steamcmd.tar.gz https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz
|
||||
tar -xzvf steamcmd.tar.gz -C /mnt/server/steamcmd
|
||||
mkdir -p /mnt/server/steamapps # Fix steamcmd disk write error when this folder is missing
|
||||
cd /mnt/server/steamcmd
|
||||
|
||||
# SteamCMD fails otherwise for some reason, even running as root.
|
||||
# This is changed at the end of the install process anyways.
|
||||
chown -R root:root /mnt
|
||||
export HOME=/mnt/server
|
||||
|
||||
## install game using steamcmd
|
||||
./steamcmd.sh +force_install_dir /mnt/server +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} $( [[ "${WINDOWS_INSTALL}" == "1" ]] && printf %s '+@sSteamCmdForcePlatformType windows' ) +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} validate +quit ## other flags may be needed depending on install. looking at you cs 1.6
|
||||
|
||||
## set up 32 bit libraries
|
||||
mkdir -p /mnt/server/.steam/sdk32
|
||||
cp -v linux32/steamclient.so ../.steam/sdk32/steamclient.so
|
||||
|
||||
## set up 64 bit libraries
|
||||
mkdir -p /mnt/server/.steam/sdk64
|
||||
cp -v linux64/steamclient.so ../.steam/sdk64/steamclient.so
|
||||
container: 'ghcr.io/pelican-eggs/installers:debian'
|
||||
entrypoint: bash
|
||||
variables:
|
||||
-
|
||||
name: 'Game ID'
|
||||
description: 'The ID corresponding to the game to download and run using SRCDS.'
|
||||
env_variable: SRCDS_APPID
|
||||
default_value: ''
|
||||
user_viewable: true
|
||||
user_editable: false
|
||||
rules:
|
||||
- required
|
||||
- numeric
|
||||
- 'digits_between:1,6'
|
||||
sort: 1
|
||||
-
|
||||
name: 'Game Name'
|
||||
description: 'The name corresponding to the game to download and run using SRCDS.'
|
||||
env_variable: SRCDS_GAME
|
||||
default_value: ''
|
||||
user_viewable: true
|
||||
user_editable: false
|
||||
rules:
|
||||
- required
|
||||
- alpha_dash
|
||||
- 'between:1,100'
|
||||
sort: 2
|
||||
-
|
||||
name: Map
|
||||
description: 'The default map for the server.'
|
||||
env_variable: SRCDS_MAP
|
||||
default_value: ''
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- required
|
||||
- string
|
||||
- alpha_dash
|
||||
sort: 3
|
||||
-
|
||||
name: 'Steam Auth'
|
||||
description: ''
|
||||
env_variable: STEAM_AUTH
|
||||
default_value: ''
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- nullable
|
||||
- string
|
||||
sort: 6
|
||||
-
|
||||
name: 'Steam Password'
|
||||
description: ''
|
||||
env_variable: STEAM_PASS
|
||||
default_value: ''
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- nullable
|
||||
- string
|
||||
sort: 5
|
||||
-
|
||||
name: 'Steam Username'
|
||||
description: ''
|
||||
env_variable: STEAM_USER
|
||||
default_value: ''
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- nullable
|
||||
- string
|
||||
sort: 4
|
||||
229
database/Seeders/eggs/source-engine/egg-garrys-mod.yaml
Normal file
229
database/Seeders/eggs/source-engine/egg-garrys-mod.yaml
Normal file
@@ -0,0 +1,229 @@
|
||||
_comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL'
|
||||
meta:
|
||||
version: PLCN_v3
|
||||
update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/source-engine/egg-garrys-mod.yaml'
|
||||
exported_at: '2025-10-31T12:37:53+00:00'
|
||||
name: 'Garrys Mod'
|
||||
author: panel@example.com
|
||||
uuid: 60ef81d4-30a2-4d98-ab64-f59c69e2f915
|
||||
description: |-
|
||||
Garrys Mod, is a sandbox physics game created by Garry Newman, and developed by his company,
|
||||
Facepunch Studios.
|
||||
image: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyNC4zLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCAzODQgMzg0IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAzODQgMzg0OyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPg0KCS5zdDB7ZmlsbDojMDA4MUZGO30NCgkuc3Qxe2ZpbGw6I0ZGRkZGRjt9DQo8L3N0eWxlPg0KPHBhdGggY2xhc3M9InN0MCIgZD0iTTM0My45MiwzODRjLTEwMS42LDAtMjAzLjIsMC0zMDQuOCwwYy0wLjg5LTAuNDQtMS44NC0wLjMxLTIuNzktMC4zOGMtNC41MS0wLjM2LTguNjEtMi4wNy0xMi41Ni00LjEzDQoJYy01LjktMy4wNy0xMC44NC03LjMxLTE0LjkyLTEyLjU0Yy0zLjkzLTUuMDQtNi43My0xMC42NS04LjE3LTE2LjljLTAuMzktMS42OC0wLjMxLTMuNDEtMC40MS01LjEyYy0wLjAyLTAuMzQsMC4wOS0wLjc0LTAuMjctMQ0KCUMwLDI0Mi42NCwwLDE0MS4zNiwwLDQwLjA4YzAuNDMtMC42NywwLjI1LTEuNDQsMC4yNy0yLjE1YzAuMTEtNC42NCwxLjU3LTguOTIsMy42My0xMi45N0M5LjIyLDE0LjQ4LDE3LjI1LDYuODgsMjguMjUsMi40Ng0KCWMzLjM2LTEuMzUsNi44MS0yLjA5LDEwLjQtMi4yN0MzOS4xMywwLjE2LDM5LjY3LDAuNiw0MC4wOCwwQzE0MS45MiwwLDI0My43NiwwLDM0NS42LDBjMC43MSwwLjc3LDEuNjgsMC4zNiwyLjQ5LDAuNDINCgljMS45MiwwLjE1LDMuNzQsMC42NCw1LjUzLDEuMjVjNy4zMSwyLjQ2LDEzLjU4LDYuNTYsMTguODIsMTIuMmM1LjM5LDUuOCw5LjEyLDEyLjUsMTAuOSwyMC4yNWMwLjM4LDEuNjYsMC4yOSwzLjM3LDAuMzksNS4wNg0KCWMwLjAyLDAuMzItMC4wNywwLjY3LDAuMjYsMC45YzAsMTAxLjI4LDAsMjAyLjU2LDAsMzAzLjg0Yy0wLjU4LDAuOC0wLjM1LDEuNzYtMC4zNCwyLjU5YzAuMDMsMi43LTAuNjMsNS4yNC0xLjUzLDcuNzENCgljLTQuMjMsMTEuNTUtMTEuODgsMjAuMTItMjIuOCwyNS43M2MtMy4xMywxLjYxLTYuNDIsMi45MS05Ljk2LDMuNDNjLTEuNTIsMC4yMi0zLjA0LDAuMjYtNC41NiwwLjMzDQoJQzM0NC40OCwzODMuNzMsMzQ0LjE1LDM4My42OCwzNDMuOTIsMzg0eiIvPg0KPHBhdGggY2xhc3M9InN0MSIgZD0iTTIzNS41NywyMjEuOTFjLTIuODQsMy40LTUuNjgsNy41MS05LjIyLDEwLjg4Yy03LjIsNi44NC0xNi4yLDEwLjQxLTI1Ljc3LDEyLjQ5DQoJYy0xMS41MywyLjUxLTIzLjEzLDIuMTgtMzQuNzYsMC42MWMtMTQuNTItMS45NS0yNy4yNy03LjgzLTM4LjM4LTE3LjIyYy05LjM1LTcuOTEtMTYuMDYtMTcuOC0yMC44NC0yOS4wNA0KCWMtNC44OC0xMS40Ni03LjQtMjMuNTEtOC42OC0zNS44MWMtMS44NS0xNy43OC0wLjk5LTM1LjQyLDQuMzMtNTIuNmM0LjM3LTE0LjExLDExLjAxLTI3LDIxLjA4LTM4LjAzDQoJQzEzNSw2MC40LDE0OS4xOCw1Mi4wOCwxNjYuMSw0OC41OWM5LjQ2LTEuOTUsMTkuMDEtMS45NSwyOC41Ny0wLjRjMTcsMi43NywzMC4xMywxMS40OSwzOS43NSwyNS42OGMwLjI1LDAuMzIsMS4xOSwxLjcyLDEuMTksMS43Mg0KCXMwLTQuNzgsMC02LjgzYy0wLjAxLTUuMTYsMC0xNi41MSwwLTE2LjUxczEuMDksMCwxLjM1LDBjMTUuMjgtMC4wNSw0OS45Ny0wLjAxLDQ5Ljk3LTAuMDFzLTAuMDEsMS45Ni0wLjAxLDQuMjgNCgljMCw2NC40LDAuMDksMTI4LjgtMC4wNSwxOTMuMTljLTAuMDQsMjAuMzYtNS43OSwzOS0xOC44NCw1NC45NWMtMTAuMTUsMTIuNC0yMy4yNSwyMC41Ny0zOC4yNSwyNS44NQ0KCWMtMTQuNiw1LjE0LTI5LjcsNi45MS00NS4wOCw2LjI2Yy0xNi44MS0wLjcxLTMyLjk5LTQuMzUtNDcuNzUtMTIuNjRjLTIyLjIyLTEyLjQ5LTM1LjcyLTMxLjIyLTM5LjA3LTU2Ljc4DQoJYy0wLjQ1LTMuMzktMC41Ni02Ljg0LTAuNjktMTAuMjZjLTAuMDEtMC4yMS0wLjA3LTEuMzEtMC4wNy0xLjMxczEuMDQsMCwxLjI2LDBjMTUuMjQtMC4wNywzMC40OC0wLjA4LDQ1LjcyLTAuMDMNCgljMC4xOCwwLDAuOSwwLjAxLDAuOSwwLjAxcy0wLjAxLDAuOTYtMC4wMSwxLjEyYzAuMTYsOC44NiwyLjYyLDE2Ljg2LDguNzcsMjMuNDRjNC41OSw0LjkxLDEwLjI2LDguMTYsMTYuNyw5Ljg5DQoJYzE0Ljk2LDQuMDMsMjkuNTksMy4xOSw0My40OS0zLjk4YzEyLjUtNi40NSwyMC40LTE2LjU0LDIxLjMtMzAuODVjMC42Ny0xMC43OSwwLjI3LTIxLjY2LDAuMzQtMzIuNDkNCglDMjM1LjU3LDIyMi4zNSwyMzUuNTcsMjIxLjc5LDIzNS41NywyMjEuOTF6Ii8+DQo8cGF0aCBjbGFzcz0ic3QwIiBkPSJNMTUwLjc3LDE0OS44MmMtMC4zLTExLjc5LDAuOTMtMjMuMzcsNS41NS0zNC4zNWM1LjQzLTEyLjg5LDE0LjkxLTIwLjc0LDI4LjktMjIuODgNCgljOS42Ny0xLjQ4LDE5LjA4LTAuODUsMjcuOTksMy40N2M4LjksNC4zMiwxNC4zOSwxMS43LDE3Ljk5LDIwLjY0YzMuNTMsOC43NSw1LjMzLDE3Ljk0LDUuNDYsMjcuMzFjMC4xMiw4LjM4LDAuMDksMTYuODktMS4yLDI1LjE0DQoJYy0yLjU5LDE2LjU3LTE0LjA5LDMyLjA4LTMxLjczLDM2LjRjLTE2LjA5LDMuOTQtMzIuNjktMi4yMy00Mi4yOC0xNS44MWMtNi43NC05LjUzLTkuODUtMjAuMjEtMTAuNjYtMzEuNjgNCglDMTUwLjYxLDE1NS4zMywxNTAuNzcsMTUyLjU3LDE1MC43NywxNDkuODJ6Ii8+DQo8L3N2Zz4NCg=='
|
||||
tags:
|
||||
- source
|
||||
- steamcmd
|
||||
features:
|
||||
- gsl_token
|
||||
- steam_disk_space
|
||||
docker_images:
|
||||
Source: 'ghcr.io/pelican-eggs/games:source'
|
||||
file_denylist: { }
|
||||
startup_commands:
|
||||
Default: './srcds_run -game garrysmod -console -port {{SERVER_PORT}} +ip 0.0.0.0 +host_workshop_collection {{WORKSHOP_ID}} +map {{SRCDS_MAP}} +gamemode {{GAMEMODE}} -strictportbind -norestart +sv_setsteamaccount {{STEAM_ACC}} +maxplayers {{MAX_PLAYERS}} -tickrate {{TICKRATE}} $( [ "$LUA_REFRESH" == "1" ] || printf %s ''-disableluarefresh'' )'
|
||||
config:
|
||||
files: { }
|
||||
startup:
|
||||
done: 'gameserver Steam ID'
|
||||
logs: { }
|
||||
stop: quit
|
||||
scripts:
|
||||
installation:
|
||||
script: |-
|
||||
#!/bin/bash
|
||||
# steamcmd Base Installation Script
|
||||
#
|
||||
# Server Files: /mnt/server
|
||||
|
||||
## just in case someone removed the defaults.
|
||||
if [ "${STEAM_USER}" == "" ]; then
|
||||
echo -e "steam user is not set.
|
||||
"
|
||||
echo -e "Using anonymous user.
|
||||
"
|
||||
STEAM_USER=anonymous
|
||||
STEAM_PASS=""
|
||||
STEAM_AUTH=""
|
||||
else
|
||||
echo -e "user set to ${STEAM_USER}"
|
||||
fi
|
||||
|
||||
## download and install steamcmd
|
||||
cd /tmp
|
||||
mkdir -p /mnt/server/steamcmd
|
||||
curl -sSL -o steamcmd.tar.gz https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz
|
||||
tar -xzvf steamcmd.tar.gz -C /mnt/server/steamcmd
|
||||
mkdir -p /mnt/server/steamapps # Fix steamcmd disk write error when this folder is missing
|
||||
cd /mnt/server/steamcmd
|
||||
|
||||
# SteamCMD fails otherwise for some reason, even running as root.
|
||||
# This is changed at the end of the install process anyways.
|
||||
chown -R root:root /mnt
|
||||
export HOME=/mnt/server
|
||||
|
||||
## install game using steamcmd
|
||||
./steamcmd.sh +force_install_dir /mnt/server +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} $( [[ "${WINDOWS_INSTALL}" == "1" ]] && printf %s '+@sSteamCmdForcePlatformType windows' ) +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} validate +quit ## other flags may be needed depending on install. looking at you cs 1.6
|
||||
|
||||
## set up 32 bit libraries
|
||||
mkdir -p /mnt/server/.steam/sdk32
|
||||
cp -v linux32/steamclient.so ../.steam/sdk32/steamclient.so
|
||||
|
||||
## set up 64 bit libraries
|
||||
mkdir -p /mnt/server/.steam/sdk64
|
||||
cp -v linux64/steamclient.so ../.steam/sdk64/steamclient.so
|
||||
|
||||
# Creating needed default files for the game
|
||||
cd /mnt/server/garrysmod/lua/autorun/server
|
||||
echo '
|
||||
-- Docs: https://wiki.garrysmod.com/page/resource/AddWorkshop
|
||||
-- Place the ID of the workshop addon you want to be downloaded to people who join your server, not the collection ID
|
||||
-- Use https://beta.configcreator.com/create/gmod/resources.lua to easily create a list based on your collection ID
|
||||
|
||||
resource.AddWorkshop( "" )
|
||||
' > workshop.lua
|
||||
|
||||
cd /mnt/server/garrysmod/cfg
|
||||
echo '
|
||||
// Please do not set RCon in here, use the startup parameters.
|
||||
|
||||
hostname "New Gmod Server"
|
||||
sv_password ""
|
||||
sv_loadingurl ""
|
||||
sv_downloadurl ""
|
||||
|
||||
// Steam Server List Settings
|
||||
// sv_location "eu"
|
||||
sv_region "255"
|
||||
sv_lan "0"
|
||||
sv_max_queries_sec_global "30000"
|
||||
sv_max_queries_window "45"
|
||||
sv_max_queries_sec "5"
|
||||
|
||||
// Server Limits
|
||||
sbox_maxprops 100
|
||||
sbox_maxragdolls 5
|
||||
sbox_maxnpcs 10
|
||||
sbox_maxballoons 10
|
||||
sbox_maxeffects 10
|
||||
sbox_maxdynamite 10
|
||||
sbox_maxlamps 10
|
||||
sbox_maxthrusters 10
|
||||
sbox_maxwheels 10
|
||||
sbox_maxhoverballs 10
|
||||
sbox_maxvehicles 20
|
||||
sbox_maxbuttons 10
|
||||
sbox_maxsents 20
|
||||
sbox_maxemitters 5
|
||||
sbox_godmode 0
|
||||
sbox_noclip 0
|
||||
|
||||
// Network Settings - Please keep these set to default.
|
||||
|
||||
sv_minrate 75000
|
||||
sv_maxrate 0
|
||||
gmod_physiterations 2
|
||||
net_splitpacket_maxrate 45000
|
||||
decalfrequency 12
|
||||
|
||||
// Execute Ban Files - Please do not edit
|
||||
exec banned_ip.cfg
|
||||
exec banned_user.cfg
|
||||
|
||||
// Add custom lines under here
|
||||
' > server.cfg
|
||||
container: 'ghcr.io/pelican-eggs/installers:debian'
|
||||
entrypoint: bash
|
||||
variables:
|
||||
-
|
||||
name: Gamemode
|
||||
description: 'The gamemode of your server.'
|
||||
env_variable: GAMEMODE
|
||||
default_value: sandbox
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- required
|
||||
- string
|
||||
sort: 5
|
||||
-
|
||||
name: 'Lua Refresh'
|
||||
description: "0 = disable Lua refresh,\r\n1 = enable Lua refresh"
|
||||
env_variable: LUA_REFRESH
|
||||
default_value: 0
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- boolean
|
||||
sort: 8
|
||||
-
|
||||
name: Map
|
||||
description: 'The default map for the server.'
|
||||
env_variable: SRCDS_MAP
|
||||
default_value: gm_flatgrass
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- required
|
||||
- string
|
||||
- alpha_dash
|
||||
sort: 1
|
||||
-
|
||||
name: 'Max Players'
|
||||
description: 'The maximum amount of players allowed on your game server.'
|
||||
env_variable: MAX_PLAYERS
|
||||
default_value: 32
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- required
|
||||
- integer
|
||||
- 'max:128'
|
||||
sort: 6
|
||||
-
|
||||
name: 'Source AppID'
|
||||
description: 'Required for game to update on server restart. Do not modify this.'
|
||||
env_variable: SRCDS_APPID
|
||||
default_value: 4020
|
||||
user_viewable: false
|
||||
user_editable: false
|
||||
rules:
|
||||
- required
|
||||
- string
|
||||
- 'max:20'
|
||||
sort: 3
|
||||
-
|
||||
name: 'Steam Account Token'
|
||||
description: 'The Steam Account Token required for the server to be displayed publicly.'
|
||||
env_variable: STEAM_ACC
|
||||
default_value: ''
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- nullable
|
||||
- string
|
||||
- alpha_num
|
||||
- 'size:32'
|
||||
sort: 2
|
||||
-
|
||||
name: Tickrate
|
||||
description: "The tickrate defines how fast the server will update each entity's location."
|
||||
env_variable: TICKRATE
|
||||
default_value: 22
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- required
|
||||
- integer
|
||||
- 'max:100'
|
||||
sort: 7
|
||||
-
|
||||
name: 'Workshop ID'
|
||||
description: 'The ID of your workshop collection (the numbers at the end of the URL)'
|
||||
env_variable: WORKSHOP_ID
|
||||
default_value: ''
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- nullable
|
||||
- integer
|
||||
sort: 4
|
||||
84
database/Seeders/eggs/source-engine/egg-insurgency.yaml
Normal file
84
database/Seeders/eggs/source-engine/egg-insurgency.yaml
Normal file
File diff suppressed because one or more lines are too long
124
database/Seeders/eggs/source-engine/egg-team-fortress2.yaml
Normal file
124
database/Seeders/eggs/source-engine/egg-team-fortress2.yaml
Normal file
@@ -0,0 +1,124 @@
|
||||
_comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL'
|
||||
meta:
|
||||
version: PLCN_v3
|
||||
update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/source-engine/egg-team-fortress2.yaml'
|
||||
exported_at: '2025-10-31T12:31:09+00:00'
|
||||
name: 'Team Fortress 2'
|
||||
author: panel@example.com
|
||||
uuid: 7f8eb681-b2c8-4bf8-b9f4-d79ff70b6e5d
|
||||
description: |-
|
||||
Team Fortress 2 is a team-based first-person shooter multiplayer video game developed and published
|
||||
by Valve Corporation. It is the sequel to the 1996 mod Team Fortress for Quake and its 1999 remake.
|
||||
image: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNi4wLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iNTAwcHgiIGhlaWdodD0iNTAwLjAwOXB4IiB2aWV3Qm94PSItNTAgLTUwLjAwNSA1MDAgNTAwLjAwOSIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAtNTAgLTUwLjAwNSA1MDAgNTAwLjAwOSINCgkgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8bGluZWFyR3JhZGllbnQgaWQ9IlNWR0lEXzFfIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjI1NS4zOTk0IiB5MT0iLTQzMy44NDM4IiB4Mj0iMTQ0LjU5ODkiIHkyPSI1My44NDUxIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIDAgMTApIj4NCgk8c3RvcCAgb2Zmc2V0PSIwIiBzdHlsZT0ic3RvcC1jb2xvcjojNUQxRjBFIi8+DQoJPHN0b3AgIG9mZnNldD0iMSIgc3R5bGU9InN0b3AtY29sb3I6I0QwOTczNyIvPg0KPC9saW5lYXJHcmFkaWVudD4NCjxwYXRoIGZpbGw9InVybCgjU1ZHSURfMV8pIiBkPSJNMjYyLjA0NC00Mi4yMzlDMzcwLjA5Ny0xNC42NDQsNDUwLDgzLjM0NCw0NTAsMTk5Ljk5OGMwLDIuNjYxLTAuMDU3LDUuMzExLTAuMTQzLDcuOTUxDQoJbC0xNjguMTUxLTIzLjkxNmMtNC45MjItMjUuMzM4LTIxLjMzMy00Ni41NTYtNDMuNTk0LTU4LjA0NUwyNjIuMDQ0LTQyLjIzOXogTTEyNS45OSwxNjEuODgNCgljMTEuNDg4LTIyLjI2MSwzMi43MDctMzguNjcsNTguMDQzLTQzLjU5M2wyMy45Mi0xNjguMTUzYy0yLjY0My0wLjA4My01LjI5LTAuMTM5LTcuOTUzLTAuMTM5DQoJYy0xMTYuNjUyLDAtMjE0LjYzOSw3OS44OTgtMjQyLjIzNSwxODcuOTUzTDEyNS45OSwxNjEuODh6IE0xNjEuODgzLDI3NC4wMDhjLTIyLjI1OS0xMS40ODktMzguNjctMzIuNzEtNDMuNTkyLTU4LjA0NQ0KCWwtMTY4LjE1Mi0yMy45MmMtMC4wODMsMi42NDMtMC4xMzksNS4yOTMtMC4xMzksNy45NTVjMCwxMTYuNjQ4LDc5Ljg5OCwyMTQuNjM3LDE4Ny45NTIsMjQyLjIzM0wxNjEuODgzLDI3NC4wMDh6IE0yNzQuMDEsMjM4LjExMw0KCWMtMTEuNDksMjIuMjYxLTMyLjcwNywzOC42NjktNTguMDQ2LDQzLjU5M2wtMjMuOTE5LDE2OC4xNThjMi42NDMsMC4wODMsNS4yOTIsMC4xNCw3Ljk1NCwwLjE0DQoJYzExNi42NTMsMCwyMTQuNjQtNzkuOTAxLDI0Mi4yMzItMTg3Ljk1NUwyNzQuMDEsMjM4LjExM3oiLz4NCjxyYWRpYWxHcmFkaWVudCBpZD0iU1ZHSURfMl8iIGN4PSI5OC4xOTE5IiBjeT0iLTE5OC4zNTYiIHI9IjQyNS45ODcxIiBmeD0iOTIuNzU2NSIgZnk9Ii0xOTkuNDgzNCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgwLjk3NzQgMC4yMTE0IDAuMTI2NiAtMC41ODUxIC03Ny4wMzAxIC0xNjcuMzcwNykiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4NCgk8c3RvcCAgb2Zmc2V0PSIwIiBzdHlsZT0ic3RvcC1jb2xvcjojRkZGRkZGO3N0b3Atb3BhY2l0eTowIi8+DQoJPHN0b3AgIG9mZnNldD0iMC40NjU3IiBzdHlsZT0ic3RvcC1jb2xvcjojRkZGRkZGO3N0b3Atb3BhY2l0eTowLjA4MDQiLz4NCgk8c3RvcCAgb2Zmc2V0PSIwLjk4NjEiIHN0eWxlPSJzdG9wLWNvbG9yOiNGRkZGRkY7c3RvcC1vcGFjaXR5OjAuMjE0MyIvPg0KCTxzdG9wICBvZmZzZXQ9IjEiIHN0eWxlPSJzdG9wLWNvbG9yOiNGRkZGRkY7c3RvcC1vcGFjaXR5OjAiLz4NCjwvcmFkaWFsR3JhZGllbnQ+DQo8cGF0aCBmaWxsPSJ1cmwoI1NWR0lEXzJfKSIgZD0iTTI2Mi4wNDQtNDIuMjM5QzM3MC4wOTctMTQuNjQ0LDQ1MCw4My4zNDQsNDUwLDE5OS45OThjMCwyLjY2MS0wLjA1Nyw1LjMxMS0wLjE0Myw3Ljk1MQ0KCWwtMTY4LjE1MS0yMy45MTZjLTQuOTIyLTI1LjMzOC0yMS4zMzMtNDYuNTU2LTQzLjU5NC01OC4wNDVMMjYyLjA0NC00Mi4yMzl6IE0xMjUuOTksMTYxLjg4DQoJYzExLjQ4OC0yMi4yNjEsMzIuNzA3LTM4LjY3LDU4LjA0My00My41OTNsMjMuOTItMTY4LjE1M2MtMi42NDMtMC4wODMtNS4yOS0wLjEzOS03Ljk1My0wLjEzOQ0KCWMtMTE2LjY1MiwwLTIxNC42MzksNzkuODk4LTI0Mi4yMzUsMTg3Ljk1M0wxMjUuOTksMTYxLjg4eiBNMTYxLjg4MywyNzQuMDA4Yy0yMi4yNTktMTEuNDg5LTM4LjY3LTMyLjcxLTQzLjU5Mi01OC4wNDUNCglsLTE2OC4xNTItMjMuOTJjLTAuMDgzLDIuNjQzLTAuMTM5LDUuMjkzLTAuMTM5LDcuOTU1YzAsMTE2LjY0OCw3OS44OTgsMjE0LjYzNywxODcuOTUyLDI0Mi4yMzNMMTYxLjg4MywyNzQuMDA4eiBNMjc0LjAxLDIzOC4xMTMNCgljLTExLjQ5LDIyLjI2MS0zMi43MDcsMzguNjY5LTU4LjA0Niw0My41OTNsLTIzLjkxOSwxNjguMTU4YzIuNjQzLDAuMDgzLDUuMjkyLDAuMTQsNy45NTQsMC4xNA0KCWMxMTYuNjUzLDAsMjE0LjY0LTc5LjkwMSwyNDIuMjMyLTE4Ny45NTVMMjc0LjAxLDIzOC4xMTN6Ii8+DQo8L3N2Zz4NCg=='
|
||||
tags:
|
||||
- source
|
||||
- steamcmd
|
||||
features:
|
||||
- gsl_token
|
||||
- steam_disk_space
|
||||
docker_images:
|
||||
Source: 'ghcr.io/pelican-eggs/games:source'
|
||||
file_denylist: { }
|
||||
startup_commands:
|
||||
Default: './srcds_run -game tf -console -port {{SERVER_PORT}} +map {{SRCDS_MAP}} +ip 0.0.0.0 -strictportbind -norestart +sv_setsteamaccount {{STEAM_ACC}}'
|
||||
config:
|
||||
files: { }
|
||||
startup:
|
||||
done: 'gameserver Steam ID'
|
||||
logs: { }
|
||||
stop: quit
|
||||
scripts:
|
||||
installation:
|
||||
script: |-
|
||||
#!/bin/bash
|
||||
# steamcmd Base Installation Script
|
||||
#
|
||||
# Server Files: /mnt/server
|
||||
# Image to install with is 'debian:buster-slim'
|
||||
|
||||
##
|
||||
#
|
||||
# Variables
|
||||
# STEAM_USER, STEAM_PASS, STEAM_AUTH - Steam user setup. If a user has 2fa enabled it will most likely fail due to timeout. Leave blank for anon install.
|
||||
# WINDOWS_INSTALL - if it's a windows server you want to install set to 1
|
||||
# SRCDS_APPID - steam app id ffound here - https://developer.valvesoftware.com/wiki/Dedicated_Servers_List
|
||||
# EXTRA_FLAGS - when a server has extra glas for things like beta installs or updates.
|
||||
#
|
||||
##
|
||||
|
||||
## just in case someone removed the defaults.
|
||||
if [ "${STEAM_USER}" == "" ]; then
|
||||
echo -e "steam user is not set.
|
||||
"
|
||||
echo -e "Using anonymous user.
|
||||
"
|
||||
STEAM_USER=anonymous
|
||||
STEAM_PASS=""
|
||||
STEAM_AUTH=""
|
||||
else
|
||||
echo -e "user set to ${STEAM_USER}"
|
||||
fi
|
||||
|
||||
## download and install steamcmd
|
||||
cd /tmp
|
||||
mkdir -p /mnt/server/steamcmd
|
||||
curl -sSL -o steamcmd.tar.gz https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz
|
||||
tar -xzvf steamcmd.tar.gz -C /mnt/server/steamcmd
|
||||
mkdir -p /mnt/server/steamapps # Fix steamcmd disk write error when this folder is missing
|
||||
cd /mnt/server/steamcmd
|
||||
|
||||
# SteamCMD fails otherwise for some reason, even running as root.
|
||||
# This is changed at the end of the install process anyways.
|
||||
chown -R root:root /mnt
|
||||
export HOME=/mnt/server
|
||||
|
||||
## install game using steamcmd
|
||||
./steamcmd.sh +force_install_dir /mnt/server +login ${STEAM_USER} ${STEAM_PASS} ${STEAM_AUTH} $( [[ "${WINDOWS_INSTALL}" == "1" ]] && printf %s '+@sSteamCmdForcePlatformType windows' ) +app_update ${SRCDS_APPID} ${EXTRA_FLAGS} validate +quit ## other flags may be needed depending on install. looking at you cs 1.6
|
||||
|
||||
## set up 32 bit libraries
|
||||
mkdir -p /mnt/server/.steam/sdk32
|
||||
cp -v linux32/steamclient.so ../.steam/sdk32/steamclient.so
|
||||
|
||||
## set up 64 bit libraries
|
||||
mkdir -p /mnt/server/.steam/sdk64
|
||||
cp -v linux64/steamclient.so ../.steam/sdk64/steamclient.so
|
||||
container: 'ghcr.io/pelican-eggs/installers:debian'
|
||||
entrypoint: bash
|
||||
variables:
|
||||
-
|
||||
name: 'Default Map'
|
||||
description: 'The default map to use when starting the server.'
|
||||
env_variable: SRCDS_MAP
|
||||
default_value: cp_dustbowl
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- required
|
||||
- 'regex:/^(\w{1,20})$/'
|
||||
sort: 2
|
||||
-
|
||||
name: 'Game ID'
|
||||
description: 'The ID corresponding to the game to download and run using SRCDS.'
|
||||
env_variable: SRCDS_APPID
|
||||
default_value: 232250
|
||||
user_viewable: true
|
||||
user_editable: false
|
||||
rules:
|
||||
- required
|
||||
- 'in:232250'
|
||||
sort: 1
|
||||
-
|
||||
name: Steam
|
||||
description: |-
|
||||
The Steam Game Server Login Token to display servers publicly. Generate one at
|
||||
https://steamcommunity.com/dev/managegameservers
|
||||
env_variable: STEAM_ACC
|
||||
default_value: ''
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- required
|
||||
- string
|
||||
- alpha_num
|
||||
- 'size:32'
|
||||
sort: 3
|
||||
70
database/Seeders/eggs/voice-servers/egg-mumble-server.yaml
Normal file
70
database/Seeders/eggs/voice-servers/egg-mumble-server.yaml
Normal file
File diff suppressed because one or more lines are too long
118
database/Seeders/eggs/voice-servers/egg-teamspeak3-server.yaml
Normal file
118
database/Seeders/eggs/voice-servers/egg-teamspeak3-server.yaml
Normal file
File diff suppressed because one or more lines are too long
@@ -1,25 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('allocations')
|
||||
->whereNull('server_id')
|
||||
->update(['notes' => null]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// Not needed
|
||||
}
|
||||
};
|
||||
@@ -2,8 +2,8 @@
|
||||
# check for .env file or symlink and generate app keys if missing
|
||||
if [ -f /var/www/html/.env ]; then
|
||||
echo "external vars exist."
|
||||
# load specific env vars from .env used in the entrypoint and they are not already set
|
||||
for VAR in "APP_KEY" "APP_INSTALLED" "DB_CONNECTION" "DB_HOST" "DB_PORT"; do if ! (printenv | grep -q ${VAR}); then export $(grep ${VAR} .env | grep -ve "^#"); fi; done
|
||||
# load env vars from .env
|
||||
export $(grep -v '^#' .env | xargs)
|
||||
else
|
||||
echo "external vars don't exist."
|
||||
# webroot .env is symlinked to this path
|
||||
@@ -25,7 +25,7 @@ else
|
||||
fi
|
||||
|
||||
# create directories for volumes
|
||||
mkdir -p /pelican-data/database /pelican-data/storage/avatars /pelican-data/storage/fonts /pelican-data/storage/icons /pelican-data/plugins /var/www/html/storage/logs/supervisord 2>/dev/null
|
||||
mkdir -p /pelican-data/database /pelican-data/storage/avatars /pelican-data/storage/fonts /var/www/html/storage/logs/supervisord 2>/dev/null
|
||||
|
||||
# if the app is installed then we need to run migrations on start. New installs will run migrations when you run the installer.
|
||||
if [ "${APP_INSTALLED}" == "true" ]; then
|
||||
|
||||
@@ -41,7 +41,7 @@ stdout_logfile_maxbytes=0
|
||||
redirect_stderr=true
|
||||
|
||||
[program:supercronic]
|
||||
command=supercronic -overlapping /etc/crontabs/crontab
|
||||
command=supercronic -overlapping /etc/supercronic/crontab
|
||||
autostart=true
|
||||
autorestart=true
|
||||
redirect_stderr=true
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user