Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot]
0208bb4630 ci(release): bump version 2025-12-19 23:34:21 +00:00
837 changed files with 11743 additions and 19476 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +0,0 @@
<?php
namespace App\Enums;
enum StepPosition: string
{
case Before = 'before';
case After = 'after';
}

View File

@@ -1,9 +0,0 @@
<?php
namespace App\Enums;
enum TabPosition: string
{
case Before = 'before';
case After = 'after';
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>[] */

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -78,10 +78,7 @@ class ServerDeletionService
}
}
$server->allocations()->update([
'server_id' => null,
'notes' => null,
]);
$server->allocations()->update(['server_id' => null]);
$server->delete();
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ return [
'logo' => env('APP_LOGO'),
'favicon' => env('APP_FAVICON', '/pelican.ico'),
'version' => '1.0.0-beta31',
'version' => '1.0.0-beta30',
'timezone' => 'UTC',

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View 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

File diff suppressed because one or more lines are too long

View 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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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