Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot]
2f2ecd9995 ci(release): bump version 2025-04-07 23:00:58 +00:00
152 changed files with 1856 additions and 3424 deletions

View File

@@ -35,7 +35,7 @@ jobs:
strategy:
fail-fast: false
matrix:
php: [ 8.2, 8.3, 8.4 ]
php: [8.2, 8.3, 8.4]
steps:
- name: Code Checkout
uses: actions/checkout@v4
@@ -68,4 +68,4 @@ jobs:
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
- name: PHPStan
run: vendor/bin/phpstan --memory-limit=-1 --error-format=github
run: vendor/bin/phpstan --memory-limit=-1

4
.gitignore vendored
View File

@@ -1,6 +1,7 @@
/.phpunit.cache
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/storage/pail
@@ -23,7 +24,8 @@ yarn-error.log
/.vscode
public/assets/manifest.json
/database/*.sqlite*
/database/*.sqlite
/database/*.sqlite-journal
filament-monaco-editor/
_ide_helper*
/.phpstorm.meta.php

View File

@@ -1,9 +1,16 @@
# syntax=docker.io/docker/dockerfile:1.13-labs
# Pelican Production Dockerfile
##
# If you want to build this locally you want to run `docker build -f Dockerfile.dev`
##
# For those who want to build this Dockerfile themselves, uncomment lines 6-12 and replace "localhost:5000/base-php:$TARGETARCH" on lines 17 and 67 with "base".
# FROM --platform=$TARGETOS/$TARGETARCH php:8.3-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 opcache pcntl posix pdo_mysql
# RUN rm /usr/local/bin/install-php-extensions
# ================================
# Stage 1-1: Composer Install
@@ -75,16 +82,13 @@ 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 /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \
# Symlinks for env, database, and avatars
# Symlink to env/database path, as www-data won't be able to write to webroot
&& 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 \
# 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 \
# Create necessary directories
&& mkdir -p /pelican-data /var/run/supervisord /etc/supercronic \
# Finally allow www-data write permissions where necessary
&& chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord
# Configure Supervisor

View File

@@ -1,10 +1,10 @@
# ================================
# Stage 0: Build PHP Base Image
# ================================
FROM --platform=$TARGETOS/$TARGETARCH php:8.4-fpm-alpine
FROM --platform=$TARGETOS/$TARGETARCH php:8.3-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 opcache pcntl posix pdo_mysql pdo_pgsql
RUN install-php-extensions bcmath gd intl zip opcache pcntl posix pdo_mysql
RUN rm /usr/local/bin/install-php-extensions
RUN rm /usr/local/bin/install-php-extensions

View File

@@ -1,112 +0,0 @@
# syntax=docker.io/docker/dockerfile:1.13-labs
# Pelican Development Dockerfile
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 opcache pcntl posix pdo_mysql pdo_pgsql
RUN rm /usr/local/bin/install-php-extensions
# ================================
# Stage 1-1: Composer Install
# ================================
FROM --platform=$TARGETOS/$TARGETARCH base AS composer
WORKDIR /build
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
# Copy bare minimum to install Composer dependencies
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-interaction --no-autoloader --no-scripts
# ================================
# Stage 1-2: Yarn Install
# ================================
FROM --platform=$TARGETOS/$TARGETARCH node:20-alpine AS yarn
WORKDIR /build
# Copy bare minimum to install Yarn dependencies
COPY package.json yarn.lock ./
RUN yarn config set network-timeout 300000 \
&& yarn install --frozen-lockfile
# ================================
# Stage 2-1: Composer Optimize
# ================================
FROM --platform=$TARGETOS/$TARGETARCH composer AS composerbuild
# Copy full code to optimize autoload
COPY --exclude=Caddyfile --exclude=docker/ . ./
RUN composer dump-autoload --optimize
# ================================
# Stage 2-2: Build Frontend Assets
# ================================
FROM --platform=$TARGETOS/$TARGETARCH yarn AS yarnbuild
WORKDIR /build
# Copy full code
COPY --exclude=Caddyfile --exclude=docker/ . ./
COPY --from=composer /build .
RUN yarn run build
# ================================
# Stage 5: Build Final Application Image
# ================================
FROM --platform=$TARGETOS/$TARGETARCH base AS final
WORKDIR /var/www/html
# Install additional required libraries
RUN apk update && apk add --no-cache \
caddy ca-certificates supervisor supercronic
COPY --chown=root:www-data --chmod=640 --from=composerbuild /build .
COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public
# Set permissions
# First ensure all files are owned by root and restrict www-data to read access
RUN chown root:www-data ./ \
&& chmod 750 ./ \
# Files should not have execute set, but directories need it
&& find ./ -type d -exec chmod 750 {} \; \
# Create necessary directories
&& mkdir -p /pelican-data/storage /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \
# Symlinks for env, database, and avatars
&& 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 \
# 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
# Configure Supervisor
COPY docker/supervisord.conf /etc/supervisord.conf
COPY docker/Caddyfile /etc/caddy/Caddyfile
# Add Laravel scheduler to crontab
COPY docker/crontab /etc/supercronic/crontab
COPY docker/entrypoint.sh ./docker/entrypoint.sh
HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost/up || exit 1
EXPOSE 80 443
VOLUME /pelican-data
USER www-data
ENTRYPOINT [ "/bin/ash", "docker/entrypoint.sh" ]
CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ]

View File

@@ -14,9 +14,9 @@ class NodeVersionsCheck extends Check
public function run(): Result
{
$all = Node::all();
$all = Node::query()->count();
if ($all->isEmpty()) {
if ($all === 0) {
$result = Result::make()
->notificationMessage(trans('admin/health.results.nodeversions.no_nodes_created'))
->shortSummary(trans('admin/health.results.nodeversions.no_nodes'));
@@ -25,18 +25,16 @@ class NodeVersionsCheck extends Check
return $result;
}
$outdated = $all
->filter(fn (Node $node) => !isset($node->systemInformation()['exception']) && !$this->versionService->isLatestWings($node->systemInformation()['version']))
->count();
$all = $all->count();
$latestVersion = $this->versionService->latestWingsVersion();
$outdated = Node::query()->get()
->filter(fn (Node $node) => !isset($node->systemInformation()['exception']) && $node->systemInformation()['version'] !== $latestVersion)
->count();
$result = Result::make()
->meta([
'all' => $all,
'outdated' => $outdated,
'latestVersion' => $latestVersion,
])
->shortSummary($outdated === 0 ? trans('admin/health.results.nodeversions.all_up_to_date') : trans('admin/health.results.nodeversions.outdated', ['outdated' => $outdated, 'all' => $all]));

View File

@@ -3,6 +3,7 @@
namespace App\Console\Commands\Environment;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
class AppSettingsCommand extends Command
{
@@ -20,13 +21,9 @@ class AppSettingsCommand extends Command
if (!config('app.key')) {
$this->comment('Generating app key');
$this->call('key:generate');
Artisan::call('key:generate');
}
$this->comment('Creating storage link');
$this->call('storage:link');
$this->comment('Caching components & icons');
$this->call('filament:optimize');
Artisan::call('filament:optimize');
}
}

View File

@@ -31,11 +31,8 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule): void
{
if (config('cache.default') === 'redis') {
// https://laravel.com/docs/10.x/upgrade#redis-cache-tags
// This only needs to run when using redis. anything else throws an error.
$schedule->command('cache:prune-stale-tags')->hourly();
}
// https://laravel.com/docs/10.x/upgrade#redis-cache-tags
$schedule->command('cache:prune-stale-tags')->hourly();
// Execute scheduled commands for servers every minute, as if there was a normal cron running.
$schedule->command(ProcessRunnableCommand::class)->everyMinute()->withoutOverlapping();

View File

@@ -1,37 +0,0 @@
<?php
namespace App\Enums;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
enum BackupStatus: string implements HasColor, HasIcon, HasLabel
{
case InProgress = 'in_progress';
case Successful = 'successful';
case Failed = 'failed';
public function getIcon(): string
{
return match ($this) {
self::InProgress => 'tabler-circle-dashed',
self::Successful => 'tabler-circle-check',
self::Failed => 'tabler-circle-x',
};
}
public function getColor(): string
{
return match ($this) {
self::InProgress => 'primary',
self::Successful => 'success',
self::Failed => 'danger',
};
}
public function getLabel(): string
{
return str($this->value)->headline();
}
}

View File

@@ -14,24 +14,4 @@ enum RolePermissionModels: string
case Server = 'server';
case User = 'user';
case Webhook = 'webhook';
public function viewAny(): string
{
return RolePermissionPrefixes::ViewAny->value . ' ' . $this->value;
}
public function view(): string
{
return RolePermissionPrefixes::View->value . ' ' . $this->value;
}
public function create(): string
{
return RolePermissionPrefixes::Create->value . ' ' . $this->value;
}
public function update(): string
{
return RolePermissionPrefixes::Update->value . ' ' . $this->value;
}
}

View File

@@ -2,11 +2,11 @@
namespace App\Extensions\Avatar;
use App\Models\User;
use Filament\AvatarProviders\Contracts\AvatarProvider as AvatarProviderContract;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
abstract class AvatarProvider
abstract class AvatarProvider implements AvatarProviderContract
{
/**
* @var array<string, static>
@@ -33,8 +33,6 @@ abstract class AvatarProvider
abstract public function getId(): string;
abstract public function get(User $user): ?string;
public function getName(): string
{
return Str::title($this->getId());

View File

@@ -4,6 +4,8 @@ namespace App\Extensions\Avatar\Providers;
use App\Extensions\Avatar\AvatarProvider;
use App\Models\User;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
class GravatarProvider extends AvatarProvider
{
@@ -12,9 +14,10 @@ class GravatarProvider extends AvatarProvider
return 'gravatar';
}
public function get(User $user): string
public function get(Model|Authenticatable $record): string
{
return 'https://gravatar.com/avatar/' . md5($user->email);
/** @var User $record */
return 'https://gravatar.com/avatar/' . md5($record->email);
}
public static function register(): self

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Extensions\Avatar\Providers;
use App\Extensions\Avatar\AvatarProvider;
use Filament\AvatarProviders\UiAvatarsProvider as FilamentUiAvatarsProvider;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
class LocalAvatarProvider extends AvatarProvider
{
public function getId(): string
{
return 'local';
}
public function get(Model|Authenticatable $record): string
{
$path = 'avatars/' . $record->getKey() . '.png';
return Storage::disk('public')->exists($path) ? Storage::url($path) : (new FilamentUiAvatarsProvider())->get($record);
}
public static function register(): self
{
return new self();
}
}

View File

@@ -3,7 +3,9 @@
namespace App\Extensions\Avatar\Providers;
use App\Extensions\Avatar\AvatarProvider;
use App\Models\User;
use Filament\AvatarProviders\UiAvatarsProvider as FilamentUiAvatarsProvider;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
class UiAvatarsProvider extends AvatarProvider
{
@@ -17,10 +19,9 @@ class UiAvatarsProvider extends AvatarProvider
return 'UI Avatars';
}
public function get(User $user): ?string
public function get(Model|Authenticatable $record): string
{
// UI Avatars is the default of filament so just return null here
return null;
return (new FilamentUiAvatarsProvider())->get($record);
}
public static function register(): self

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Extensions;
use App\Models\DatabaseHost;
class DynamicDatabaseConnection
{
public const DB_CHARSET = 'utf8';
public const DB_COLLATION = 'utf8_unicode_ci';
public const DB_DRIVER = 'mysql';
/**
* Adds a dynamic database connection entry to the runtime config.
*/
public function set(string $connection, DatabaseHost|int $host, string $database = 'mysql'): void
{
if (!$host instanceof DatabaseHost) {
$host = DatabaseHost::query()->findOrFail($host);
}
config()->set('database.connections.' . $connection, [
'driver' => self::DB_DRIVER,
'host' => $host->host,
'port' => $host->port,
'database' => $database,
'username' => $host->username,
'password' => $host->password,
'charset' => self::DB_CHARSET,
'collation' => self::DB_COLLATION,
]);
}
}

View File

@@ -1,51 +0,0 @@
<?php
namespace App\Extensions\Features;
use Filament\Actions\Action;
use Illuminate\Foundation\Application;
abstract class FeatureProvider
{
/**
* @var array<string, static>
*/
protected static array $providers = [];
/**
* @param string[] $id
* @return self|static[]
*/
public static function getProviders(string|array|null $id = null): array|self
{
if (is_array($id)) {
return array_intersect_key(static::$providers, array_flip($id));
}
return $id ? static::$providers[$id] : static::$providers;
}
protected function __construct(protected Application $app)
{
if (array_key_exists($this->getId(), static::$providers)) {
if (!$this->app->runningUnitTests()) {
logger()->warning("Tried to create duplicate Feature provider with id '{$this->getId()}'");
}
return;
}
static::$providers[$this->getId()] = $this;
}
abstract public function getId(): string;
/**
* A matching subset string (case-insensitive) from the console output
*
* @return array<string>
*/
abstract public function getListeners(): array;
abstract public function getAction(): Action;
}

View File

@@ -1,127 +0,0 @@
<?php
namespace App\Extensions\Features;
use App\Facades\Activity;
use App\Models\Permission;
use App\Models\Server;
use App\Models\ServerVariable;
use App\Repositories\Daemon\DaemonPowerRepository;
use Closure;
use Exception;
use Filament\Actions\Action;
use Filament\Facades\Filament;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\HtmlString;
class GSLToken extends FeatureProvider
{
public function __construct(protected Application $app)
{
parent::__construct($app);
}
/** @return array<string> */
public function getListeners(): array
{
return [
'(gsl token expired)',
'(account not found)',
];
}
public function getId(): string
{
return 'gsl_token';
}
public function getAction(): Action
{
/** @var Server $server */
$server = Filament::getTenant();
/** @var ServerVariable $serverVariable */
$serverVariable = $server->serverVariables()->whereHas('variable', function (Builder $query) {
$query->where('env_variable', 'STEAM_ACC');
})->first();
return Action::make($this->getId())
->requiresConfirmation()
->modalHeading('Invalid GSL token')
->modalDescription('It seems like your Gameserver Login Token (GSL token) is invalid or has expired.')
->modalSubmitActionLabel('Update GSL Token')
->disabledForm(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server))
->form([
Placeholder::make('info')
->label(new HtmlString(Blade::render('You can either <x-filament::link href="https://steamcommunity.com/dev/managegameservers" target="_blank">generate a new one</x-filament::link> and enter it below or leave the field blank to remove it completely.'))),
TextInput::make('gsltoken')
->label('GSL Token')
->rules([
fn (): Closure => function (string $attribute, $value, Closure $fail) use ($serverVariable) {
$validator = Validator::make(['validatorkey' => $value], [
'validatorkey' => $serverVariable->variable->rules,
]);
if ($validator->fails()) {
$message = str($validator->errors()->first())->replace('validatorkey', $serverVariable->variable->name);
$fail($message);
}
},
])
->hintIcon('tabler-code')
->label(fn () => $serverVariable->variable->name)
->hintIconTooltip(fn () => implode('|', $serverVariable->variable->rules))
->prefix(fn () => '{{' . $serverVariable->variable->env_variable . '}}')
->helperText(fn () => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description),
])
->action(function (array $data, DaemonPowerRepository $powerRepository) use ($server, $serverVariable) {
/** @var Server $server */
$server = Filament::getTenant();
try {
$new = $data['gsltoken'] ?? '';
$original = $serverVariable->variable_value;
$serverVariable->update([
'variable_value' => $new,
]);
if ($original !== $new) {
Activity::event('server:startup.edit')
->property([
'variable' => $serverVariable->variable->env_variable,
'old' => $original,
'new' => $new,
])
->log();
}
$powerRepository->setServer($server)->send('restart');
Notification::make()
->title('GSL Token updated')
->body('Server will restart now.')
->success()
->send();
} catch (Exception $exception) {
Notification::make()
->title('Could not update GSL Token')
->body($exception->getMessage())
->danger()
->send();
}
});
}
public static function register(Application $app): self
{
return new self($app);
}
}

View File

@@ -1,100 +0,0 @@
<?php
namespace App\Extensions\Features;
use App\Facades\Activity;
use App\Models\Permission;
use App\Models\Server;
use App\Repositories\Daemon\DaemonPowerRepository;
use Exception;
use Filament\Actions\Action;
use Filament\Facades\Filament;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Select;
use Filament\Notifications\Notification;
use Illuminate\Foundation\Application;
class JavaVersion extends FeatureProvider
{
public function __construct(protected Application $app)
{
parent::__construct($app);
}
/** @return array<string> */
public function getListeners(): array
{
return [
'java.lang.UnsupportedClassVersionError',
'unsupported major.minor version',
'has been compiled by a more recent version of the java runtime',
'minecraft 1.17 requires running the server with java 16 or above',
'minecraft 1.18 requires running the server with java 17 or above',
'minecraft 1.19 requires running the server with java 17 or above',
];
}
public function getId(): string
{
return 'java_version';
}
public function getAction(): Action
{
/** @var Server $server */
$server = Filament::getTenant();
return Action::make($this->getId())
->requiresConfirmation()
->modalHeading('Unsupported Java Version')
->modalDescription('This server is currently running an unsupported version of Java and cannot be started.')
->modalSubmitActionLabel('Update Docker Image')
->disabledForm(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_DOCKER_IMAGE, $server))
->form([
Placeholder::make('java')
->label('Please select a supported version from the list below to continue starting the server.'),
Select::make('image')
->label('Docker Image')
->disabled(fn () => !in_array($server->image, $server->egg->docker_images))
->options(fn () => collect($server->egg->docker_images)->mapWithKeys(fn ($key, $value) => [$key => $value]))
->selectablePlaceholder(false)
->default(fn () => $server->image)
->notIn(fn () => $server->image)
->required()
->preload()
->native(false),
])
->action(function (array $data, DaemonPowerRepository $powerRepository) use ($server) {
try {
$new = $data['image'];
$original = $server->image;
$server->forceFill(['image' => $new])->saveOrFail();
if ($original !== $server->image) {
Activity::event('server:startup.image')
->property(['old' => $original, 'new' => $new])
->log();
}
$powerRepository->setServer($server)->send('restart');
Notification::make()
->title('Docker image updated')
->body('Server will restart now.')
->success()
->send();
} catch (Exception $exception) {
Notification::make()
->title('Could not update docker image')
->body($exception->getMessage())
->danger()
->send();
}
});
}
public static function register(Application $app): self
{
return new self($app);
}
}

View File

@@ -1,71 +0,0 @@
<?php
namespace App\Extensions\Features;
use App\Models\Server;
use App\Repositories\Daemon\DaemonFileRepository;
use App\Repositories\Daemon\DaemonPowerRepository;
use Exception;
use Filament\Actions\Action;
use Filament\Facades\Filament;
use Filament\Notifications\Notification;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
class MinecraftEula extends FeatureProvider
{
public function __construct(protected Application $app)
{
parent::__construct($app);
}
/** @return array<string> */
public function getListeners(): array
{
return [
'you need to agree to the eula in order to run the server',
];
}
public function getId(): string
{
return 'eula';
}
public function getAction(): Action
{
return Action::make($this->getId())
->requiresConfirmation()
->modalHeading('Minecraft EULA')
->modalDescription(new HtmlString(Blade::render('By pressing "I Accept" below you are indicating your agreement to the <x-filament::link href="https://minecraft.net/eula" target="_blank">Minecraft EULA </x-filament::link>.')))
->modalSubmitActionLabel('I Accept')
->action(function (DaemonFileRepository $fileRepository, DaemonPowerRepository $powerRepository) {
try {
/** @var Server $server */
$server = Filament::getTenant();
$fileRepository->setServer($server)->putContent('eula.txt', 'eula=true');
$powerRepository->setServer($server)->send('restart');
Notification::make()
->title('Minecraft EULA accepted')
->body('Server will restart now.')
->success()
->send();
} catch (Exception $exception) {
Notification::make()
->title('Could not accept Minecraft EULA')
->body($exception->getMessage())
->danger()
->send();
}
});
}
public static function register(Application $app): self
{
return new self($app);
}
}

View File

@@ -1,76 +0,0 @@
<?php
namespace App\Extensions\Features;
use Filament\Actions\Action;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
class PIDLimit extends FeatureProvider
{
public function __construct(protected Application $app)
{
parent::__construct($app);
}
/** @return array<string> */
public function getListeners(): array
{
return [
'pthread_create failed',
'failed to create thread',
'unable to create thread',
'unable to create native thread',
'unable to create new native thread',
'exception in thread "craft async scheduler management thread"',
];
}
public function getId(): string
{
return 'pid_limit';
}
public function getAction(): Action
{
return Action::make($this->getId())
->requiresConfirmation()
->icon('tabler-alert-triangle')
->modalHeading(fn () => auth()->user()->isAdmin() ? 'Memory or process limit reached...' : 'Possible resource limit reached...')
->modalDescription(new HtmlString(Blade::render(
auth()->user()->isAdmin() ? <<<'HTML'
<p>
This server has reached the maximum process or memory limit.
</p>
<p class="mt-4">
Increasing <code>container_pid_limit</code> in the wings
configuration, <code>config.yml</code>, might help resolve
this issue.
</p>
<p class="mt-4">
<b>Note: Wings must be restarted for the configuration file changes to take effect</b>
</p>
HTML
:
<<<'HTML'
<p>
This server is attempting to use more resources than allocated. Please contact the administrator
and give them the error below.
</p>
<p class="mt-4">
<code>
pthread_create failed, Possibly out of memory or process/resource limits reached
</code>
</p>
HTML
)))
->modalCancelActionLabel('Close')
->action(fn () => null);
}
public static function register(Application $app): self
{
return new self($app);
}
}

View File

@@ -1,64 +0,0 @@
<?php
namespace App\Extensions\Features;
use Filament\Actions\Action;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
class SteamDiskSpace extends FeatureProvider
{
public function __construct(protected Application $app)
{
parent::__construct($app);
}
/** @return array<string> */
public function getListeners(): array
{
return [
'steamcmd needs 250mb of free disk space to update',
'0x202 after update job',
];
}
public function getId(): string
{
return 'steam_disk_space';
}
public function getAction(): Action
{
return Action::make($this->getId())
->requiresConfirmation()
->modalHeading('Out of available disk space...')
->modalDescription(new HtmlString(Blade::render(
auth()->user()->isAdmin() ? <<<'HTML'
<p>
This server has run out of available disk space and cannot complete the install or update
process.
</p>
<p class="mt-4">
Ensure the machine has enough disk space by typing{' '}
<code class="rounded py-1 px-2">df -h</code> on the machine hosting
this server. Delete files or increase the available disk space to resolve the issue.
</p>
HTML
:
<<<'HTML'
<p>
This server has run out of available disk space and cannot complete the install or update
process. Please get in touch with the administrator(s) and inform them of disk space issues.
</p>
HTML
)))
->modalCancelActionLabel('Close')
->action(fn () => null);
}
public static function register(Application $app): self
{
return new self($app);
}
}

View File

@@ -13,7 +13,6 @@ use Filament\Actions\Action;
use Filament\Forms\Components\Actions;
use Filament\Forms\Components\Actions\Action as FormAction;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Group;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Section;
@@ -34,7 +33,6 @@ use Filament\Pages\Concerns\InteractsWithHeaderActions;
use Filament\Pages\Page;
use Filament\Support\Enums\MaxWidth;
use Illuminate\Http\Client\Factory;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Notification as MailNotification;
use Illuminate\Support\Str;
@@ -138,7 +136,8 @@ class Settings extends Page implements HasForms
->placeholder('/pelican.ico'),
]),
Group::make()
->columns(2)
->columnSpan(2)
->columns(4)
->schema([
Toggle::make('APP_DEBUG')
->label(trans('admin/setting.general.debug_mode'))
@@ -160,26 +159,13 @@ class Settings extends Page implements HasForms
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('FILAMENT_TOP_NAVIGATION', (bool) $state))
->default(env('FILAMENT_TOP_NAVIGATION', config('panel.filament.top-navigation'))),
]),
Group::make()
->columns(2)
->schema([
Select::make('FILAMENT_AVATAR_PROVIDER')
->label(trans('admin/setting.general.avatar_provider'))
->columnSpan(2)
->native(false)
->options(collect(AvatarProvider::getAll())->mapWithKeys(fn ($provider) => [$provider->getId() => $provider->getName()]))
->selectablePlaceholder(false)
->default(env('FILAMENT_AVATAR_PROVIDER', config('panel.filament.avatar-provider'))),
Toggle::make('FILAMENT_UPLOADABLE_AVATARS')
->label(trans('admin/setting.general.uploadable_avatars'))
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->formatStateUsing(fn ($state) => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('FILAMENT_UPLOADABLE_AVATARS', (bool) $state))
->default(env('FILAMENT_UPLOADABLE_AVATARS', config('panel.filament.uploadable-avatars'))),
]),
ToggleButtons::make('PANEL_USE_BINARY_PREFIX')
->label(trans('admin/setting.general.unit_prefix'))
@@ -202,18 +188,12 @@ class Settings extends Page implements HasForms
->formatStateUsing(fn ($state): int => (int) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('APP_2FA_REQUIRED', (int) $state))
->default(env('APP_2FA_REQUIRED', config('panel.auth.2fa_required'))),
Select::make('FILAMENT_WIDTH')
->label(trans('admin/setting.general.display_width'))
->native(false)
->options(MaxWidth::class)
->selectablePlaceholder(false)
->default(env('FILAMENT_WIDTH', config('panel.filament.display-width'))),
TagsInput::make('TRUSTED_PROXIES')
->label(trans('admin/setting.general.trusted_proxies'))
->separator()
->splitKeys(['Tab', ' '])
->placeholder(trans('admin/setting.general.trusted_proxies_help'))
->default(env('TRUSTED_PROXIES', implode(',', Arr::wrap(config('trustedproxy.proxies')))))
->default(env('TRUSTED_PROXIES', implode(',', config('trustedproxy.proxies'))))
->hintActions([
FormAction::make('clear')
->label(trans('admin/setting.general.clear'))
@@ -248,6 +228,12 @@ class Settings extends Page implements HasForms
$set('TRUSTED_PROXIES', $ips->values()->all());
}),
]),
Select::make('FILAMENT_WIDTH')
->label(trans('admin/setting.general.display_width'))
->native(false)
->options(MaxWidth::class)
->selectablePlaceholder(false)
->default(env('FILAMENT_WIDTH', config('panel.filament.display-width'))),
];
}
@@ -729,17 +715,10 @@ class Settings extends Page implements HasForms
->onColor('success')
->offColor('danger')
->live()
->columnSpan(1)
->columnSpanFull()
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_EDITABLE_SERVER_DESCRIPTIONS', (bool) $state))
->default(env('PANEL_EDITABLE_SERVER_DESCRIPTIONS', config('panel.editable_server_descriptions'))),
FileUpload::make('ConsoleFonts')
->hint(trans('admin/setting.misc.server.console_font_hint'))
->label(trans('admin/setting.misc.server.console_font_upload'))
->directory('fonts')
->columnSpan(1)
->maxFiles(1)
->preserveFilenames(),
]),
Section::make(trans('admin/setting.misc.webhook.title'))
->description(trans('admin/setting.misc.webhook.helper'))
@@ -768,7 +747,6 @@ class Settings extends Page implements HasForms
{
try {
$data = $this->form->getState();
unset($data['ConsoleFonts']);
// Convert bools to a string, so they are correctly written to the .env file
$data = array_map(fn ($value) => is_bool($value) ? ($value ? 'true' : 'false') : $value, $data);

View File

@@ -16,7 +16,6 @@ use Filament\Tables\Actions\EditAction;
use Filament\Tables\Actions\ViewAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
class DatabaseHostResource extends Resource
{
@@ -28,7 +27,7 @@ class DatabaseHostResource extends Resource
public static function getNavigationBadge(): ?string
{
return (string) static::getEloquentQuery()->count() ?: null;
return static::getModel()::count() ?: null;
}
public static function getNavigationLabel(): string
@@ -145,7 +144,7 @@ class DatabaseHostResource extends Resource
->preload()
->helperText(trans('admin/databasehost.linked_nodes_help'))
->label(trans('admin/databasehost.linked_nodes'))
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'))),
->relationship('nodes', 'name'),
]),
]);
}
@@ -159,13 +158,4 @@ class DatabaseHostResource extends Resource
'edit' => Pages\EditDatabaseHost::route('/{record}/edit'),
];
}
public static function getEloquentQuery(): Builder
{
$query = parent::getEloquentQuery();
return $query->whereHas('nodes', function (Builder $query) {
$query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'));
})->orDoesntHave('nodes');
}
}

View File

@@ -17,7 +17,6 @@ use Filament\Notifications\Notification;
use Filament\Resources\Pages\CreateRecord;
use Filament\Resources\Pages\CreateRecord\Concerns\HasWizard;
use Filament\Support\Exceptions\Halt;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\HtmlString;
use Illuminate\Support\Str;
@@ -146,7 +145,7 @@ class CreateDatabaseHost extends CreateRecord
->preload()
->helperText(trans('admin/databasehost.linked_nodes_help'))
->label(trans('admin/databasehost.linked_nodes'))
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'))),
->relationship('nodes', 'name'),
]),
];
}

View File

@@ -21,7 +21,7 @@ class EggResource extends Resource
public static function getNavigationGroup(): ?string
{
return config('panel.filament.top-navigation', false) ? null : trans('admin/dashboard.server');
return trans('admin/dashboard.server');
}
public static function getNavigationLabel(): string

View File

@@ -18,7 +18,6 @@ use Filament\Tables\Actions\EditAction;
use Filament\Tables\Actions\ViewAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
class MountResource extends Resource
{
@@ -45,7 +44,7 @@ class MountResource extends Resource
public static function getNavigationBadge(): ?string
{
return (string) static::getEloquentQuery()->count() ?: null;
return static::getModel()::count() ?: null;
}
public static function getNavigationGroup(): ?string
@@ -148,7 +147,7 @@ class MountResource extends Resource
->preload(),
Select::make('nodes')->multiple()
->label(trans('admin/mount.nodes'))
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id')))
->relationship('nodes', 'name')
->searchable(['name', 'fqdn'])
->preload(),
]),
@@ -171,13 +170,4 @@ class MountResource extends Resource
'edit' => Pages\EditMount::route('/{record}/edit'),
];
}
public static function getEloquentQuery(): Builder
{
$query = parent::getEloquentQuery();
return $query->whereHas('nodes', function (Builder $query) {
$query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'));
})->orDoesntHave('nodes');
}
}

View File

@@ -6,7 +6,6 @@ use App\Filament\Admin\Resources\NodeResource\Pages;
use App\Filament\Admin\Resources\NodeResource\RelationManagers;
use App\Models\Node;
use Filament\Resources\Resource;
use Illuminate\Database\Eloquent\Builder;
class NodeResource extends Resource
{
@@ -33,12 +32,12 @@ class NodeResource extends Resource
public static function getNavigationGroup(): ?string
{
return config('panel.filament.top-navigation', false) ? null : trans('admin/dashboard.server');
return trans('admin/dashboard.server');
}
public static function getNavigationBadge(): ?string
{
return (string) static::getEloquentQuery()->count() ?: null;
return static::getModel()::count() ?: null;
}
public static function getRelations(): array
@@ -57,11 +56,4 @@ class NodeResource extends Resource
'edit' => Pages\EditNode::route('/{record}/edit'),
];
}
public static function getEloquentQuery(): Builder
{
$query = parent::getEloquentQuery();
return $query->whereIn('id', auth()->user()->accessibleNodes()->pluck('id'));
}
}

View File

@@ -7,7 +7,6 @@ use App\Models\Node;
use Filament\Forms;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
@@ -150,15 +149,14 @@ class CreateNode extends CreateRecord
->required()
->maxLength(100),
Hidden::make('scheme')
->default(fn () => request()->isSecure() ? 'https' : 'http'),
Hidden::make('behind_proxy')
->default(false),
ToggleButtons::make('connection')
ToggleButtons::make('scheme')
->label(trans('admin/node.ssl'))
->columnSpan(1)
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 1,
])
->inline()
->helperText(function (Get $get) {
if (request()->isSecure()) {
@@ -171,29 +169,20 @@ class CreateNode extends CreateRecord
return '';
})
->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure())
->disableOptionWhen(fn (string $value): bool => $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');
}),
->default(fn () => request()->isSecure() ? 'https' : 'http'),
]),
Step::make('advanced')
->label(trans('admin/node.tabs.advanced_settings'))

View File

@@ -14,7 +14,6 @@ use Filament\Forms;
use Filament\Forms\Components\Actions as FormActions;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\Tabs\Tab;
@@ -200,9 +199,7 @@ class EditNode extends EditRecord
])
->required()
->maxLength(100),
Hidden::make('scheme'),
Hidden::make('behind_proxy'),
ToggleButtons::make('connection')
ToggleButtons::make('scheme')
->label(trans('admin/node.ssl'))
->columnSpan(1)
->inline()
@@ -217,30 +214,20 @@ class EditNode extends EditRecord
return '';
})
->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure())
->disableOptionWhen(fn (string $value): bool => $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',
])
->formatStateUsing(fn (Get $get) => $get('scheme') === 'http' ? 'http' : ($get('behind_proxy') ? 'https_proxy' : 'https'))
->live()
->dehydrated(false)
->afterStateUpdated(function ($state, Set $set) {
$set('scheme', $state === 'http' ? 'http' : 'https');
$set('behind_proxy', $state === 'https_proxy');
}),
]),
->default(fn () => request()->isSecure() ? 'https' : 'http'), ]),
Tab::make('adv')
->label(trans('admin/node.tabs.advanced_settings'))
->columns([

View File

@@ -12,7 +12,8 @@ use Filament\Forms\Components\TextInput;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables\Actions\Action;
use Filament\Tables;
use Filament\Tables\Actions\BulkActionGroup;
use Filament\Tables\Actions\DeleteBulkAction;
use Filament\Tables\Columns\SelectColumn;
use Filament\Tables\Columns\TextColumn;
@@ -31,12 +32,18 @@ class AllocationsRelationManager extends RelationManager
public function setTitle(): string
{
return trans('admin/server.allocations');
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('address')
->recordTitleAttribute('ip')
// Non Primary Allocations
// ->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->id !== $allocation->server?->allocation_id)
// All assigned allocations
->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->server_id === null)
->paginationPageOptions(['10', '20', '50', '100', '200', '500'])
->searchable()
@@ -65,14 +72,14 @@ class AllocationsRelationManager extends RelationManager
->label(trans('admin/node.table.ip')),
])
->headerActions([
Action::make('create new allocation')
Tables\Actions\Action::make('create new allocation')
->label(trans('admin/node.create_allocation'))
->form(fn () => [
Select::make('allocation_ip')
->options(collect($this->getOwnerRecord()->ipAddresses())->mapWithKeys(fn (string $ip) => [$ip => $ip]))
->label(trans('admin/node.ip_address'))
->inlineLabel()
->ip()
->ipv4()
->helperText(trans('admin/node.ip_help'))
->afterStateUpdated(fn (Set $set) => $set('allocation_ports', []))
->live()
@@ -89,15 +96,19 @@ class AllocationsRelationManager extends RelationManager
->inlineLabel()
->live()
->disabled(fn (Get $get) => empty($get('allocation_ip')))
->afterStateUpdated(fn ($state, Set $set, Get $get) => $set('allocation_ports', CreateServer::retrieveValidPorts($this->getOwnerRecord(), $state, $get('allocation_ip'))))
->afterStateUpdated(fn ($state, Set $set, Get $get) => $set('allocation_ports',
CreateServer::retrieveValidPorts($this->getOwnerRecord(), $state, $get('allocation_ip')))
)
->splitKeys(['Tab', ' ', ','])
->required(),
])
->action(fn (array $data, AssignmentService $service) => $service->handle($this->getOwnerRecord(), $data)),
])
->groupedBulkActions([
DeleteBulkAction::make()
->authorize(fn () => auth()->user()->can('update node')),
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make()
->authorize(fn () => auth()->user()->can('update node')),
]),
]);
}
}

View File

@@ -23,7 +23,7 @@ class NodeCpuChart extends ChartWidget
$cpu = collect(cache()->get("nodes.{$this->node->id}.cpu_percent"))
->slice(-10)
->map(fn ($value, $key) => [
'cpu' => round($value * $threads, 2),
'cpu' => Number::format($value * $threads, maxPrecision: 2),
'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
])
->all();

View File

@@ -20,7 +20,7 @@ class NodeMemoryChart extends ChartWidget
{
$memUsed = collect(cache()->get("nodes.{$this->node->id}.memory_used"))->slice(-10)
->map(fn ($value, $key) => [
'memory' => round(config('panel.use_binary_prefix') ? $value / 1024 / 1024 / 1024 : $value / 1000 / 1000 / 1000, 2),
'memory' => Number::format(config('panel.use_binary_prefix') ? $value / 1024 / 1024 / 1024 : $value / 1000 / 1000 / 1000, maxPrecision: 2),
'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
])
->all();

View File

@@ -4,6 +4,7 @@ namespace App\Filament\Admin\Resources\NodeResource\Widgets;
use App\Models\Node;
use Filament\Widgets\ChartWidget;
use Illuminate\Support\Number;
class NodeStorageChart extends ChartWidget
{
@@ -45,8 +46,8 @@ class NodeStorageChart extends ChartWidget
$unused = $total - $used;
$used = round($used, 2);
$unused = round($unused, 2);
$used = Number::format($used, maxPrecision: 2);
$unused = Number::format($unused, maxPrecision: 2);
return [
'datasets' => [

View File

@@ -10,7 +10,6 @@ use Filament\Forms\Components\Component;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Forms\Get;
@@ -49,7 +48,7 @@ class RoleResource extends Resource
public static function getNavigationGroup(): ?string
{
return config('panel.filament.top-navigation', false) ? trans('admin/dashboard.advanced') : trans('admin/dashboard.user');
return trans('admin/dashboard.user');
}
public static function getNavigationBadge(): ?string
@@ -70,11 +69,6 @@ class RoleResource extends Resource
->badge()
->counts('permissions')
->formatStateUsing(fn (Role $role, $state) => $role->isRootAdmin() ? trans('admin/role.all') : $state),
TextColumn::make('nodes.name')
->icon('tabler-server-2')
->label(trans('admin/role.nodes'))
->badge()
->placeholder(trans('admin/role.all')),
TextColumn::make('users_count')
->label(trans('admin/role.users'))
->counts('users')
@@ -131,14 +125,6 @@ class RoleResource extends Resource
->label(trans('admin/role.permissions'))
->content(trans('admin/role.root_admin', ['role' => Role::ROOT_ADMIN]))
->visible(fn (Get $get) => $get('name') === Role::ROOT_ADMIN),
Select::make('nodes')
->label(trans('admin/role.nodes'))
->multiple()
->relationship('nodes', 'name')
->searchable(['name', 'fqdn'])
->preload()
->hint(trans('admin/role.nodes_hint'))
->hidden(fn (Get $get) => $get('name') === Role::ROOT_ADMIN),
]);
}

View File

@@ -3,12 +3,8 @@
namespace App\Filament\Admin\Resources;
use App\Filament\Admin\Resources\ServerResource\Pages;
use App\Models\Mount;
use App\Models\Server;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Get;
use Filament\Resources\Resource;
use Illuminate\Database\Eloquent\Builder;
class ServerResource extends Resource
{
@@ -35,35 +31,12 @@ class ServerResource extends Resource
public static function getNavigationGroup(): ?string
{
return config('panel.filament.top-navigation', false) ? null : trans('admin/dashboard.server');
return trans('admin/dashboard.server');
}
public static function getNavigationBadge(): ?string
{
return (string) static::getEloquentQuery()->count() ?: null;
}
public static function getMountCheckboxList(Get $get): CheckboxList
{
$allowedMounts = Mount::all();
$node = $get('node_id');
$egg = $get('egg_id');
if ($node && $egg) {
$allowedMounts = $allowedMounts->filter(fn (Mount $mount) => ($mount->nodes->isEmpty() || $mount->nodes->contains($node)) &&
($mount->eggs->isEmpty() || $mount->eggs->contains($egg))
);
}
return CheckboxList::make('mounts')
->label('')
->relationship('mounts')
->live()
->options(fn () => $allowedMounts->mapWithKeys(fn ($mount) => [$mount->id => $mount->name]))
->descriptions(fn () => $allowedMounts->mapWithKeys(fn ($mount) => [$mount->id => "$mount->source -> $mount->target"]))
->helperText(fn () => $allowedMounts->isEmpty() ? trans('admin/server.no_mounts') : null)
->bulkToggleable()
->columnSpanFull();
return static::getModel()::count() ?: null;
}
public static function getPages(): array
@@ -74,13 +47,4 @@ class ServerResource extends Resource
'edit' => Pages\EditServer::route('/{record}/edit'),
];
}
public static function getEloquentQuery(): Builder
{
$query = parent::getEloquentQuery();
return $query->whereHas('node', function (Builder $query) {
$query->whereIn('id', auth()->user()->accessibleNodes()->pluck('id'));
});
}
}

View File

@@ -15,6 +15,7 @@ use Closure;
use Exception;
use Filament\Forms;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Grid;
@@ -108,20 +109,14 @@ class CreateServer extends CreateRecord
->disabledOn('edit')
->prefixIcon('tabler-server-2')
->selectablePlaceholder(false)
->default(function () {
/** @var ?Node $latestNode */
$latestNode = auth()->user()->accessibleNodes()->latest()->first();
$this->node = $latestNode;
return $this->node?->id;
})
->default(fn () => ($this->node = Node::query()->latest()->first())?->id)
->columnSpan([
'default' => 1,
'sm' => 2,
'md' => 2,
])
->live()
->relationship('node', 'name', fn (Builder $query) => $query->whereIn('id', auth()->user()->accessibleNodes()->pluck('id')))
->relationship('node', 'name')
->searchable()
->preload()
->afterStateUpdated(function (Set $set, $state) {
@@ -188,7 +183,10 @@ class CreateServer extends CreateRecord
$set('allocation_additional', null);
$set('allocation_additional.needstobeastringhere.extra_allocations', null);
})
->getOptionLabelFromRecordUsing(fn (Allocation $allocation) => $allocation->address)
->getOptionLabelFromRecordUsing(
fn (Allocation $allocation) => "$allocation->ip:$allocation->port" .
($allocation->ip_alias ? " ($allocation->ip_alias)" : '')
)
->placeholder(function (Get $get) {
$node = Node::find($get('node_id'));
@@ -214,7 +212,7 @@ class CreateServer extends CreateRecord
->label(trans('admin/server.ip_address'))->inlineLabel()
->helperText(trans('admin/server.ip_address_helper'))
->afterStateUpdated(fn (Set $set) => $set('allocation_ports', []))
->ip()
->ipv4()
->live()
->required(),
TextInput::make('allocation_alias')
@@ -265,7 +263,10 @@ class CreateServer extends CreateRecord
->columnSpan(2)
->disabled(fn (Get $get) => $get('../../node_id') === null)
->searchable(['ip', 'port', 'ip_alias'])
->getOptionLabelFromRecordUsing(fn (Allocation $allocation) => $allocation->address)
->getOptionLabelFromRecordUsing(
fn (Allocation $allocation) => "$allocation->ip:$allocation->port" .
($allocation->ip_alias ? " ($allocation->ip_alias)" : '')
)
->placeholder(trans('admin/server.select_additional'))
->disableOptionsWhenSelectedInSiblingRepeaterItems()
->relationship(
@@ -425,7 +426,7 @@ class CreateServer extends CreateRecord
Repeater::make('server_variables')
->label('')
->relationship('serverVariables', fn (Builder $query) => $query->orderByPowerJoins('variable.sort'))
->relationship('serverVariables')
->saveRelationshipsBeforeChildrenUsing(null)
->saveRelationshipsUsing(null)
->grid(2)
@@ -743,7 +744,7 @@ class CreateServer extends CreateRecord
'lg' => 4,
])
->columnSpan(6)
->schema(fn (Get $get) => [
->schema([
Select::make('select_image')
->label(trans('admin/server.image_name'))
->live()
@@ -791,13 +792,19 @@ class CreateServer extends CreateRecord
]),
KeyValue::make('docker_labels')
->live()
->label('Container Labels')
->keyLabel(trans('admin/server.title'))
->valueLabel(trans('admin/server.description'))
->columnSpanFull(),
ServerResource::getMountCheckboxList($get),
CheckboxList::make('mounts')
->label('Mounts')
->live()
->relationship('mounts')
->options(fn () => $this->node?->mounts->mapWithKeys(fn ($mount) => [$mount->id => $mount->name]) ?? [])
->descriptions(fn () => $this->node?->mounts->mapWithKeys(fn ($mount) => [$mount->id => "$mount->source -> $mount->target"]) ?? [])
->helperText(fn () => $this->node?->mounts->isNotEmpty() ? '' : 'No Mounts exist for this Node')
->columnSpanFull(),
]),
]),
])

View File

@@ -2,7 +2,7 @@
namespace App\Filament\Admin\Resources\ServerResource\Pages;
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
use App\Enums\ServerState;
use App\Enums\SuspendAction;
use App\Filament\Admin\Resources\ServerResource;
use App\Filament\Admin\Resources\ServerResource\RelationManagers\AllocationsRelationManager;
@@ -13,6 +13,7 @@ use App\Models\Allocation;
use App\Models\Database;
use App\Models\DatabaseHost;
use App\Models\Egg;
use App\Models\Mount;
use App\Models\Node;
use App\Models\Server;
use App\Models\ServerVariable;
@@ -32,6 +33,7 @@ use Filament\Actions;
use Filament\Forms;
use Filament\Forms\Components\Actions as FormActions;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Grid;
@@ -51,7 +53,6 @@ use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use Filament\Support\Enums\Alignment;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Arr;
@@ -136,39 +137,7 @@ class EditServer extends EditRecord
'sm' => 1,
'md' => 1,
'lg' => 1,
])
->hintAction(
Action::make('view_install_log')
->label(trans('admin/server.view_install_log'))
//->visible(fn (Server $server) => $server->isFailedInstall())
->modalHeading('')
->modalSubmitAction(false)
->modalFooterActionsAlignment(Alignment::Right)
->modalCancelActionLabel(trans('filament::components/modal.actions.close.label'))
->form([
MonacoEditor::make('logs')
->hiddenLabel()
->placeholderText(trans('admin/server.no_log'))
->formatStateUsing(function (Server $server, DaemonServerRepository $serverRepository) {
try {
return $serverRepository->setServer($server)->getInstallLogs();
} catch (ConnectionException) {
Notification::make()
->title(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
->body(trans('admin/server.notifications.log_failed'))
->color('warning')
->warning()
->send();
} catch (Exception) {
return '';
}
return '';
})
->language('shell')
->view('filament.plugins.monaco-editor-logs'),
])
),
]),
Textarea::make('description')
->label(trans('admin/server.description'))
@@ -208,7 +177,7 @@ class EditServer extends EditRecord
->maxLength(255),
Select::make('node_id')
->label(trans('admin/server.node'))
->relationship('node', 'name', fn (Builder $query) => $query->whereIn('id', auth()->user()->accessibleNodes()->pluck('id')))
->relationship('node', 'name')
->columnSpan([
'default' => 2,
'sm' => 1,
@@ -517,7 +486,6 @@ class EditServer extends EditRecord
]),
KeyValue::make('docker_labels')
->live()
->label(trans('admin/server.container_labels'))
->keyLabel(trans('admin/server.title'))
->valueLabel(trans('admin/server.description'))
@@ -627,7 +595,9 @@ class EditServer extends EditRecord
]);
}
return $query->orderByPowerJoins('variable.sort');
return $query
->join('egg_variables', 'server_variables.variable_id', '=', 'egg_variables.id')
->orderBy('egg_variables.sort');
})
->grid()
->mutateRelationshipDataBeforeSaveUsing(function (array &$data): array {
@@ -682,8 +652,14 @@ class EditServer extends EditRecord
]),
Tab::make(trans('admin/server.mounts'))
->icon('tabler-layers-linked')
->schema(fn (Get $get) => [
ServerResource::getMountCheckboxList($get),
->schema([
CheckboxList::make('mounts')
->label('')
->relationship('mounts')
->options(fn (Server $server) => $server->node->mounts->filter(fn (Mount $mount) => $mount->eggs->contains($server->egg))->mapWithKeys(fn (Mount $mount) => [$mount->id => $mount->name]))
->descriptions(fn (Server $server) => $server->node->mounts->mapWithKeys(fn (Mount $mount) => [$mount->id => "$mount->source -> $mount->target"]))
->helperText(fn (Server $server) => $server->node->mounts->isNotEmpty() ? '' : trans('admin/server.no_mounts'))
->columnSpanFull(),
]),
Tab::make(trans('admin/server.databases'))
->hidden(fn () => !auth()->user()->can('viewList database'))
@@ -716,8 +692,8 @@ class EditServer extends EditRecord
->requiresConfirmation()
->modalIcon('tabler-database-x')
->modalHeading(trans('admin/server.delete_db_heading'))
->modalSubmitActionLabel(trans('filament-actions::delete.single.label'))
->modalDescription(fn (Get $get) => trans('admin/server.delete_db', ['name' => $get('database')]))
->modalSubmitActionLabel(fn (Get $get) => 'Delete ' . $get('database') . '?')
->modalDescription(fn (Get $get) => trans('admin/server.delete_db') . $get('database') . '?')
->action(function (DatabaseManagementService $databaseManagementService, $record) {
$databaseManagementService->delete($record);
$this->fillForm();
@@ -833,12 +809,12 @@ class EditServer extends EditRecord
Action::make('toggleInstall')
->label(trans('admin/server.toggle_install'))
->disabled(fn (Server $server) => $server->isSuspended())
->modal(fn (Server $server) => $server->isFailedInstall())
->modal(fn (Server $server) => $server->status === ServerState::InstallFailed)
->modalHeading(trans('admin/server.toggle_install_failed_header'))
->modalDescription(trans('admin/server.toggle_install_failed_desc'))
->modalSubmitActionLabel(trans('admin/server.reinstall'))
->action(function (ToggleInstallService $toggleService, ReinstallServerService $reinstallService, Server $server) {
if ($server->isFailedInstall()) {
if ($server->status === ServerState::InstallFailed) {
try {
$reinstallService->handle($server);
@@ -851,7 +827,7 @@ class EditServer extends EditRecord
} catch (Exception) {
Notification::make()
->title(trans('admin/server.notifications.reinstall_failed'))
->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
->body(trans('admin/server.error_connecting', ['node' => $server->node->name]))
->danger()
->send();
}
@@ -900,7 +876,7 @@ class EditServer extends EditRecord
Notification::make()
->warning()
->title(trans('admin/server.notifications.server_suspension'))
->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
->body(trans('admin/server.error_connecting', ['node' => $server->node->name]))
->send();
}
}),
@@ -922,7 +898,7 @@ class EditServer extends EditRecord
Notification::make()
->warning()
->title(trans('admin/server.notifications.server_suspension'))
->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
->body(trans('admin/server.error_connecting', ['node' => $server->node->name]))
->send();
}
}),
@@ -987,7 +963,7 @@ class EditServer extends EditRecord
} catch (Exception) {
Notification::make()
->title(trans('admin/server.notifications.reinstall_failed'))
->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
->body(trans('admin/server.error_connecting', ['node' => $server->node->name]))
->danger()
->send();
}
@@ -1103,7 +1079,7 @@ class EditServer extends EditRecord
$data['description'] = '';
}
unset($data['docker'], $data['status'], $data['allocation_id']);
unset($data['docker'], $data['status']);
return $data;
}

View File

@@ -16,7 +16,6 @@ use Filament\Support\Exceptions\Halt;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\AssociateAction;
use Filament\Tables\Actions\CreateAction;
use Filament\Tables\Actions\DissociateAction;
use Filament\Tables\Actions\DissociateBulkAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
@@ -35,18 +34,15 @@ class AllocationsRelationManager extends RelationManager
{
return $table
->selectCurrentPageOnly()
->recordTitleAttribute('address')
->recordTitle(fn (Allocation $allocation) => $allocation->address)
->recordTitleAttribute('ip')
->recordTitle(fn (Allocation $allocation) => "$allocation->ip:$allocation->port")
->checkIfRecordIsSelectableUsing(fn (Allocation $record) => $record->id !== $this->getOwnerRecord()->allocation_id)
->inverseRelationship('server')
->heading(trans('admin/server.allocations'))
->columns([
TextColumn::make('ip')
->label(trans('admin/server.ip_address')),
TextColumn::make('port')
->label(trans('admin/server.port')),
TextInputColumn::make('ip_alias')
->label(trans('admin/server.alias')),
TextColumn::make('ip')->label(trans('admin/server.ip_address')),
TextColumn::make('port')->label(trans('admin/server.port')),
TextInputColumn::make('ip_alias')->label(trans('admin/server.alias')),
IconColumn::make('primary')
->icon(fn ($state) => match ($state) {
true => 'tabler-star-filled',
@@ -62,11 +58,8 @@ class AllocationsRelationManager extends RelationManager
])
->actions([
Action::make('make-primary')
->label(trans('admin/server.make_primary'))
->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]) && $this->deselectAllTableRecords())
->hidden(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id),
DissociateAction::make()
->hidden(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id),
->label(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id ? '' : trans('admin/server.make_primary')),
])
->headerActions([
CreateAction::make()->label(trans('admin/server.create_allocation'))
@@ -76,8 +69,7 @@ class AllocationsRelationManager extends RelationManager
->options(collect($this->getOwnerRecord()->node->ipAddresses())->mapWithKeys(fn (string $ip) => [$ip => $ip]))
->label(trans('admin/server.ip_address'))
->inlineLabel()
->ip()
->live()
->ipv4()
->afterStateUpdated(fn (Set $set) => $set('allocation_ports', []))
->required(),
TextInput::make('allocation_alias')
@@ -91,8 +83,9 @@ class AllocationsRelationManager extends RelationManager
->label(trans('admin/server.ports'))
->inlineLabel()
->live()
->disabled(fn (Get $get) => empty($get('allocation_ip')))
->afterStateUpdated(fn ($state, Set $set, Get $get) => $set('allocation_ports', CreateServer::retrieveValidPorts($this->getOwnerRecord()->node, $state, $get('allocation_ip'))))
->afterStateUpdated(fn ($state, Set $set, Get $get) => $set('allocation_ports',
CreateServer::retrieveValidPorts($this->getOwnerRecord()->node, $state, $get('allocation_ip')))
)
->splitKeys(['Tab', ' ', ','])
->required(),
])

View File

@@ -45,7 +45,7 @@ class UserResource extends Resource
public static function getNavigationGroup(): ?string
{
return config('panel.filament.top-navigation', false) ? null : trans('admin/dashboard.user');
return trans('admin/dashboard.user');
}
public static function getNavigationBadge(): ?string

View File

@@ -6,22 +6,15 @@ use App\Enums\ServerResourceType;
use App\Filament\App\Resources\ServerResource;
use App\Filament\Components\Tables\Columns\ServerEntryColumn;
use App\Filament\Server\Pages\Console;
use App\Models\Permission;
use App\Models\Server;
use App\Repositories\Daemon\DaemonPowerRepository;
use AymanAlhattami\FilamentContextMenu\Columns\ContextMenuTextColumn;
use Filament\Notifications\Notification;
use Filament\Resources\Components\Tab;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\Action;
use Filament\Tables\Columns\ColumnGroup;
use Filament\Tables\Columns\Layout\Stack;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Client\ConnectionException;
use Livewire\Attributes\On;
class ListServers extends ListRecords
{
@@ -31,51 +24,12 @@ class ListServers extends ListRecords
public const WARNING_THRESHOLD = 0.7;
private DaemonPowerRepository $daemonPowerRepository;
public function boot(): void
{
$this->daemonPowerRepository = new DaemonPowerRepository();
}
public function table(Table $table): Table
{
$baseQuery = auth()->user()->accessibleServers();
$menuOptions = function (Server $server) {
$status = $server->retrieveStatus();
return [
Action::make('start')
->color('primary')
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_START, $server))
->visible(fn () => $status->isStartable())
->dispatch('powerAction', ['server' => $server, 'action' => 'start'])
->icon('tabler-player-play-filled'),
Action::make('restart')
->color('gray')
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_RESTART, $server))
->visible(fn () => $status->isRestartable())
->dispatch('powerAction', ['server' => $server, 'action' => 'restart'])
->icon('tabler-refresh'),
Action::make('stop')
->color('danger')
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
->visible(fn () => $status->isStoppable())
->dispatch('powerAction', ['server' => $server, 'action' => 'stop'])
->icon('tabler-player-stop-filled'),
Action::make('kill')
->color('danger')
->tooltip('This can result in data corruption and/or data loss!')
->dispatch('powerAction', ['server' => $server, 'action' => 'kill'])
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
->visible(fn () => $status->isKillable())
->icon('tabler-alert-square'),
];
};
$viewOne = [
ContextMenuTextColumn::make('condition')
TextColumn::make('condition')
->label('')
->default('unknown')
->wrap()
@@ -83,24 +37,20 @@ class ListServers extends ListRecords
->alignCenter()
->tooltip(fn (Server $server) => $server->formatResource('uptime', type: ServerResourceType::Time))
->icon(fn (Server $server) => $server->condition->getIcon())
->color(fn (Server $server) => $server->condition->getColor())
->contextMenuActions($menuOptions)
->enableContextMenu(fn (Server $server) => !$server->isInConflictState()),
->color(fn (Server $server) => $server->condition->getColor()),
];
$viewTwo = [
ContextMenuTextColumn::make('name')
TextColumn::make('name')
->label('')
->size('md')
->searchable()
->contextMenuActions($menuOptions)
->enableContextMenu(fn (Server $server) => !$server->isInConflictState()),
ContextMenuTextColumn::make('allocation.address')
->searchable(),
TextColumn::make('')
->label('')
->badge()
->copyable(request()->isSecure())
->contextMenuActions($menuOptions)
->enableContextMenu(fn (Server $server) => !$server->isInConflictState()),
->copyMessage(fn (Server $server, string $state) => 'Copied ' . $server->allocation->address)
->state(fn (Server $server) => $server->allocation->address),
];
$viewThree = [
@@ -240,25 +190,4 @@ class ListServers extends ListRecords
return null;
}
#[On('powerAction')]
public function powerAction(Server $server, string $action): void
{
try {
$this->daemonPowerRepository->setServer($server)->send($action);
Notification::make()
->title('Power Action')
->body($action . ' sent to ' . $server->name)
->success()
->send();
$this->redirect(self::getUrl(['activeTab' => $this->activeTab]));
} catch (ConnectionException) {
Notification::make()
->title(trans('exceptions.node.error_connecting', ['node' => $server->node->name]))
->danger()
->send();
}
}
}

View File

@@ -40,7 +40,6 @@ use Filament\Support\Exceptions\Halt;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\HtmlString;
use Illuminate\Validation\Rules\Password;
use Laravel\Socialite\Facades\Socialite;
@@ -129,20 +128,10 @@ class EditProfile extends BaseEditProfile
->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages())
->native(false),
FileUpload::make('avatar')
->visible(fn () => config('panel.filament.uploadable-avatars'))
->visible(fn () => config('panel.filament.avatar-provider') === 'local')
->avatar()
->acceptedFileTypes(['image/png'])
->directory('avatars')
->getUploadedFileNameForStorageUsing(fn () => $this->getUser()->id . '.png')
->hintAction(function (FileUpload $fileUpload) {
$path = $fileUpload->getDirectory() . '/' . $this->getUser()->id . '.png';
return Action::make('remove_avatar')
->icon('tabler-photo-minus')
->iconButton()
->hidden(fn () => !$fileUpload->getDisk()->exists($path))
->action(fn () => $fileUpload->getDisk()->delete($path));
}),
->getUploadedFileNameForStorageUsing(fn () => $this->getUser()->id . '.png'),
]),
Tab::make(trans('profile.tabs.oauth'))
@@ -366,8 +355,18 @@ class EditProfile extends BaseEditProfile
Section::make(trans('profile.console'))
->collapsible()
->icon('tabler-brand-tabler')
->columns(4)
->schema([
TextInput::make('console_rows')
->label(trans('profile.rows'))
->minValue(1)
->numeric()
->required()
->columnSpan(1)
->default(30),
// Select::make('console_font')
// ->label(trans('profile.font'))
// ->hidden() //TODO
// ->columnSpan(1),
TextInput::make('console_font_size')
->label(trans('profile.font_size'))
->columnSpan(1)
@@ -375,74 +374,6 @@ class EditProfile extends BaseEditProfile
->numeric()
->required()
->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();
}
foreach (Storage::disk('public')->allFiles('fonts') as $file) {
$fileInfo = pathinfo($file);
if ($fileInfo['extension'] === 'ttf') {
$fonts[$fileInfo['filename']] = $fileInfo['filename'];
}
}
return $fonts;
})
->reactive()
->default('monospace')
->afterStateUpdated(fn ($state, callable $set) => $set('font_preview', $state)),
Placeholder::make('font_preview')
->label(trans('profile.font_preview'))
->columnSpan(2)
->content(function (Get $get) {
$fontName = $get('console_font') ?? 'monospace';
$fontSize = $get('console_font_size') . 'px';
$fontUrl = asset("storage/fonts/{$fontName}.ttf");
return new HtmlString(<<<HTML
<style>
@font-face {
font-family: "CustomPreviewFont";
src: url("$fontUrl");
}
.preview-text {
font-family: "CustomPreviewFont";
font-size: $fontSize;
margin-top: 10px;
display: block;
}
</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')
->hintIconTooltip(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),
]),
]),
]),
@@ -505,14 +436,12 @@ class EditProfile extends BaseEditProfile
protected function mutateFormDataBeforeSave(array $data): array
{
$moarbetterdata = [
'console_font' => $data['console_font'],
'console_font_size' => $data['console_font_size'],
'console_rows' => $data['console_rows'],
'console_graph_period' => $data['console_graph_period'],
'dashboard_layout' => $data['dashboard_layout'],
];
unset($data['console_font'],$data['console_font_size'], $data['console_rows'], $data['dashboard_layout']);
unset($data['dashboard_layout'], $data['console_font_size'], $data['console_rows']);
$data['customization'] = json_encode($moarbetterdata);
return $data;
@@ -522,10 +451,8 @@ class EditProfile extends BaseEditProfile
{
$moarbetterdata = json_decode($data['customization'], true);
$data['console_font'] = $moarbetterdata['console_font'] ?? 'monospace';
$data['console_font_size'] = $moarbetterdata['console_font_size'] ?? 14;
$data['console_rows'] = $moarbetterdata['console_rows'] ?? 30;
$data['console_graph_period'] = $moarbetterdata['console_graph_period'] ?? 30;
$data['dashboard_layout'] = $moarbetterdata['dashboard_layout'] ?? 'grid';
return $data;

View File

@@ -5,18 +5,16 @@ namespace App\Filament\Server\Pages;
use App\Enums\ConsoleWidgetPosition;
use App\Enums\ContainerStatus;
use App\Exceptions\Http\Server\ServerStateConflictException;
use App\Extensions\Features\FeatureProvider;
use App\Filament\Server\Widgets\ServerConsole;
use App\Filament\Server\Widgets\ServerCpuChart;
use App\Filament\Server\Widgets\ServerMemoryChart;
use App\Filament\Server\Widgets\ServerNetworkChart;
// use App\Filament\Server\Widgets\ServerNetworkChart;
use App\Filament\Server\Widgets\ServerOverview;
use App\Livewire\AlertBanner;
use App\Models\Permission;
use App\Models\Server;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Facades\Filament;
use Filament\Actions\Action;
use Filament\Facades\Filament;
use Filament\Pages\Page;
use Filament\Support\Enums\ActionSize;
use Filament\Widgets\Widget;
@@ -25,8 +23,6 @@ use Livewire\Attributes\On;
class Console extends Page
{
use InteractsWithActions;
protected static ?string $navigationIcon = 'tabler-brand-tabler';
protected static ?int $navigationSort = 1;
@@ -51,30 +47,6 @@ class Console extends Page
}
}
public function boot(): void
{
/** @var Server $server */
$server = Filament::getTenant();
/** @var FeatureProvider $feature */
foreach ($server->egg->features() as $feature) {
$this->cacheAction($feature->getAction());
}
}
#[On('mount-feature')]
public function mountFeature(string $data): void
{
$data = json_decode($data);
$feature = data_get($data, 'key');
$feature = FeatureProvider::getProviders($feature);
if ($this->getMountedAction()) {
return;
}
$this->mountAction($feature->getId());
sleep(2); // TODO find a better way
}
public function getWidgetData(): array
{
return [
@@ -112,7 +84,7 @@ class Console extends Page
$allWidgets = array_merge($allWidgets, [
ServerCpuChart::class,
ServerMemoryChart::class,
ServerNetworkChart::class,
//ServerNetworkChart::class, TODO: convert units.
]);
$allWidgets = array_merge($allWidgets, static::$customWidgets[ConsoleWidgetPosition::Bottom->value] ?? []);
@@ -154,30 +126,33 @@ class Console extends Page
Action::make('start')
->color('primary')
->size(ActionSize::ExtraLarge)
->dispatch('setServerState', ['state' => 'start', 'uuid' => $server->uuid])
->action(fn () => $this->dispatch('setServerState', state: 'start', uuid: $server->uuid))
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_START, $server))
->disabled(fn () => $server->isInConflictState() || !$this->status->isStartable())
->icon('tabler-player-play-filled'),
Action::make('restart')
->color('gray')
->size(ActionSize::ExtraLarge)
->dispatch('setServerState', ['state' => 'restart', 'uuid' => $server->uuid])
->action(fn () => $this->dispatch('setServerState', state: 'restart', uuid: $server->uuid))
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_RESTART, $server))
->disabled(fn () => $server->isInConflictState() || !$this->status->isRestartable())
->icon('tabler-reload'),
Action::make('stop')
->color('danger')
->size(ActionSize::ExtraLarge)
->dispatch('setServerState', ['state' => 'stop', 'uuid' => $server->uuid])
->action(fn () => $this->dispatch('setServerState', state: 'stop', uuid: $server->uuid))
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
->hidden(fn () => $this->status->isStartingOrStopping() || $this->status->isKillable())
->disabled(fn () => $server->isInConflictState() || !$this->status->isStoppable())
->icon('tabler-player-stop-filled'),
Action::make('kill')
->color('danger')
->tooltip('This can result in data corruption and/or data loss!')
->requiresConfirmation()
->modalHeading('Do you wish to kill this server?')
->modalDescription('This can result in data corruption and/or data loss!')
->modalSubmitActionLabel('Kill Server')
->size(ActionSize::ExtraLarge)
->dispatch('setServerState', ['state' => 'kill', 'uuid' => $server->uuid])
->action(fn () => $this->dispatch('setServerState', state: 'kill', uuid: $server->uuid))
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
->hidden(fn () => $server->isInConflictState() || !$this->status->isKillable())
->icon('tabler-alert-square'),

View File

@@ -18,7 +18,6 @@ use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Validator;
class Startup extends ServerFormPage
@@ -101,7 +100,7 @@ class Startup extends ServerFormPage
->schema([
Repeater::make('server_variables')
->label('')
->relationship('serverVariables', fn (Builder $query) => $query->where('egg_variables.user_viewable', true)->orderByPowerJoins('variable.sort'))
->relationship('viewableServerVariables')
->grid()
->disabled(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server))
->reorderable(false)->addable(false)->deletable(false)

View File

@@ -30,7 +30,8 @@ class ActivityResource extends Resource
/** @var Server $server */
$server = Filament::getTenant();
return ActivityLog::whereHas('subjects', fn (Builder $query) => $query->where('subject_id', $server->id))
return $server->activity()
->getQuery()
->whereNotIn('activity_logs.event', ActivityLog::DISABLED_EVENTS)
->when(config('activity.hide_admin_activity'), function (Builder $builder) use ($server) {
// We could do this with a query and a lot of joins, but that gets pretty

View File

@@ -19,7 +19,6 @@ use Filament\Tables\Actions\ViewAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Support\Arr;
use Illuminate\Support\HtmlString;
class ListActivities extends ListRecords
@@ -99,7 +98,7 @@ class ListActivities extends ListRecords
DateTimePicker::make('timestamp'),
KeyValue::make('properties')
->label('Metadata')
->formatStateUsing(fn ($state) => Arr::dot($state)),
->formatStateUsing(fn ($state) => collect($state)->filter(fn ($item) => !is_array($item))->all()),
]),
])
->filters([

View File

@@ -2,7 +2,6 @@
namespace App\Filament\Server\Resources\BackupResource\Pages;
use App\Enums\BackupStatus;
use App\Enums\ServerState;
use App\Facades\Activity;
use App\Filament\Server\Resources\BackupResource;
@@ -71,14 +70,13 @@ class ListBackups extends ListRecords
->label('Created')
->since()
->sortable(),
TextColumn::make('status')
->label('Status')
->badge(),
IconColumn::make('is_successful')
->label('Successful')
->boolean(),
IconColumn::make('is_locked')
->visibleFrom('md')
->label('Lock Status')
->trueIcon('tabler-lock')
->falseIcon('tabler-lock-open'),
->icon(fn (Backup $backup) => !$backup->is_locked ? 'tabler-lock-open' : 'tabler-lock'),
])
->actions([
ActionGroup::make([
@@ -86,14 +84,12 @@ class ListBackups extends ListRecords
->icon(fn (Backup $backup) => !$backup->is_locked ? 'tabler-lock' : 'tabler-lock-open')
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DELETE, $server))
->label(fn (Backup $backup) => !$backup->is_locked ? 'Lock' : 'Unlock')
->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->toggleLock($request, $server, $backup))
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->toggleLock($request, $server, $backup)),
Action::make('download')
->color('primary')
->icon('tabler-download')
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DOWNLOAD, $server))
->url(fn (DownloadLinkService $downloadLinkService, Backup $backup, Request $request) => $downloadLinkService->handle($backup, $request->user()), true)
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
->url(fn (DownloadLinkService $downloadLinkService, Backup $backup, Request $request) => $downloadLinkService->handle($backup, $request->user()), true),
Action::make('restore')
->color('success')
->icon('tabler-folder-up')
@@ -142,14 +138,12 @@ class ListBackups extends ListRecords
return Notification::make()
->title('Restoring Backup')
->send();
})
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
}),
DeleteAction::make('delete')
->disabled(fn (Backup $backup) => $backup->is_locked)
->disabled(fn (Backup $backup): bool => $backup->is_locked)
->modalDescription(fn (Backup $backup) => 'Do you wish to delete, ' . $backup->name . '?')
->modalSubmitActionLabel('Delete Backup')
->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->delete($request, $server, $backup))
->visible(fn (Backup $backup) => $backup->status !== BackupStatus::InProgress),
->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->delete($request, $server, $backup)),
]),
]);
}
@@ -186,6 +180,7 @@ class ListBackups extends ListRecords
->body($backup->name . ' created.')
->success()
->send();
} catch (HttpException $e) {
return Notification::make()
->danger()

View File

@@ -2,6 +2,7 @@
namespace App\Filament\Server\Resources\DatabaseResource\Pages;
use App\Facades\Activity;
use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction;
use App\Filament\Components\Tables\Columns\DateTimeColumn;
use App\Filament\Server\Resources\DatabaseResource;
@@ -81,7 +82,12 @@ class ListDatabases extends ListRecords
ViewAction::make()
->modalHeading(fn (Database $database) => 'Viewing ' . $database->database),
DeleteAction::make()
->using(fn (Database $database, DatabaseManagementService $service) => $service->delete($database)),
->after(function (Database $database) {
Activity::event('server:database.delete')
->subject($database)
->property('name', $database->database)
->log();
}),
]);
}

View File

@@ -26,8 +26,6 @@ use Filament\Resources\Pages\Page;
use Filament\Resources\Pages\PageRegistration;
use Filament\Support\Enums\Alignment;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Routing\Route;
use Illuminate\Support\Facades\Route as RouteFacade;
use Livewire\Attributes\Locked;
@@ -130,33 +128,31 @@ class EditFiles extends Page
return $this->getDaemonFileRepository()->getContent($this->path, config('panel.files.max_edit_size'));
} catch (FileSizeTooLargeException) {
AlertBanner::make()
->title('<code>' . basename($this->path) . '</code> is too large!')
->body('Max is ' . convert_bytes_to_readable(config('panel.files.max_edit_size')))
->title('File too large!')
->body('<code>' . $this->path . '</code> Max is ' . convert_bytes_to_readable(config('panel.files.max_edit_size')))
->danger()
->closable()
->send();
$this->redirect(ListFiles::getUrl(['path' => dirname($this->path)]));
$this->redirect(ListFiles::getUrl());
} catch (FileNotFoundException) {
AlertBanner::make()
->title('<code>' . basename($this->path) . '</code> not found!')
->title('File Not found!')
->body('<code>' . $this->path . '</code>')
->danger()
->closable()
->send();
$this->redirect(ListFiles::getUrl(['path' => dirname($this->path)]));
$this->redirect(ListFiles::getUrl());
} catch (FileNotEditableException) {
AlertBanner::make()
->title('<code>' . basename($this->path) . '</code> is a directory')
->title('Could not edit directory!')
->body('<code>' . $this->path . '</code>')
->danger()
->closable()
->send();
$this->redirect(ListFiles::getUrl(['path' => dirname($this->path)]));
} catch (ConnectionException) {
// Alert banner for this one will be handled by ListFiles
$this->redirect(ListFiles::getUrl(['path' => dirname($this->path)]));
$this->redirect(ListFiles::getUrl());
}
})
->language(fn (Get $get) => $get('lang'))
@@ -180,15 +176,6 @@ class EditFiles extends Page
->info()
->closable()
->send();
try {
$this->getDaemonFileRepository()->getDirectory('/');
} catch (ConnectionException) {
AlertBanner::make('node_connection_error')
->title('Could not connect to the node!')
->danger()
->send();
}
}
}
@@ -243,14 +230,6 @@ class EditFiles extends Page
return $this->fileRepository;
}
/**
* @param array<string, mixed> $parameters
*/
public static function getUrl(array $parameters = [], bool $isAbsolute = true, ?string $panel = null, ?Model $tenant = null): string
{
return parent::getUrl($parameters, $isAbsolute, $panel, $tenant) . '/';
}
public static function route(string $path): PageRegistration
{
return new PageRegistration(

View File

@@ -12,6 +12,7 @@ use App\Models\Server;
use App\Repositories\Daemon\DaemonFileRepository;
use App\Filament\Components\Tables\Columns\BytesColumn;
use App\Filament\Components\Tables\Columns\DateTimeColumn;
use App\Livewire\AlertBanner;
use Filament\Actions\Action as HeaderAction;
use Filament\Facades\Filament;
use Filament\Forms\Components\CheckboxList;
@@ -29,12 +30,14 @@ use Filament\Resources\Pages\PageRegistration;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\ActionGroup;
use Filament\Tables\Actions\BulkAction;
use Filament\Tables\Actions\BulkActionGroup;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\DeleteBulkAction;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\UploadedFile;
use Illuminate\Routing\Route;
use Illuminate\Support\Carbon;
@@ -46,10 +49,30 @@ class ListFiles extends ListRecords
protected static string $resource = FileResource::class;
#[Locked]
public string $path = '/';
public string $path;
private DaemonFileRepository $fileRepository;
private bool $isDisabled = false;
public function mount(?string $path = null): void
{
parent::mount();
$this->path = $path ?? '/';
try {
$this->getDaemonFileRepository()->getDirectory('/');
} catch (ConnectionException) {
$this->isDisabled = true;
AlertBanner::make('node_connection_error')
->title('Could not connect to the node!')
->danger()
->send();
}
}
public function getBreadcrumbs(): array
{
$resource = static::getResource();
@@ -107,18 +130,21 @@ class ListFiles extends ListRecords
->actions([
Action::make('view')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server))
->disabled($this->isDisabled)
->label('Open')
->icon('tabler-eye')
->visible(fn (File $file) => $file->is_directory)
->url(fn (File $file) => self::getUrl(['path' => join_paths($this->path, $file->name)])),
EditAction::make('edit')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server))
->disabled($this->isDisabled)
->icon('tabler-edit')
->visible(fn (File $file) => $file->canEdit())
->url(fn (File $file) => EditFiles::getUrl(['path' => join_paths($this->path, $file->name)])),
ActionGroup::make([
Action::make('rename')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->disabled($this->isDisabled)
->label('Rename')
->icon('tabler-forms')
->form([
@@ -147,6 +173,7 @@ class ListFiles extends ListRecords
}),
Action::make('copy')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
->disabled($this->isDisabled)
->label('Copy')
->icon('tabler-copy')
->visible(fn (File $file) => $file->is_file)
@@ -166,12 +193,14 @@ class ListFiles extends ListRecords
}),
Action::make('download')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server))
->disabled($this->isDisabled)
->label('Download')
->icon('tabler-download')
->visible(fn (File $file) => $file->is_file)
->url(fn (File $file) => DownloadFiles::getUrl(['path' => join_paths($this->path, $file->name)]), true),
Action::make('move')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->disabled($this->isDisabled)
->label('Move')
->icon('tabler-replace')
->form([
@@ -207,6 +236,7 @@ class ListFiles extends ListRecords
}),
Action::make('permissions')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->disabled($this->isDisabled)
->label('Permissions')
->icon('tabler-license')
->form([
@@ -263,6 +293,7 @@ class ListFiles extends ListRecords
}),
Action::make('archive')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
->disabled($this->isDisabled)
->label('Archive')
->icon('tabler-archive')
->form([
@@ -290,6 +321,7 @@ class ListFiles extends ListRecords
}),
Action::make('unarchive')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
->disabled($this->isDisabled)
->label('Unarchive')
->icon('tabler-archive')
->visible(fn (File $file) => $file->isArchive())
@@ -311,6 +343,7 @@ class ListFiles extends ListRecords
]),
DeleteAction::make()
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server))
->disabled($this->isDisabled)
->label('')
->icon('tabler-trash')
->requiresConfirmation()
@@ -325,77 +358,83 @@ class ListFiles extends ListRecords
->log();
}),
])
->groupedBulkActions([
BulkAction::make('move')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->form([
TextInput::make('location')
->label('Directory')
->hint('Enter the new directory, relative to the current directory.')
->required()
->live(),
Placeholder::make('new_location')
->content(fn (Get $get) => resolve_path('./' . join_paths($this->path, $get('location') ?? ''))),
])
->action(function (Collection $files, $data) {
$location = rtrim($data['location'], '/');
->bulkActions([
BulkActionGroup::make([
BulkAction::make('move')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->disabled($this->isDisabled)
->form([
TextInput::make('location')
->label('Directory')
->hint('Enter the new directory, relative to the current directory.')
->required()
->live(),
Placeholder::make('new_location')
->content(fn (Get $get) => resolve_path('./' . join_paths($this->path, $get('location') ?? ''))),
])
->action(function (Collection $files, $data) {
$location = rtrim($data['location'], '/');
$files = $files->map(fn ($file) => ['to' => join_paths($location, $file['name']), 'from' => $file['name']])->toArray();
$this->getDaemonFileRepository()->renameFiles($this->path, $files);
$files = $files->map(fn ($file) => ['to' => join_paths($location, $file['name']), 'from' => $file['name']])->toArray();
$this->getDaemonFileRepository()
->renameFiles($this->path, $files);
Activity::event('server:file.rename')
->property('directory', $this->path)
->property('files', $files)
->log();
Activity::event('server:file.rename')
->property('directory', $this->path)
->property('files', $files)
->log();
Notification::make()
->title(count($files) . ' Files were moved to ' . resolve_path(join_paths($this->path, $location)))
->success()
->send();
}),
BulkAction::make('archive')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
->form([
TextInput::make('name')
->label('Archive name')
->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z')
->suffix('.tar.gz'),
])
->action(function ($data, Collection $files) {
$files = $files->map(fn ($file) => $file['name'])->toArray();
Notification::make()
->title(count($files) . ' Files were moved to ' . resolve_path(join_paths($this->path, $location)))
->success()
->send();
}),
BulkAction::make('archive')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
->disabled($this->isDisabled)
->form([
TextInput::make('name')
->label('Archive name')
->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z')
->suffix('.tar.gz'),
])
->action(function ($data, Collection $files) {
$files = $files->map(fn ($file) => $file['name'])->toArray();
$archive = $this->getDaemonFileRepository()->compressFiles($this->path, $files, $data['name']);
$archive = $this->getDaemonFileRepository()->compressFiles($this->path, $files, $data['name']);
Activity::event('server:file.compress')
->property('name', $archive['name'])
->property('directory', $this->path)
->property('files', $files)
->log();
Activity::event('server:file.compress')
->property('name', $archive['name'])
->property('directory', $this->path)
->property('files', $files)
->log();
Notification::make()
->title('Archive created')
->body($archive['name'])
->success()
->send();
Notification::make()
->title('Archive created')
->body($archive['name'])
->success()
->send();
return redirect(ListFiles::getUrl(['path' => $this->path]));
}),
DeleteBulkAction::make()
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server))
->action(function (Collection $files) {
$files = $files->map(fn ($file) => $file['name'])->toArray();
$this->getDaemonFileRepository()->deleteFiles($this->path, $files);
return redirect(ListFiles::getUrl(['path' => $this->path]));
}),
DeleteBulkAction::make()
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server))
->disabled($this->isDisabled)
->action(function (Collection $files) {
$files = $files->map(fn ($file) => $file['name'])->toArray();
$this->getDaemonFileRepository()->deleteFiles($this->path, $files);
Activity::event('server:file.delete')
->property('directory', $this->path)
->property('files', $files)
->log();
Activity::event('server:file.delete')
->property('directory', $this->path)
->property('files', $files)
->log();
Notification::make()
->title(count($files) . ' Files deleted.')
->success()
->send();
}),
Notification::make()
->title(count($files) . ' Files deleted.')
->success()
->send();
}),
]),
]);
}
@@ -407,6 +446,7 @@ class ListFiles extends ListRecords
return [
HeaderAction::make('new_file')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
->disabled($this->isDisabled)
->label('New File')
->color('gray')
->keyBindings('')
@@ -438,6 +478,7 @@ class ListFiles extends ListRecords
]),
HeaderAction::make('new_folder')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
->disabled($this->isDisabled)
->label('New Folder')
->color('gray')
->action(function ($data) {
@@ -454,6 +495,7 @@ class ListFiles extends ListRecords
]),
HeaderAction::make('upload')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
->disabled($this->isDisabled)
->label('Upload')
->action(function ($data) {
if (count($data['files']) > 0 && !isset($data['url'])) {
@@ -503,14 +545,14 @@ class ListFiles extends ListRecords
]),
HeaderAction::make('search')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server))
->disabled($this->isDisabled)
->label('Global Search')
->modalSubmitActionLabel('Search')
->form([
TextInput::make('searchTerm')
->placeholder('Enter a search term, e.g. *.txt')
->required()
->regex('/^[^*]*\*?[^*]*$/')
->minValue(3),
->minLength(3),
])
->action(fn ($data) => redirect(SearchFiles::getUrl([
'searchTerm' => $data['searchTerm'],

View File

@@ -12,7 +12,6 @@ use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Url;
class SearchFiles extends ListRecords
{
@@ -23,8 +22,15 @@ class SearchFiles extends ListRecords
#[Locked]
public string $searchTerm;
#[Url]
public string $path = '/';
#[Locked]
public string $path;
public function mount(?string $searchTerm = null, ?string $path = null): void
{
parent::mount();
$this->searchTerm = $searchTerm;
$this->path = $path ?? '/';
}
public function getBreadcrumbs(): array
{

View File

@@ -19,8 +19,8 @@ use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Set;
use Filament\Notifications\Notification;
use Filament\Resources\Resource;
use Filament\Tables\Actions\DeleteAction;
use Filament\Resources\Resource;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\TextColumn;
@@ -83,35 +83,6 @@ class UserResource extends Resource
/** @var Server $server */
$server = Filament::getTenant();
$tabs = [];
$permissionsArray = [];
foreach (Permission::permissionData() as $data) {
$options = [];
$descriptions = [];
foreach ($data['permissions'] as $permission) {
$options[$permission] = str($permission)->headline();
$descriptions[$permission] = trans('server/users.permissions.' . $data['name'] . '_' . str($permission)->replace('-', '_'));
$permissionsArray[$data['name']][] = $permission;
}
$tabs[] = Tab::make(str($data['name'])->headline())
->schema([
Section::make()
->description(trans('server/users.permissions.' . $data['name'] . '_desc'))
->icon($data['icon'])
->schema([
CheckboxList::make($data['name'])
->label('')
->bulkToggleable()
->columns(2)
->options($options)
->descriptions($descriptions),
]),
]);
}
return $table
->paginated(false)
->searchable(false)
@@ -187,8 +158,69 @@ class UserResource extends Resource
Actions::make([
Action::make('assignAll')
->label('Assign All')
->action(function (Set $set) use ($permissionsArray) {
$permissions = $permissionsArray;
->action(function (Set $set) {
$permissions = [
'control' => [
'console',
'start',
'stop',
'restart',
],
'user' => [
'read',
'create',
'update',
'delete',
],
'file' => [
'read',
'read-content',
'create',
'update',
'delete',
'archive',
'sftp',
],
'backup' => [
'read',
'create',
'delete',
'download',
'restore',
],
'allocation' => [
'read',
'create',
'update',
'delete',
],
'startup' => [
'read',
'update',
'docker-image',
],
'database' => [
'read',
'create',
'update',
'delete',
'view_password',
],
'schedule' => [
'read',
'create',
'update',
'delete',
],
'settings' => [
'rename',
'reinstall',
],
'activity' => [
'read',
],
];
foreach ($permissions as $key => $value) {
$allValues = array_unique($value);
$set($key, $allValues);
@@ -203,25 +235,264 @@ class UserResource extends Resource
]),
Tabs::make()
->columnSpanFull()
->schema($tabs),
->schema([
Tab::make('Console')
->schema([
Section::make()
->description(trans('server/users.permissions.control_desc'))
->icon('tabler-terminal-2')
->schema([
CheckboxList::make('control')
->formatStateUsing(function (User $user, Set $set) use ($server) {
$permissionsArray = $server->subusers->where('user_id', $user->id)->first()->permissions;
$transformedPermissions = [];
foreach ($permissionsArray as $permission) {
[$group, $action] = explode('.', $permission, 2);
$transformedPermissions[$group][] = $action;
}
foreach ($transformedPermissions as $key => $value) {
$set($key, $value);
}
return $transformedPermissions['control'] ?? [];
})
->bulkToggleable()
->label('')
->columns(2)
->options([
'console' => 'Console',
'start' => 'Start',
'stop' => 'Stop',
'restart' => 'Restart',
])
->descriptions([
'console' => trans('server/users.permissions.control_console'),
'start' => trans('server/users.permissions.control_start'),
'stop' => trans('server/users.permissions.control_stop'),
'restart' => trans('server/users.permissions.control_restart'),
]),
]),
]),
Tab::make('User')
->schema([
Section::make()
->description(trans('server/users.permissions.user_desc'))
->icon('tabler-users')
->schema([
CheckboxList::make('user')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'create' => 'Create',
'update' => 'Update',
'delete' => 'Delete',
])
->descriptions([
'create' => trans('server/users.permissions.user_create'),
'read' => trans('server/users.permissions.user_read'),
'update' => trans('server/users.permissions.user_update'),
'delete' => trans('server/users.permissions.user_delete'),
]),
]),
]),
Tab::make('File')
->schema([
Section::make()
->description(trans('server/users.permissions.file_desc'))
->icon('tabler-folders')
->schema([
CheckboxList::make('file')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'read-content' => 'Read Content',
'create' => 'Create',
'update' => 'Update',
'delete' => 'Delete',
'archive' => 'Archive',
'sftp' => 'SFTP',
])
->descriptions([
'create' => trans('server/users.permissions.file_create'),
'read' => trans('server/users.permissions.file_read'),
'read-content' => trans('server/users.permissions.file_read_content'),
'update' => trans('server/users.permissions.file_update'),
'delete' => trans('server/users.permissions.file_delete'),
'archive' => trans('server/users.permissions.file_archive'),
'sftp' => trans('server/users.permissions.file_sftp'),
]),
]),
]),
Tab::make('Backup')
->schema([
Section::make()
->description(trans('server/users.permissions.backup_desc'))
->icon('tabler-download')
->schema([
CheckboxList::make('backup')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'create' => 'Create',
'delete' => 'Delete',
'download' => 'Download',
'restore' => 'Restore',
])
->descriptions([
'create' => trans('server/users.permissions.backup_create'),
'read' => trans('server/users.permissions.backup_read'),
'delete' => trans('server/users.permissions.backup_delete'),
'download' => trans('server/users.permissions.backup_download'),
'restore' => trans('server/users.permissions.backup_restore'),
]),
]),
]),
Tab::make('Allocation')
->schema([
Section::make()
->description(trans('server/users.permissions.allocation_desc'))
->icon('tabler-network')
->schema([
CheckboxList::make('allocation')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'create' => 'Create',
'update' => 'Update',
'delete' => 'Delete',
])
->descriptions([
'read' => trans('server/users.permissions.allocation_read'),
'create' => trans('server/users.permissions.allocation_create'),
'update' => trans('server/users.permissions.allocation_update'),
'delete' => trans('server/users.permissions.allocation_delete'),
]),
]),
]),
Tab::make('Startup')
->schema([
Section::make()
->description(trans('server/users.permissions.startup_desc'))
->icon('tabler-question-mark')
->schema([
CheckboxList::make('startup')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'update' => 'Update',
'docker-image' => 'Docker Image',
])
->descriptions([
'read' => trans('server/users.permissions.startup_read'),
'update' => trans('server/users.permissions.startup_update'),
'docker-image' => trans('server/users.permissions.startup_docker_image'),
]),
]),
]),
Tab::make('Database')
->schema([
Section::make()
->description(trans('server/users.permissions.database_desc'))
->icon('tabler-database')
->schema([
CheckboxList::make('database')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'create' => 'Create',
'update' => 'Update',
'delete' => 'Delete',
'view_password' => 'View Password',
])
->descriptions([
'read' => trans('server/users.permissions.database_read'),
'create' => trans('server/users.permissions.database_create'),
'update' => trans('server/users.permissions.database_update'),
'delete' => trans('server/users.permissions.database_delete'),
'view_password' => trans('server/users.permissions.database_view_password'),
]),
]),
]),
Tab::make('Schedule')
->schema([
Section::make()
->description(trans('server/users.permissions.schedule_desc'))
->icon('tabler-clock')
->schema([
CheckboxList::make('schedule')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'create' => 'Create',
'update' => 'Update',
'delete' => 'Delete',
])
->descriptions([
'read' => trans('server/users.permissions.schedule_read'),
'create' => trans('server/users.permissions.schedule_create'),
'update' => trans('server/users.permissions.schedule_update'),
'delete' => trans('server/users.permissions.schedule_delete'),
]),
]),
]),
Tab::make('Settings')
->schema([
Section::make()
->description(trans('server/users.permissions.settings_desc'))
->icon('tabler-settings')
->schema([
CheckboxList::make('settings')
->bulkToggleable()
->label('')
->columns(2)
->options([
'rename' => 'Rename',
'reinstall' => 'Reinstall',
])
->descriptions([
'rename' => trans('server/users.permissions.setting_rename'),
'reinstall' => trans('server/users.permissions.setting_reinstall'),
]),
]),
]),
Tab::make('Activity')
->schema([
Section::make()
->description(trans('server/users.permissions.activity_desc'))
->icon('tabler-stack')
->schema([
CheckboxList::make('activity')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
])
->descriptions([
'read' => trans('server/users.permissions.activity_read'),
]),
]),
]),
]),
]),
])
->mutateRecordDataUsing(function ($data, User $user) use ($server) {
$permissionsArray = $server->subusers->where('user_id', $user->id)->first()->permissions;
$transformedPermissions = [];
foreach ($permissionsArray as $permission) {
[$group, $action] = explode('.', $permission, 2);
$transformedPermissions[$group][] = $action;
}
foreach ($transformedPermissions as $key => $value) {
$data[$key] = $value;
}
return $data;
}),
]),
]);
}

View File

@@ -10,8 +10,8 @@ use App\Services\Subusers\SubuserCreationService;
use Exception;
use Filament\Actions;
use Filament\Facades\Filament;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Actions as assignAll;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Section;
@@ -32,35 +32,6 @@ class ListUsers extends ListRecords
/** @var Server $server */
$server = Filament::getTenant();
$tabs = [];
$permissionsArray = [];
foreach (Permission::permissionData() as $data) {
$options = [];
$descriptions = [];
foreach ($data['permissions'] as $permission) {
$options[$permission] = str($permission)->headline();
$descriptions[$permission] = trans('server/users.permissions.' . $data['name'] . '_' . str($permission)->replace('-', '_'));
$permissionsArray[$data['name']][] = $permission;
}
$tabs[] = Tab::make(str($data['name'])->headline())
->schema([
Section::make()
->description(trans('server/users.permissions.' . $data['name'] . '_desc'))
->icon($data['icon'])
->schema([
CheckboxList::make($data['name'])
->label('')
->bulkToggleable()
->columns(2)
->options($options)
->descriptions($descriptions),
]),
]);
}
return [
Actions\CreateAction::make('invite')
->label('Invite User')
@@ -89,10 +60,72 @@ class ListUsers extends ListRecords
assignAll::make([
Action::make('assignAll')
->label('Assign All')
->action(function (Set $set, Get $get) use ($permissionsArray) {
$permissions = $permissionsArray;
->action(function (Set $set, Get $get) {
$permissions = [
'control' => [
'console',
'start',
'stop',
'restart',
],
'user' => [
'read',
'create',
'update',
'delete',
],
'file' => [
'read',
'read-content',
'create',
'update',
'delete',
'archive',
'sftp',
],
'backup' => [
'read',
'create',
'delete',
'download',
'restore',
],
'allocation' => [
'read',
'create',
'update',
'delete',
],
'startup' => [
'read',
'update',
'docker-image',
],
'database' => [
'read',
'create',
'update',
'delete',
'view_password',
],
'schedule' => [
'read',
'create',
'update',
'delete',
],
'settings' => [
'rename',
'reinstall',
],
'activity' => [
'read',
],
];
foreach ($permissions as $key => $value) {
$allValues = array_unique($value);
$currentValues = $get($key) ?? [];
$allValues = array_unique(array_merge($currentValues, $value));
$set($key, $allValues);
}
}),
@@ -105,7 +138,247 @@ class ListUsers extends ListRecords
]),
Tabs::make()
->columnSpanFull()
->schema($tabs),
->schema([
Tab::make('Console')
->schema([
Section::make()
->description(trans('server/users.permissions.control_desc'))
->icon('tabler-terminal-2')
->schema([
CheckboxList::make('control')
->bulkToggleable()
->label('')
->columns(2)
->options([
'console' => 'Console',
'start' => 'Start',
'stop' => 'Stop',
'restart' => 'Restart',
])
->descriptions([
'console' => trans('server/users.permissions.control_console'),
'start' => trans('server/users.permissions.control_start'),
'stop' => trans('server/users.permissions.control_stop'),
'restart' => trans('server/users.permissions.control_restart'),
]),
]),
]),
Tab::make('User')
->schema([
Section::make()
->description(trans('server/users.permissions.user_desc'))
->icon('tabler-users')
->schema([
CheckboxList::make('user')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'create' => 'Create',
'update' => 'Update',
'delete' => 'Delete',
])
->descriptions([
'create' => trans('server/users.permissions.user_create'),
'read' => trans('server/users.permissions.user_read'),
'update' => trans('server/users.permissions.user_update'),
'delete' => trans('server/users.permissions.user_delete'),
]),
]),
]),
Tab::make('File')
->schema([
Section::make()
->description(trans('server/users.permissions.file_desc'))
->icon('tabler-folders')
->schema([
CheckboxList::make('file')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'read-content' => 'Read Content',
'create' => 'Create',
'update' => 'Update',
'delete' => 'Delete',
'archive' => 'Archive',
'sftp' => 'SFTP',
])
->descriptions([
'create' => trans('server/users.permissions.file_create'),
'read' => trans('server/users.permissions.file_read'),
'read-content' => trans('server/users.permissions.file_read_content'),
'update' => trans('server/users.permissions.file_update'),
'delete' => trans('server/users.permissions.file_delete'),
'archive' => trans('server/users.permissions.file_archive'),
'sftp' => trans('server/users.permissions.file_sftp'),
]),
]),
]),
Tab::make('Backup')
->schema([
Section::make()
->description(trans('server/users.permissions.backup_desc'))
->icon('tabler-download')
->schema([
CheckboxList::make('backup')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'create' => 'Create',
'delete' => 'Delete',
'download' => 'Download',
'restore' => 'Restore',
])
->descriptions([
'create' => trans('server/users.permissions.backup_create'),
'read' => trans('server/users.permissions.backup_read'),
'delete' => trans('server/users.permissions.backup_delete'),
'download' => trans('server/users.permissions.backup_download'),
'restore' => trans('server/users.permissions.backup_restore'),
]),
]),
]),
Tab::make('Allocation')
->schema([
Section::make()
->description(trans('server/users.permissions.allocation_desc'))
->icon('tabler-network')
->schema([
CheckboxList::make('allocation')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'create' => 'Create',
'update' => 'Update',
'delete' => 'Delete',
])
->descriptions([
'read' => trans('server/users.permissions.allocation_read'),
'create' => trans('server/users.permissions.allocation_create'),
'update' => trans('server/users.permissions.allocation_update'),
'delete' => trans('server/users.permissions.allocation_delete'),
]),
]),
]),
Tab::make('Startup')
->schema([
Section::make()
->description(trans('server/users.permissions.startup_desc'))
->icon('tabler-question-mark')
->schema([
CheckboxList::make('startup')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'update' => 'Update',
'docker-image' => 'Docker Image',
])
->descriptions([
'read' => trans('server/users.permissions.startup_read'),
'update' => trans('server/users.permissions.startup_update'),
'docker-image' => trans('server/users.permissions.startup_docker_image'),
]),
]),
]),
Tab::make('Database')
->schema([
Section::make()
->description(trans('server/users.permissions.database_desc'))
->icon('tabler-database')
->schema([
CheckboxList::make('database')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'create' => 'Create',
'update' => 'Update',
'delete' => 'Delete',
'view_password' => 'View Password',
])
->descriptions([
'read' => trans('server/users.permissions.database_read'),
'create' => trans('server/users.permissions.database_create'),
'update' => trans('server/users.permissions.database_update'),
'delete' => trans('server/users.permissions.database_delete'),
'view_password' => trans('server/users.permissions.database_view_password'),
]),
]),
]),
Tab::make('Schedule')
->schema([
Section::make()
->description(trans('server/users.permissions.schedule_desc'))
->icon('tabler-clock')
->schema([
CheckboxList::make('schedule')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'create' => 'Create',
'update' => 'Update',
'delete' => 'Delete',
])
->descriptions([
'read' => trans('server/users.permissions.schedule_read'),
'create' => trans('server/users.permissions.schedule_create'),
'update' => trans('server/users.permissions.schedule_update'),
'delete' => trans('server/users.permissions.schedule_delete'),
]),
]),
]),
Tab::make('Settings')
->schema([
Section::make()
->description(trans('server/users.permissions.settings_desc'))
->icon('tabler-settings')
->schema([
CheckboxList::make('settings')
->bulkToggleable()
->label('')
->columns(2)
->options([
'rename' => 'Rename',
'reinstall' => 'Reinstall',
])
->descriptions([
'rename' => trans('server/users.permissions.setting_rename'),
'reinstall' => trans('server/users.permissions.setting_reinstall'),
]),
]),
]),
Tab::make('Activity')
->schema([
Section::make()
->description(trans('server/users.permissions.activity_desc'))
->icon('tabler-stack')
->schema([
CheckboxList::make('activity')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
])
->descriptions([
'read' => trans('server/users.permissions.activity_read'),
]),
]),
]),
]),
]),
])
->modalHeading('Invite User')

View File

@@ -11,7 +11,6 @@ use App\Services\Nodes\NodeJWTService;
use App\Services\Servers\GetUserPermissionsService;
use Filament\Widgets\Widget;
use Illuminate\Support\Arr;
use Livewire\Attributes\Session;
use Livewire\Attributes\On;
class ServerConsole extends Widget
@@ -27,7 +26,6 @@ class ServerConsole extends Widget
public ?User $user = null;
/** @var string[] */
#[Session(key: 'server.{server.id}.history')]
public array $history = [];
public int $historyIndex = 0;

View File

@@ -4,7 +4,6 @@ namespace App\Filament\Server\Widgets;
use App\Models\Server;
use Carbon\Carbon;
use Filament\Facades\Filament;
use Filament\Support\RawJs;
use Filament\Widgets\ChartWidget;
use Illuminate\Support\Number;
@@ -17,19 +16,10 @@ class ServerCpuChart extends ChartWidget
public ?Server $server = null;
public static function canView(): bool
{
/** @var Server $server */
$server = Filament::getTenant();
return !$server->isInConflictState() && !$server->retrieveStatus()->isOffline();
}
protected function getData(): array
{
$period = auth()->user()->getCustomization()['console_graph_period'] ?? 30;
$cpu = collect(cache()->get("servers.{$this->server->id}.cpu_absolute"))
->slice(-$period)
->slice(-10)
->map(fn ($value, $key) => [
'cpu' => Number::format($value, maxPrecision: 2),
'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'),

View File

@@ -4,7 +4,6 @@ namespace App\Filament\Server\Widgets;
use App\Models\Server;
use Carbon\Carbon;
use Filament\Facades\Filament;
use Filament\Support\RawJs;
use Filament\Widgets\ChartWidget;
use Illuminate\Support\Number;
@@ -17,19 +16,9 @@ class ServerMemoryChart extends ChartWidget
public ?Server $server = null;
public static function canView(): bool
{
/** @var Server $server */
$server = Filament::getTenant();
return !$server->isInConflictState() && !$server->retrieveStatus()->isOffline();
}
protected function getData(): array
{
$period = auth()->user()->getCustomization()['console_graph_period'] ?? 30;
$memUsed = collect(cache()->get("servers.{$this->server->id}.memory_bytes"))
->slice(-$period)
$memUsed = collect(cache()->get("servers.{$this->server->id}.memory_bytes"))->slice(-10)
->map(fn ($value, $key) => [
'memory' => Number::format(config('panel.use_binary_prefix') ? $value / 1024 / 1024 / 1024 : $value / 1000 / 1000 / 1000, maxPrecision: 2),
'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'),

View File

@@ -4,72 +4,61 @@ namespace App\Filament\Server\Widgets;
use App\Models\Server;
use Carbon\Carbon;
use Filament\Facades\Filament;
use Filament\Support\RawJs;
use Filament\Widgets\ChartWidget;
class ServerNetworkChart extends ChartWidget
{
protected static ?string $heading = 'Network';
protected static ?string $pollingInterval = '1s';
protected static ?string $maxHeight = '200px';
protected static ?string $maxHeight = '300px';
public ?Server $server = null;
public static function canView(): bool
{
/** @var Server $server */
$server = Filament::getTenant();
return !$server->isInConflictState() && !$server->retrieveStatus()->isOffline();
}
protected function getData(): array
{
$previous = null;
$data = cache()->get("servers.{$this->server->id}.network");
$period = auth()->user()->getCustomization()['console_graph_period'] ?? 30;
$net = collect(cache()->get("servers.{$this->server->id}.network"))
->slice(-$period)
->map(function ($current, $timestamp) use (&$previous) {
$net = null;
$rx = collect($data)
->slice(-10)
->map(fn ($value, $key) => [
'rx' => $value->rx_bytes,
'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'),
])
->all();
if ($previous !== null) {
$net = [
'rx' => max(0, $current->rx_bytes - $previous->rx_bytes),
'tx' => max(0, $current->tx_bytes - $previous->tx_bytes),
'timestamp' => Carbon::createFromTimestamp($timestamp, auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
];
}
$previous = $current;
return $net;
})
$tx = collect($data)
->slice(-10)
->map(fn ($value, $key) => [
'tx' => $value->rx_bytes,
'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'),
])
->all();
return [
'datasets' => [
[
'label' => 'Inbound',
'data' => array_column($net, 'rx'),
'backgroundColor' => [
'rgba(100, 255, 105, 0.5)',
],
'tension' => '0.3',
'fill' => true,
],
[
'label' => 'Outbound',
'data' => array_column($net, 'tx'),
'data' => array_column($rx, 'rx'),
'backgroundColor' => [
'rgba(96, 165, 250, 0.3)',
],
'tension' => '0.3',
'fill' => true,
],
[
'label' => 'Outbound',
'data' => array_column($tx, 'tx'),
'backgroundColor' => [
'rgba(165, 96, 250, 0.3)',
],
'tension' => '0.3',
'fill' => true,
],
],
'labels' => array_column($net, 'timestamp'),
'labels' => array_column($rx, 'timestamp'),
];
}
@@ -80,38 +69,25 @@ class ServerNetworkChart extends ChartWidget
protected function getOptions(): RawJs
{
// TODO: use "panel.use_binary_prefix" config value
return RawJs::make(<<<'JS'
{
scales: {
x: {
display: false,
},
y: {
min: 0,
grid: {
display: false,
},
ticks: {
display: true,
},
display: false, //debug
},
y: {
ticks: {
display: true,
callback(value) {
const bytes = typeof value === 'string' ? parseInt(value, 10) : value;
if (bytes < 1) return '0 Bytes';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
const number = Number((bytes / Math.pow(1024, i)).toFixed(2));
return `${number} ${['Bytes', 'KiB', 'MiB', 'GiB', 'TiB'][i]}`;
},
},
},
}
}
JS);
}
public function getHeading(): string
{
$lastData = collect(cache()->get("servers.{$this->server->id}.network"))->last();
return 'Network - ↓' . convert_bytes_to_readable($lastData->rx_bytes ?? 0) . ' - ↑' . convert_bytes_to_readable($lastData->tx_bytes ?? 0);
}
}

View File

@@ -9,7 +9,6 @@ use App\Repositories\Daemon\DaemonPowerRepository;
use App\Http\Controllers\Api\Client\ClientApiController;
use App\Http\Requests\Api\Client\Servers\SendPowerRequest;
use Dedoc\Scramble\Attributes\Group;
use Illuminate\Http\Client\ConnectionException;
#[Group('Server', weight: 2)]
class PowerController extends ClientApiController
@@ -26,8 +25,6 @@ class PowerController extends ClientApiController
* Send power action
*
* Send a power action to a server.
*
* @throws ConnectionException
*/
public function index(SendPowerRequest $request, Server $server): Response
{

View File

@@ -36,22 +36,26 @@ class SettingsController extends ClientApiController
$name = $request->input('name');
$description = $request->has('description') ? (string) $request->input('description') : $server->description;
if ($server->name !== $name) {
Activity::event('server:settings.rename')
->property(['old' => $server->name, 'new' => $name])
->log();
$server->name = $name;
}
$server->name = $name;
if ($server->description !== $description && config('panel.editable_server_descriptions')) {
Activity::event('server:settings.description')
->property(['old' => $server->description, 'new' => $description])
->log();
if (config('panel.editable_server_descriptions')) {
$server->description = $description;
}
$server->save();
if ($server->name !== $name) {
Activity::event('server:settings.rename')
->property(['old' => $server->name, 'new' => $name])
->log();
}
if ($server->description !== $description) {
Activity::event('server:settings.description')
->property(['old' => $server->description, 'new' => $description])
->log();
}
return new JsonResponse([], Response::HTTP_NO_CONTENT);
}

View File

@@ -37,7 +37,7 @@ class StartupController extends ClientApiController
$startup = $this->startupCommandService->handle($server);
return $this->fractal->collection(
$server->variables()->where('user_viewable', true)->orderBy('sort')->get()
$server->variables()->orderBy('sort')->where('user_viewable', true)->get()
)
->transformWith($this->getTransformer(EggVariableTransformer::class))
->addMeta([

View File

@@ -14,6 +14,8 @@ class ActivityProcessingController extends Controller
{
public function __invoke(ActivityEventRequest $request): void
{
$tz = Carbon::now()->getTimezone();
/** @var \App\Models\Node $node */
$node = $request->attributes->get('node');
@@ -47,8 +49,11 @@ class ActivityProcessingController extends Controller
$log = [
'ip' => empty($datum['ip']) ? '127.0.0.1' : $datum['ip'],
'event' => $datum['event'],
'properties' => $datum['metadata'] ?? [],
'timestamp' => $when,
'properties' => json_encode($datum['metadata'] ?? []),
// We have to change the time to the current timezone due to the way Laravel is handling
// the date casting internally. If we just leave it in UTC it ends up getting double-cast
// and the time is way off.
'timestamp' => $when->setTimezone($tz),
];
if ($user = $users->get($datum['user'])) {

View File

@@ -44,20 +44,6 @@ class OAuthController extends Controller
return redirect()->route('auth.login');
}
// Check for errors (https://www.oauth.com/oauth2-servers/server-side-apps/possible-errors/)
if ($request->get('error')) {
report($request->get('error_description') ?? $request->get('error'));
Notification::make()
->title('Something went wrong')
->body($request->get('error'))
->danger()
->persistent()
->send();
return redirect()->route('auth.login');
}
$oauthUser = Socialite::driver($driver)->user();
// User is already logged in and wants to link a new OAuth Provider

View File

@@ -5,9 +5,6 @@ namespace App\Http\Middleware;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use App\Exceptions\Http\TwoFactorAuthRequiredException;
use App\Filament\Pages\Auth\EditProfile;
use App\Livewire\AlertBanner;
use App\Models\User;
class RequireTwoFactorAuthentication
{
@@ -17,6 +14,11 @@ class RequireTwoFactorAuthentication
public const LEVEL_ALL = 2;
/**
* The route to redirect a user to enable 2FA.
*/
protected string $redirectRoute = '/account';
/**
* Check the user state on the incoming request to determine if they should be allowed to
* proceed or not. This checks if the Panel is configured to require 2FA on an account in
@@ -27,37 +29,31 @@ class RequireTwoFactorAuthentication
*/
public function handle(Request $request, \Closure $next): mixed
{
/** @var ?User $user */
$user = $request->user();
$uri = rtrim($request->getRequestUri(), '/') . '/';
$current = $request->route()->getName();
if (!$user || Str::startsWith($uri, ['/auth/', '/profile']) || Str::startsWith($current, ['auth.', 'account.', 'filament.app.auth.'])) {
if (!$user || Str::startsWith($uri, ['/auth/']) || Str::startsWith($current, ['auth.', 'account.'])) {
return $next($request);
}
/** @var \App\Models\User $user */
$level = (int) config('panel.auth.2fa_required');
// If this setting is not configured, or the user is already using 2FA then we can just
// send them right through, nothing else needs to be checked.
//
// If the level is set as admin and the user is not an admin, pass them through as well.
if ($level === self::LEVEL_NONE || $user->use_totp) {
// If this setting is not configured, or the user is already using 2FA then we can just send them right through, nothing else needs to be checked.
return $next($request);
} elseif ($level === self::LEVEL_ADMIN && !$user->isAdmin()) {
// If the level is set as admin and the user is not an admin, pass them through as well.
} elseif ($level === self::LEVEL_ADMIN && !$user->isRootAdmin()) {
return $next($request);
}
// For API calls return an exception which gets rendered nicely in the API response...
// For API calls return an exception which gets rendered nicely in the API response.
if ($request->isJson() || Str::startsWith($uri, '/api/')) {
throw new TwoFactorAuthRequiredException();
}
// ... otherwise display banner and redirect to profile
AlertBanner::make('2fa_must_be_enabled')
->body(trans('auth.2fa_must_be_enabled'))
->warning()
->send();
return redirect(EditProfile::getUrl(['tab' => '-2fa-tab'], panel: 'app'));
return redirect()->to($this->redirectRoute);
}
}

View File

@@ -18,7 +18,26 @@ class StoreNodeRequest extends ApplicationApiRequest
*/
public function rules(?array $rules = null): array
{
return collect($rules ?? Node::getRules())->mapWithKeys(function ($value, $key) {
return collect($rules ?? Node::getRules())->only([
'public',
'name',
'description',
'fqdn',
'scheme',
'behind_proxy',
'maintenance_mode',
'memory',
'memory_overallocate',
'disk',
'disk_overallocate',
'cpu',
'cpu_overallocate',
'upload_size',
'daemon_listen',
'daemon_sftp',
'daemon_sftp_alias',
'daemon_base',
])->mapWithKeys(function ($value, $key) {
return [snake_case($key) => $value];
})->toArray();
}

View File

@@ -12,7 +12,7 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest
*/
public function rules(): array
{
$rules = $this->route() ? Server::getRulesForUpdate($this->parameter('server', Server::class)) : Server::getRules();
$rules = Server::getRulesForUpdate($this->parameter('server', Server::class));
return [
'allocation' => $rules['allocation_id'],
@@ -26,17 +26,13 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest
'limits.threads' => $this->requiredToOptional('threads', $rules['threads'], true),
'limits.disk' => $this->requiredToOptional('disk', $rules['disk'], true),
// Deprecated - use limits.memory
// Legacy rules to maintain backwards compatable API support without requiring
// a major version bump.
'memory' => $this->requiredToOptional('memory', $rules['memory']),
// Deprecated - use limits.swap
'swap' => $this->requiredToOptional('swap', $rules['swap']),
// Deprecated - use limits.io
'io' => $this->requiredToOptional('io', $rules['io']),
// Deprecated - use limits.cpu
'cpu' => $this->requiredToOptional('cpu', $rules['cpu']),
// Deprecated - use limits.threads
'threads' => $this->requiredToOptional('threads', $rules['threads']),
// Deprecated - use limits.disk
'disk' => $this->requiredToOptional('disk', $rules['disk']),
'add_allocations' => 'bail|array',

View File

@@ -11,7 +11,7 @@ class UpdateServerDetailsRequest extends ServerWriteRequest
*/
public function rules(): array
{
$rules = $this->route() ? Server::getRulesForUpdate($this->parameter('server', Server::class)) : Server::getRules();
$rules = Server::getRulesForUpdate($this->parameter('server', Server::class));
return [
'external_id' => $rules['external_id'],

View File

@@ -17,12 +17,12 @@ class UpdateServerStartupRequest extends ApplicationApiRequest
*/
public function rules(): array
{
$rules = $this->route() ? Server::getRulesForUpdate($this->parameter('server', Server::class)) : Server::getRules();
$data = Server::getRulesForUpdate($this->parameter('server', Server::class));
return [
'startup' => 'sometimes|string',
'environment' => 'present|array',
'egg' => $rules['egg_id'],
'egg' => $data['egg_id'],
'image' => 'sometimes|string',
'skip_scripts' => 'present|boolean',
];

View File

@@ -117,7 +117,7 @@ class Allocation extends Model
protected function address(): Attribute
{
return Attribute::make(
get: fn () => (is_ipv6($this->alias) ? "[$this->alias]" : $this->alias) . ":$this->port",
get: fn () => "$this->alias:$this->port",
);
}

View File

@@ -7,8 +7,6 @@ use App\Traits\HasValidation;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Eloquent\BackupQueryBuilder;
use App\Enums\BackupStatus;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -25,7 +23,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property int $bytes
* @property string|null $upload_id
* @property \Carbon\CarbonImmutable|null $completed_at
* @property BackupStatus $status
* @property \Carbon\CarbonImmutable $created_at
* @property \Carbon\CarbonImmutable $updated_at
* @property \Carbon\CarbonImmutable|null $deleted_at
@@ -82,13 +79,6 @@ class Backup extends Model implements Validatable
];
}
protected function status(): Attribute
{
return Attribute::make(
get: fn () => !$this->completed_at ? BackupStatus::InProgress : ($this->is_successful ? BackupStatus::Successful : BackupStatus::Failed),
);
}
public function server(): BelongsTo
{
return $this->belongsTo(Server::class);

View File

@@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\DB;
/**
* @property int $id
@@ -35,6 +36,8 @@ class Database extends Model implements Validatable
*/
public const RESOURCE_NAME = 'server_database';
public const DEFAULT_CONNECTION_NAME = 'dynamic';
/**
* The attributes excluded from the model's JSON form.
*/
@@ -101,7 +104,7 @@ class Database extends Model implements Validatable
*/
private function run(string $statement): bool
{
return $this->host->buildConnection()->statement($statement);
return DB::connection(self::DEFAULT_CONNECTION_NAME)->statement($statement);
}
/**

View File

@@ -4,12 +4,10 @@ namespace App\Models;
use App\Contracts\Validatable;
use App\Traits\HasValidation;
use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\DB;
/**
* @property int $id
@@ -84,21 +82,4 @@ class DatabaseHost extends Model implements Validatable
{
return $this->hasMany(Database::class);
}
public function buildConnection(string $database = 'mysql', string $charset = 'utf8', string $collation = 'utf8_unicode_ci'): Connection
{
/** @var Connection $connection */
$connection = DB::build([
'driver' => 'mysql',
'host' => $this->host,
'port' => $this->port,
'database' => $database,
'username' => $this->username,
'password' => $this->password,
'charset' => $charset,
'collation' => $collation,
]);
return $connection;
}
}

View File

@@ -5,13 +5,11 @@ namespace App\Models;
use App\Contracts\Validatable;
use App\Exceptions\Service\Egg\HasChildrenException;
use App\Exceptions\Service\HasActiveServersException;
use App\Extensions\Features\FeatureProvider;
use App\Traits\HasValidation;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Support\Str;
/**
@@ -72,6 +70,19 @@ class Egg extends Model implements Validatable
*/
public const EXPORT_VERSION = 'PLCN_v1';
/**
* Different features that can be enabled on any given egg. These are used internally
* to determine which types of frontend functionality should be shown to the user. Eggs
* will automatically inherit features from a parent egg if they are already configured
* to copy configuration values from said egg.
*
* To skip copying the features, an empty array value should be passed in ("[]") rather
* than leaving it null.
*/
public const FEATURE_EULA_POPUP = 'eula';
public const FEATURE_FASTDL = 'fastdl';
/**
* Fields that are not mass assignable.
*/
@@ -161,12 +172,6 @@ class Egg extends Model implements Validatable
});
}
/** @return array<FeatureProvider> */
public function features(): array
{
return FeatureProvider::getProviders($this->features);
}
/**
* Returns the install script for the egg; if egg is copying from another
* it will return the copied script.
@@ -284,11 +289,6 @@ class Egg extends Model implements Validatable
return $this->configFrom->file_denylist;
}
public function mounts(): MorphToMany
{
return $this->morphToMany(Mount::class, 'mountable');
}
/**
* Gets all servers associated with this egg.
*/

View File

@@ -12,9 +12,6 @@ use Illuminate\Http\Client\ConnectionException;
use Sushi\Sushi;
/**
* \App\Models\File.
*
* @property int $id
* @property string $name
* @property Carbon $created_at
* @property Carbon $modified_at
@@ -30,7 +27,11 @@ class File extends Model
{
use Sushi;
protected int $sushiInsertChunkSize = 100;
protected $primaryKey = 'name';
public $incrementing = false;
protected $keyType = 'string';
public const ARCHIVE_MIMES = [
'application/vnd.rar', // .rar
@@ -150,17 +151,23 @@ class File extends Model
try {
$fileRepository = (new DaemonFileRepository())->setServer(self::$server);
if (!is_null(self::$searchTerm)) {
$contents = cache()->remember('file_search_' . self::$path . '_' . self::$searchTerm, now()->addMinute(), fn () => $fileRepository->search(self::$searchTerm, self::$path));
} else {
$contents = $fileRepository->getDirectory(self::$path ?? '/');
$contents = [];
try {
if (!is_null(self::$searchTerm)) {
$contents = cache()->remember('file_search_' . self::$path . '_' . self::$searchTerm, now()->addMinute(), fn () => $fileRepository->search(self::$searchTerm, self::$path));
} else {
$contents = $fileRepository->getDirectory(self::$path ?? '/');
}
} catch (ConnectionException $exception) {
report($exception);
}
if (isset($contents['error'])) {
throw new Exception($contents['error']);
}
$rows = array_map(function ($file) {
return array_map(function ($file) {
return [
'name' => $file['name'],
'created_at' => Carbon::parse($file['created'])->timezone('UTC'),
@@ -174,14 +181,6 @@ class File extends Model
'mime_type' => $file['mime'],
];
}, $contents);
$rowCount = count($rows);
$limit = 999;
if ($rowCount > $limit) {
$this->sushiInsertChunkSize = min(floor($limit / count($this->getSchema())), $rowCount);
}
return $rows;
} catch (Exception $exception) {
report($exception);
@@ -190,12 +189,8 @@ class File extends Model
$message = $message->after('cURL error 7: ')->before(' after ');
}
if ($exception instanceof ConnectionException) {
$message = str('Node connection failed');
}
AlertBanner::make()
->title('Could not load files!')
->title('Could not load files')
->body($message->toString())
->danger()
->send();

View File

@@ -5,7 +5,7 @@ namespace App\Models;
use App\Contracts\Validatable;
use App\Traits\HasValidation;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
/**
* @property int $id
@@ -102,24 +102,24 @@ class Mount extends Model implements Validatable
/**
* Returns all eggs that have this mount assigned.
*/
public function eggs(): MorphToMany
public function eggs(): BelongsToMany
{
return $this->morphedByMany(Egg::class, 'mountable');
return $this->belongsToMany(Egg::class);
}
/**
* Returns all nodes that have this mount assigned.
*/
public function nodes(): MorphToMany
public function nodes(): BelongsToMany
{
return $this->morphedByMany(Node::class, 'mountable');
return $this->belongsToMany(Node::class);
}
/**
* Returns all servers that have this mount assigned.
*/
public function servers(): MorphToMany
public function servers(): BelongsToMany
{
return $this->morphedByMany(Server::class, 'mountable');
return $this->belongsToMany(Server::class);
}
}

14
app/Models/MountNode.php Normal file
View File

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

View File

@@ -12,7 +12,6 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
@@ -50,8 +49,6 @@ use Symfony\Component\Yaml\Yaml;
* @property int|null $servers_count
* @property \App\Models\Allocation[]|\Illuminate\Database\Eloquent\Collection $allocations
* @property int|null $allocations_count
* @property \App\Models\Role[]|\Illuminate\Database\Eloquent\Collection $roles
* @property int|null $roles_count
*/
class Node extends Model implements Validatable
{
@@ -217,7 +214,7 @@ class Node extends Model implements Validatable
],
],
'allowed_mounts' => $this->mounts->pluck('source')->toArray(),
'remote' => config('app.url'),
'remote' => route('filament.app.resources...index'),
];
}
@@ -242,9 +239,9 @@ class Node extends Model implements Validatable
return $this->maintenance_mode;
}
public function mounts(): MorphToMany
public function mounts(): HasManyThrough
{
return $this->morphToMany(Mount::class, 'mountable');
return $this->hasManyThrough(Mount::class, MountNode::class, 'node_id', 'id', 'id', 'mount_id');
}
/**
@@ -271,11 +268,6 @@ class Node extends Model implements Validatable
return $this->belongsToMany(DatabaseHost::class);
}
public function roles(): HasManyThrough
{
return $this->hasManyThrough(Role::class, NodeRole::class, 'node_id', 'id', 'id', 'role_id');
}
/**
* Returns a boolean if the node is viable for an additional server to be placed on it.
*/
@@ -404,11 +396,10 @@ class Node extends Model implements Validatable
}
}
$ips = $ips->filter(fn (string $ip) => is_ip($ip));
// Only IPV4
$ips = $ips->filter(fn (string $ip) => filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false);
// TODO: remove later
$ips->push('0.0.0.0');
$ips->push('::');
return $ips->unique()->all();
});

View File

@@ -1,12 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\Pivot;
class NodeRole extends Pivot
{
protected $table = 'node_role';
protected $primaryKey = null;
}

View File

@@ -4,13 +4,12 @@ namespace App\Models;
use App\Contracts\Validatable;
use App\Traits\HasValidation;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
class Permission extends Model implements Validatable
{
use HasFactory, HasValidation;
use HasValidation;
/**
* The resource name for this model when it is transformed into an
@@ -39,7 +38,7 @@ class Permission extends Model implements Validatable
public const ACTION_DATABASE_DELETE = 'database.delete';
public const ACTION_DATABASE_VIEW_PASSWORD = 'database.view-password';
public const ACTION_DATABASE_VIEW_PASSWORD = 'database.view_password';
public const ACTION_SCHEDULE_READ = 'schedule.read';
@@ -114,6 +113,127 @@ class Permission extends Model implements Validatable
'permission' => ['required', 'string'],
];
/**
* All the permissions available on the system. You should use self::permissions()
* to retrieve them, and not directly access this array as it is subject to change.
*
* @see Permission::permissions()
*
* @var array<array-key, array{
* description: string,
* keys: array<array-key, string>,
* }>
*/
protected static array $permissions = [
'websocket' => [
'description' => 'Allows the user to connect to the server websocket, giving them access to view console output and realtime server stats.',
'keys' => [
'connect' => 'Allows a user to connect to the websocket instance for a server to stream the console.',
],
],
'control' => [
'description' => 'Permissions that control a user\'s ability to control the power state of a server, or send commands.',
'keys' => [
'console' => 'Allows a user to send commands to the server instance via the console.',
'start' => 'Allows a user to start the server if it is stopped.',
'stop' => 'Allows a user to stop a server if it is running.',
'restart' => 'Allows a user to perform a server restart. This allows them to start the server if it is offline, but not put the server in a completely stopped state.',
],
],
'user' => [
'description' => 'Permissions that allow a user to manage other subusers on a server. They will never be able to edit their own account, or assign permissions they do not have themselves.',
'keys' => [
'create' => 'Allows a user to create new subusers for the server.',
'read' => 'Allows the user to view subusers and their permissions for the server.',
'update' => 'Allows a user to modify other subusers.',
'delete' => 'Allows a user to delete a subuser from the server.',
],
],
'file' => [
'description' => 'Permissions that control a user\'s ability to modify the filesystem for this server.',
'keys' => [
'create' => 'Allows a user to create additional files and folders via the Panel or direct upload.',
'read' => 'Allows a user to view the contents of a directory, but not view the contents of or download files.',
'read-content' => 'Allows a user to view the contents of a given file. This will also allow the user to download files.',
'update' => 'Allows a user to update the contents of an existing file or directory.',
'delete' => 'Allows a user to delete files or directories.',
'archive' => 'Allows a user to archive the contents of a directory as well as decompress existing archives on the system.',
'sftp' => 'Allows a user to connect to SFTP and manage server files using the other assigned file permissions.',
],
],
'backup' => [
'description' => 'Permissions that control a user\'s ability to generate and manage server backups.',
'keys' => [
'create' => 'Allows a user to create new backups for this server.',
'read' => 'Allows a user to view all backups that exist for this server.',
'delete' => 'Allows a user to remove backups from the system.',
'download' => 'Allows a user to download a backup for the server. Danger: this allows a user to access all files for the server in the backup.',
'restore' => 'Allows a user to restore a backup for the server. Danger: this allows the user to delete all the server files in the process.',
],
],
// Controls permissions for editing or viewing a server's allocations.
'allocation' => [
'description' => 'Permissions that control a user\'s ability to modify the port allocations for this server.',
'keys' => [
'read' => 'Allows a user to view all allocations currently assigned to this server. Users with any level of access to this server can always view the primary allocation.',
'create' => 'Allows a user to assign additional allocations to the server.',
'update' => 'Allows a user to change the primary server allocation and attach notes to each allocation.',
'delete' => 'Allows a user to delete an allocation from the server.',
],
],
// Controls permissions for editing or viewing a server's startup parameters.
'startup' => [
'description' => 'Permissions that control a user\'s ability to view this server\'s startup parameters.',
'keys' => [
'read' => 'Allows a user to view the startup variables for a server.',
'update' => 'Allows a user to modify the startup variables for the server.',
'docker-image' => 'Allows a user to modify the Docker image used when running the server.',
],
],
'database' => [
'description' => 'Permissions that control a user\'s access to the database management for this server.',
'keys' => [
'create' => 'Allows a user to create a new database for this server.',
'read' => 'Allows a user to view the database associated with this server.',
'update' => 'Allows a user to rotate the password on a database instance. If the user does not have the view_password permission they will not see the updated password.',
'delete' => 'Allows a user to remove a database instance from this server.',
'view_password' => 'Allows a user to view the password associated with a database instance for this server.',
],
],
'schedule' => [
'description' => 'Permissions that control a user\'s access to the schedule management for this server.',
'keys' => [
'create' => 'Allows a user to create new schedules for this server.', // task.create-schedule
'read' => 'Allows a user to view schedules and the tasks associated with them for this server.', // task.view-schedule, task.list-schedules
'update' => 'Allows a user to update schedules and schedule tasks for this server.', // task.edit-schedule, task.queue-schedule, task.toggle-schedule
'delete' => 'Allows a user to delete schedules for this server.', // task.delete-schedule
],
],
'settings' => [
'description' => 'Permissions that control a user\'s access to the settings for this server.',
'keys' => [
'rename' => 'Allows a user to rename this server and change the description of it.',
'reinstall' => 'Allows a user to trigger a reinstall of this server.',
],
],
'activity' => [
'description' => 'Permissions that control a user\'s access to the server activity logs.',
'keys' => [
'read' => 'Allows a user to view the activity logs for the server.',
],
],
];
protected function casts(): array
{
return [
@@ -121,92 +241,11 @@ class Permission extends Model implements Validatable
];
}
/**
* All the permissions available on the system.
*
* @return array<int, array{
* name: string,
* icon: string,
* permissions: string[]
* }>
*/
public static function permissionData(): array
{
return [
[
'name' => 'control',
'icon' => 'tabler-terminal-2',
'permissions' => ['console', 'start', 'stop', 'restart'],
],
[
'name' => 'user',
'icon' => 'tabler-users',
'permissions' => ['read', 'create', 'update', 'delete'],
],
[
'name' => 'file',
'icon' => 'tabler-files',
'permissions' => ['read', 'read-content', 'create', 'update', 'delete', 'archive', 'sftp'],
],
[
'name' => 'backup',
'icon' => 'tabler-file-zip',
'permissions' => ['read', 'create', 'delete', 'download', 'restore'],
],
[
'name' => 'allocation',
'icon' => 'tabler-network',
'permissions' => ['read', 'create', 'update', 'delete'],
],
[
'name' => 'startup',
'icon' => 'tabler-player-play',
'permissions' => ['read', 'update', 'docker-image'],
],
[
'name' => 'database',
'icon' => 'tabler-database',
'permissions' => ['read', 'create', 'update', 'delete', 'view-password'],
],
[
'name' => 'schedule',
'icon' => 'tabler-clock',
'permissions' => ['read', 'create', 'update', 'delete'],
],
[
'name' => 'settings',
'icon' => 'tabler-settings',
'permissions' => ['rename', 'reinstall'],
],
[
'name' => 'activity',
'icon' => 'tabler-stack',
'permissions' => ['read'],
],
];
}
/**
* Returns all the permissions available on the system for a user to have when controlling a server.
*/
public static function permissions(): Collection
{
$permissions = [
'websocket' => [
'description' => 'Allows the user to connect to the server websocket, giving them access to view console output and realtime server stats.',
'keys' => [
'connect' => 'Allows a user to connect to the websocket instance for a server to stream the console.',
],
],
];
foreach (static::permissionData() as $data) {
$permissions[$data['name']] = [
'description' => trans('server/users.permissions.' . $data['name'] . '_desc'),
'keys' => collect($data['permissions'])->mapWithKeys(fn ($key) => [$key => trans('server/users.permissions.' . $data['name'] . '_' . str($key)->replace('-', '_'))])->toArray(),
];
}
return collect($permissions);
return Collection::make(self::$permissions);
}
}

View File

@@ -4,8 +4,6 @@ namespace App\Models;
use App\Enums\RolePermissionModels;
use App\Enums\RolePermissionPrefixes;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Spatie\Permission\Models\Role as BaseRole;
/**
@@ -16,13 +14,9 @@ use Spatie\Permission\Models\Role as BaseRole;
* @property int|null $permissions_count
* @property \Illuminate\Database\Eloquent\Collection|\App\Models\User[] $users
* @property int|null $users_count
* @property \Illuminate\Database\Eloquent\Collection|\App\Models\Node[] $nodes
* @property int|null $nodes_count
*/
class Role extends BaseRole
{
use HasFactory;
public const RESOURCE_NAME = 'role';
public const ROOT_ADMIN = 'Root Admin';
@@ -131,9 +125,4 @@ class Role extends BaseRole
return $role;
}
public function nodes(): BelongsToMany
{
return $this->belongsToMany(Node::class, NodeRole::class);
}
}

View File

@@ -12,6 +12,7 @@ use Carbon\CarbonInterface;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Query\JoinClause;
@@ -231,12 +232,7 @@ class Server extends Model implements Validatable
public function isInstalled(): bool
{
return $this->status !== ServerState::Installing && !$this->isFailedInstall();
}
public function isFailedInstall(): bool
{
return $this->status === ServerState::InstallFailed || $this->status === ServerState::ReinstallFailed;
return $this->status !== ServerState::Installing && $this->status !== ServerState::InstallFailed;
}
public function isSuspended(): bool
@@ -314,6 +310,15 @@ class Server extends Model implements Validatable
return $this->hasMany(ServerVariable::class);
}
/** @deprecated use serverVariables */
public function viewableServerVariables(): HasMany
{
return $this->serverVariables()
->join('egg_variables', 'egg_variables.id', '=', 'server_variables.variable_id')
->orderBy('egg_variables.sort')
->where('egg_variables.user_viewable', true);
}
/**
* Gets information for the node associated with this server.
*/
@@ -354,9 +359,9 @@ class Server extends Model implements Validatable
return $this->hasMany(Backup::class);
}
public function mounts(): MorphToMany
public function mounts(): BelongsToMany
{
return $this->morphToMany(Mount::class, 'mountable');
return $this->belongsToMany(Mount::class);
}
/**

View File

@@ -4,7 +4,6 @@ namespace App\Models;
use App\Contracts\Validatable;
use App\Exceptions\DisplayException;
use App\Extensions\Avatar\AvatarProvider;
use App\Rules\Username;
use App\Facades\Activity;
use App\Traits\HasValidation;
@@ -24,7 +23,6 @@ use Illuminate\Auth\Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\Builder;
use App\Models\Traits\HasAccessTokens;
use Filament\Models\Contracts\HasAvatar;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\Access\Authorizable;
@@ -32,7 +30,6 @@ use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Illuminate\Support\Facades\Storage;
use ResourceBundle;
use Spatie\Permission\Traits\HasRoles;
@@ -90,7 +87,7 @@ use Spatie\Permission\Traits\HasRoles;
* @method static Builder|User whereUsername($value)
* @method static Builder|User whereUuid($value)
*/
class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract, FilamentUser, HasAvatar, HasName, HasTenants, Validatable
class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract, FilamentUser, HasName, HasTenants, Validatable
{
use Authenticatable;
use Authorizable { can as protected canned; }
@@ -286,22 +283,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
});
}
public function accessibleNodes(): Builder
{
// Root admins can access all nodes
if ($this->isRootAdmin()) {
return Node::query();
}
// Check if there are no restrictions from any role
$roleIds = $this->roles()->pluck('id');
if (!NodeRole::whereIn('role_id', $roleIds)->exists()) {
return Node::query();
}
return Node::whereHas('roles', fn (Builder $builder) => $builder->whereIn('roles.id', $roleIds));
}
public function subusers(): HasMany
{
return $this->hasMany(Subuser::class);
@@ -391,39 +372,13 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
return $this->username;
}
public function getFilamentAvatarUrl(): ?string
public function canTarget(Model $user): bool
{
if (config('panel.filament.uploadable-avatars')) {
$path = "avatars/$this->id.png";
if (Storage::disk('public')->exists($path)) {
return Storage::url($path);
}
}
$provider = AvatarProvider::getProvider(config('panel.filament.avatar-provider'));
return $provider?->get($this);
}
public function canTarget(Model $model): bool
{
// Root admins can target everyone and everything
if ($this->isRootAdmin()) {
return true;
}
// Make sure normal admins can't target root admins
if ($model instanceof User) {
return !$model->isRootAdmin();
}
// Make sure the user can only target accessible nodes
if ($model instanceof Node) {
return $this->accessibleNodes()->where('id', $model->id)->exists();
}
return false;
return $user instanceof User && !$user->isRootAdmin();
}
public function getTenants(Panel $panel): array|Collection

View File

@@ -2,7 +2,6 @@
namespace App\Notifications;
use App\Filament\Server\Pages\Console;
use App\Models\Server;
use App\Models\User;
use Illuminate\Bus\Queueable;
@@ -28,6 +27,6 @@ class AddedToServer extends Notification implements ShouldQueue
->greeting('Hello ' . $notifiable->username . '!')
->line('You have been added as a subuser for the following server, allowing you certain control over the server.')
->line('Server Name: ' . $this->server->name)
->action('Visit Server', Console::getUrl(panel: 'server', tenant: $this->server));
->action('Visit Server', url('/server/' . $this->server->uuid_short));
}
}

View File

@@ -28,6 +28,6 @@ class RemovedFromServer extends Notification implements ShouldQueue
->greeting('Hello ' . $notifiable->username . '.')
->line('You have been removed as a subuser for the following server.')
->line('Server Name: ' . $this->server->name)
->action('Visit Panel', url(''));
->action('Visit Panel', config('app.url'));
}
}

View File

@@ -2,10 +2,10 @@
namespace App\Notifications;
use App\Filament\Server\Pages\Console;
use App\Models\User;
use Illuminate\Bus\Queueable;
use App\Models\Server;
use App\Filament\App\Resources\ServerResource\Pages\ListServers;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -28,6 +28,6 @@ class ServerInstalled extends Notification implements ShouldQueue
->greeting('Hello ' . $notifiable->username . '.')
->line('Your server has finished installing and is now ready for you to use.')
->line('Server Name: ' . $this->server->name)
->action('Login and Begin Using', Console::getUrl(panel: 'server', tenant: $this->server));
->action('Login and Begin Using', ListServers::getUrl());
}
}

View File

@@ -2,28 +2,9 @@
namespace App\Policies;
use App\Models\DatabaseHost;
use App\Models\User;
class DatabaseHostPolicy
{
use DefaultPolicies;
protected string $modelName = 'databasehost';
public function before(User $user, string $ability, string|DatabaseHost $databaseHost): ?bool
{
// For "viewAny" the $databaseHost param is the class name
if (is_string($databaseHost)) {
return null;
}
foreach ($databaseHost->nodes as $node) {
if (!$user->canTarget($node)) {
return false;
}
}
return null;
}
}

View File

@@ -2,28 +2,9 @@
namespace App\Policies;
use App\Models\Mount;
use App\Models\User;
class MountPolicy
{
use DefaultPolicies;
protected string $modelName = 'mount';
public function before(User $user, string $ability, string|Mount $mount): ?bool
{
// For "viewAny" the $mount param is the class name
if (is_string($mount)) {
return null;
}
foreach ($mount->nodes as $node) {
if (!$user->canTarget($node)) {
return false;
}
}
return null;
}
}

View File

@@ -2,26 +2,9 @@
namespace App\Policies;
use App\Models\Node;
use App\Models\User;
class NodePolicy
{
use DefaultPolicies;
protected string $modelName = 'node';
public function before(User $user, string $ability, string|Node $node): ?bool
{
// For "viewAny" the $node param is the class name
if (is_string($node)) {
return null;
}
if (!$user->canTarget($node)) {
return false;
}
return null;
}
}

View File

@@ -21,11 +21,6 @@ class ServerPolicy
return null;
}
// Make sure user can target node of the server
if (!$user->canTarget($server->node)) {
return false;
}
// Owner has full server permissions
if ($server->owner_id === $user->id) {
return true;

View File

@@ -11,15 +11,11 @@ use App\Checks\PanelVersionCheck;
use App\Checks\ScheduleCheck;
use App\Checks\UsedDiskSpaceCheck;
use App\Extensions\Avatar\Providers\GravatarProvider;
use App\Extensions\Avatar\Providers\LocalAvatarProvider;
use App\Extensions\Avatar\Providers\UiAvatarsProvider;
use App\Extensions\OAuth\Providers\GitlabProvider;
use App\Models;
use App\Extensions\Captcha\Providers\TurnstileProvider;
use App\Extensions\Features\GSLToken;
use App\Extensions\Features\JavaVersion;
use App\Extensions\Features\MinecraftEula;
use App\Extensions\Features\PIDLimit;
use App\Extensions\Features\SteamDiskSpace;
use App\Extensions\OAuth\Providers\AuthentikProvider;
use App\Extensions\OAuth\Providers\CommonProvider;
use App\Extensions\OAuth\Providers\DiscordProvider;
@@ -82,7 +78,6 @@ class AppServiceProvider extends ServiceProvider
'ssh_key' => Models\UserSSHKey::class,
'task' => Models\Task::class,
'user' => Models\User::class,
'node' => Models\Node::class,
]);
Http::macro(
@@ -126,13 +121,7 @@ class AppServiceProvider extends ServiceProvider
// Default Avatar providers
GravatarProvider::register();
UiAvatarsProvider::register();
// Default Feature providers
GSLToken::register($app);
JavaVersion::register($app);
MinecraftEula::register($app);
PIDLimit::register($app);
SteamDiskSpace::register($app);
LocalAvatarProvider::register();
FilamentColor::register([
'danger' => Color::Red,

View File

@@ -2,10 +2,10 @@
namespace App\Providers\Filament;
use App\Extensions\Avatar\AvatarProvider;
use App\Filament\Pages\Auth\Login;
use App\Filament\Pages\Auth\EditProfile;
use App\Http\Middleware\LanguageMiddleware;
use App\Http\Middleware\RequireTwoFactorAuthentication;
use Filament\Http\Middleware\Authenticate;
use Filament\Http\Middleware\DisableBladeIconComponents;
use Filament\Http\Middleware\DispatchServingFilamentEvent;
@@ -37,8 +37,9 @@ class AdminPanelProvider extends PanelProvider
->brandLogo(config('app.logo'))
->brandLogoHeight('2rem')
->favicon(config('app.favicon', '/pelican.ico'))
->topNavigation(config('panel.filament.top-navigation', false))
->topNavigation(config('panel.filament.top-navigation', true))
->maxContentWidth(config('panel.filament.display-width', 'screen-2xl'))
->defaultAvatarProvider(fn () => get_class(AvatarProvider::getProvider(config('panel.filament.avatar-provider'))))
->login(Login::class)
->passwordReset()
->userMenuItems([
@@ -73,7 +74,6 @@ class AdminPanelProvider extends PanelProvider
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
LanguageMiddleware::class,
RequireTwoFactorAuthentication::class,
])
->authMiddleware([
Authenticate::class,

View File

@@ -2,10 +2,9 @@
namespace App\Providers\Filament;
use App\Extensions\Avatar\AvatarProvider;
use App\Filament\Pages\Auth\Login;
use App\Filament\Pages\Auth\EditProfile;
use App\Http\Middleware\LanguageMiddleware;
use App\Http\Middleware\RequireTwoFactorAuthentication;
use Filament\Facades\Filament;
use Filament\Http\Middleware\Authenticate;
use Filament\Http\Middleware\DisableBladeIconComponents;
@@ -34,8 +33,9 @@ class AppPanelProvider extends PanelProvider
->brandLogo(config('app.logo'))
->brandLogoHeight('2rem')
->favicon(config('app.favicon', '/pelican.ico'))
->topNavigation(config('panel.filament.top-navigation', false))
->topNavigation(config('panel.filament.top-navigation', true))
->maxContentWidth(config('panel.filament.display-width', 'screen-2xl'))
->defaultAvatarProvider(fn () => get_class(AvatarProvider::getProvider(config('panel.filament.avatar-provider'))))
->navigation(false)
->profile(EditProfile::class, false)
->login(Login::class)
@@ -59,8 +59,6 @@ class AppPanelProvider extends PanelProvider
SubstituteBindings::class,
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
LanguageMiddleware::class,
RequireTwoFactorAuthentication::class,
])
->authMiddleware([
Authenticate::class,

View File

@@ -2,13 +2,12 @@
namespace App\Providers\Filament;
use App\Extensions\Avatar\AvatarProvider;
use App\Filament\App\Resources\ServerResource\Pages\ListServers;
use App\Filament\Pages\Auth\Login;
use App\Filament\Admin\Resources\ServerResource\Pages\EditServer;
use App\Filament\Pages\Auth\EditProfile;
use App\Http\Middleware\Activity\ServerSubject;
use App\Http\Middleware\LanguageMiddleware;
use App\Http\Middleware\RequireTwoFactorAuthentication;
use App\Models\Server;
use Filament\Facades\Filament;
use Filament\Http\Middleware\Authenticate;
@@ -41,8 +40,9 @@ class ServerPanelProvider extends PanelProvider
->brandLogo(config('app.logo'))
->brandLogoHeight('2rem')
->favicon(config('app.favicon', '/pelican.ico'))
->topNavigation(config('panel.filament.top-navigation', false))
->topNavigation(config('panel.filament.top-navigation', true))
->maxContentWidth(config('panel.filament.display-width', 'screen-2xl'))
->defaultAvatarProvider(fn () => get_class(AvatarProvider::getProvider(config('panel.filament.avatar-provider'))))
->login(Login::class)
->passwordReset()
->userMenuItems([
@@ -63,7 +63,7 @@ class ServerPanelProvider extends PanelProvider
])
->navigationItems([
NavigationItem::make('Open in Admin')
->url(fn () => EditServer::getUrl(['record' => Filament::getTenant()], panel: 'admin'))
->url(fn () => EditServer::getUrl(['record' => Filament::getTenant()], panel: 'admin', tenant: null), true)
->visible(fn () => auth()->user()->can('view server', Filament::getTenant()))
->icon('tabler-arrow-back')
->sort(99),
@@ -81,8 +81,6 @@ class ServerPanelProvider extends PanelProvider
SubstituteBindings::class,
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
LanguageMiddleware::class,
RequireTwoFactorAuthentication::class,
ServerSubject::class,
])
->authMiddleware([

View File

@@ -21,7 +21,14 @@ class DaemonConfigurationRepository extends DaemonRepository
->connectTimeout(3)
->get('/api/system')
->throwIf(function ($result) {
$this->enforceValidNodeToken($result);
$header = $result->header('User-Agent');
if (
filled($header) &&
preg_match('/^Pelican Wings\/v(?:\d+\.\d+\.\d+|develop) \(id:(\w*)\)$/', $header, $matches) &&
array_get($matches, 1, '') !== $this->node->daemon_token_id
) {
throw new ConnectionException($result->effectiveUri()->__toString() . ' does not match node token_id !');
}
if (!$result->collect()->has(['architecture', 'cpu_count', 'kernel_version', 'os', 'version'])) {
throw new ConnectionException($result->effectiveUri()->__toString() . ' is not Pelican Wings !');
}

View File

@@ -7,8 +7,6 @@ use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
use Webmozart\Assert\Assert;
use App\Models\Server;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\Response;
abstract class DaemonRepository
{
@@ -49,24 +47,6 @@ abstract class DaemonRepository
{
Assert::isInstanceOf($this->node, Node::class);
return Http::daemon($this->node, $headers)->throwIf(fn ($condition) => $this->enforceValidNodeToken($condition));
}
protected function enforceValidNodeToken(Response|bool $condition): bool
{
if (is_bool($condition)) {
return $condition;
}
$header = $condition->header('User-Agent');
if (
empty($header) ||
preg_match('/^Pelican Wings\/v(?:\d+\.\d+\.\d+|develop) \(id:(\w*)\)$/', $header, $matches) &&
array_get($matches, 1, '') !== $this->node->daemon_token_id
) {
throw new ConnectionException($condition->effectiveUri()->__toString() . ' does not match node token_id !');
}
return true;
return Http::daemon($this->node, $headers);
}
}

View File

@@ -141,12 +141,4 @@ class DaemonServerRepository extends DaemonRepository
'jtis' => [md5($id . $this->server->uuid)],
]);
}
public function getInstallLogs(): string
{
return $this->getHttpClient()
->get("/api/servers/{$this->server->uuid}/install-logs")
->throw()
->json('data');
}
}

View File

@@ -51,7 +51,12 @@ class AssignmentService
}
try {
$parsed = Network::parse($data['allocation_ip']);
// TODO: how should we approach supporting IPv6 with this?
// gethostbyname only supports IPv4, but the alternative (dns_get_record) returns
// an array of records, which is not ideal for this use case, we need a SINGLE
// IP to use, not multiple.
$underlying = gethostbyname($data['allocation_ip']);
$parsed = Network::parse($underlying);
} catch (\Exception $exception) {
throw new DisplayException("Could not parse provided allocation IP address ({$data['allocation_ip']}): {$exception->getMessage()}", $exception);
}

View File

@@ -7,6 +7,7 @@ use App\Models\Server;
use App\Models\Database;
use App\Helpers\Utilities;
use Illuminate\Database\ConnectionInterface;
use App\Extensions\DynamicDatabaseConnection;
use App\Exceptions\Repository\DuplicateDatabaseNameException;
use App\Exceptions\Service\Database\TooManyDatabasesException;
use App\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException;
@@ -31,6 +32,7 @@ class DatabaseManagementService
public function __construct(
protected ConnectionInterface $connection,
protected DynamicDatabaseConnection $dynamic,
) {}
/**
@@ -92,6 +94,8 @@ class DatabaseManagementService
return $this->connection->transaction(function () use ($data) {
$database = $this->createModel($data);
$this->dynamic->set('dynamic', $data['database_host_id']);
$database->createDatabase($database->database);
$database->createUser(
$database->username,
@@ -118,18 +122,13 @@ class DatabaseManagementService
*/
public function delete(Database $database): ?bool
{
return $this->connection->transaction(function () use ($database) {
$database->dropDatabase($database->database);
$database->dropUser($database->username, $database->remote);
$database->flush();
$this->dynamic->set('dynamic', $database->database_host_id);
Activity::event('server:database.delete')
->subject($database)
->property('name', $database->database)
->log();
$database->dropDatabase($database->database);
$database->dropUser($database->username, $database->remote);
$database->flush();
return $database->delete();
});
return $database->delete();
}
/**

View File

@@ -5,6 +5,7 @@ namespace App\Services\Databases;
use App\Models\Database;
use App\Helpers\Utilities;
use Illuminate\Database\ConnectionInterface;
use App\Extensions\DynamicDatabaseConnection;
class DatabasePasswordService
{
@@ -13,6 +14,7 @@ class DatabasePasswordService
*/
public function __construct(
private ConnectionInterface $connection,
private DynamicDatabaseConnection $dynamic,
) {}
/**
@@ -27,6 +29,8 @@ class DatabasePasswordService
$password = Utilities::randomStringWithSpecialCharacters(24);
$this->connection->transaction(function () use ($database, $password) {
$this->dynamic->set('dynamic', $database->database_host_id);
$database->update([
'password' => $password,
]);

View File

@@ -3,7 +3,9 @@
namespace App\Services\Databases\Hosts;
use App\Models\DatabaseHost;
use Illuminate\Database\DatabaseManager;
use Illuminate\Database\ConnectionInterface;
use App\Extensions\DynamicDatabaseConnection;
class HostCreationService
{
@@ -12,6 +14,8 @@ class HostCreationService
*/
public function __construct(
private ConnectionInterface $connection,
private DatabaseManager $databaseManager,
private DynamicDatabaseConnection $dynamic,
) {}
/**
@@ -44,7 +48,8 @@ class HostCreationService
$host->nodes()->sync(array_get($data, 'node_ids', []));
// Confirm access using the provided credentials before saving data.
$host->buildConnection()->getPdo();
$this->dynamic->set('dynamic', $host);
$this->databaseManager->connection('dynamic')->getPdo();
return $host;
});

View File

@@ -3,7 +3,9 @@
namespace App\Services\Databases\Hosts;
use App\Models\DatabaseHost;
use Illuminate\Database\DatabaseManager;
use Illuminate\Database\ConnectionInterface;
use App\Extensions\DynamicDatabaseConnection;
class HostUpdateService
{
@@ -12,6 +14,8 @@ class HostUpdateService
*/
public function __construct(
private ConnectionInterface $connection,
private DatabaseManager $databaseManager,
private DynamicDatabaseConnection $dynamic,
) {}
/**
@@ -34,8 +38,8 @@ class HostUpdateService
return $this->connection->transaction(function () use ($data, $host) {
$host->update($data);
// Confirm access using the provided credentials before saving data.
$host->buildConnection()->getPdo();
$this->dynamic->set('dynamic', $host);
$this->databaseManager->connection('dynamic')->getPdo();
return $host;
});

View File

@@ -101,9 +101,6 @@ class ServerConfigurationStructureService
'egg' => [
'id' => $server->egg->uuid,
'file_denylist' => $server->egg->inherit_file_denylist,
'features' => collect($server->egg->features())->mapWithKeys(fn ($feature) => [
$feature->getId() => $feature->getListeners(),
])->all(),
],
];

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