mirror of
https://github.com/pelican-dev/panel.git
synced 2025-05-20 15:44:45 +02:00
Merge branch 'main' into filament-v4
This commit is contained in:
commit
1ba9a1dab3
3
.gitignore
vendored
3
.gitignore
vendored
@ -24,8 +24,7 @@ yarn-error.log
|
|||||||
/.vscode
|
/.vscode
|
||||||
|
|
||||||
public/assets/manifest.json
|
public/assets/manifest.json
|
||||||
/database/*.sqlite
|
/database/*.sqlite*
|
||||||
/database/*.sqlite-journal
|
|
||||||
filament-monaco-editor/
|
filament-monaco-editor/
|
||||||
_ide_helper*
|
_ide_helper*
|
||||||
/.phpstorm.meta.php
|
/.phpstorm.meta.php
|
||||||
|
24
Dockerfile
24
Dockerfile
@ -1,16 +1,9 @@
|
|||||||
# syntax=docker.io/docker/dockerfile:1.13-labs
|
# syntax=docker.io/docker/dockerfile:1.13-labs
|
||||||
# Pelican Production Dockerfile
|
# Pelican Production Dockerfile
|
||||||
|
|
||||||
|
##
|
||||||
# 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".
|
# If you want to build this locally you want to run `docker build -f Dockerfile.dev`
|
||||||
|
##
|
||||||
# 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
|
# Stage 1-1: Composer Install
|
||||||
@ -82,15 +75,14 @@ RUN chown root:www-data ./ \
|
|||||||
&& chmod 750 ./ \
|
&& chmod 750 ./ \
|
||||||
# Files should not have execute set, but directories need it
|
# Files should not have execute set, but directories need it
|
||||||
&& find ./ -type d -exec chmod 750 {} \; \
|
&& find ./ -type d -exec chmod 750 {} \; \
|
||||||
# Symlink to env/database path, as www-data won't be able to write to webroot
|
# 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/.env ./.env \
|
||||||
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
|
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
|
||||||
&& mkdir -p /pelican-data/storage \
|
|
||||||
&& ln -sf /var/www/html/storage/app/public /var/www/html/public/storage \
|
&& ln -sf /var/www/html/storage/app/public /var/www/html/public/storage \
|
||||||
&& ln -s /pelican-data/storage /var/www/html/storage/app/public/avatars \
|
&& ln -s /pelican-data/storage/avatars /var/www/html/storage/app/public/avatars \
|
||||||
# Create necessary directories
|
# Allow www-data write permissions where necessary
|
||||||
&& 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 /var/www/html/public/storage \
|
&& 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
|
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord
|
||||||
|
|
||||||
|
111
Dockerfile.dev
Normal file
111
Dockerfile.dev
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
# 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 \
|
||||||
|
# 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" ]
|
@ -14,4 +14,24 @@ enum RolePermissionModels: string
|
|||||||
case Server = 'server';
|
case Server = 'server';
|
||||||
case User = 'user';
|
case User = 'user';
|
||||||
case Webhook = 'webhook';
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
51
app/Extensions/Features/FeatureProvider.php
Normal file
51
app/Extensions/Features/FeatureProvider.php
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
122
app/Extensions/Features/GSLToken.php
Normal file
122
app/Extensions/Features/GSLToken.php
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
<?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 Filament\Actions\Action;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
|
use Filament\Forms\Components\Placeholder;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
|
||||||
|
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 'gsltoken';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAction(): Action
|
||||||
|
{
|
||||||
|
/** @var Server $server */
|
||||||
|
$server = Filament::getTenant();
|
||||||
|
|
||||||
|
/** @var ServerVariable $serverVariable */
|
||||||
|
$serverVariable = $server->serverVariables()->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('java')
|
||||||
|
->label('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('Restart the server to use the new token.')
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Notification::make()
|
||||||
|
->title('Error')
|
||||||
|
->body($e->getMessage())
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function register(Application $app): self
|
||||||
|
{
|
||||||
|
return new self($app);
|
||||||
|
}
|
||||||
|
}
|
97
app/Extensions/Features/JavaVersion.php
Normal file
97
app/Extensions/Features/JavaVersion.php
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Extensions\Features;
|
||||||
|
|
||||||
|
use App\Facades\Activity;
|
||||||
|
use App\Models\Permission;
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Repositories\Daemon\DaemonPowerRepository;
|
||||||
|
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',
|
||||||
|
'minecraft 1.17 requires running the server with java 16 or above',
|
||||||
|
'minecraft 1.18 requires running the server with java 17 or above',
|
||||||
|
'unsupported major.minor version',
|
||||||
|
'has been compiled by a more recent version of the java runtime',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
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('Restart the server to use the new image.')
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Notification::make()
|
||||||
|
->title('Error')
|
||||||
|
->body($e->getMessage())
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function register(Application $app): self
|
||||||
|
{
|
||||||
|
return new self($app);
|
||||||
|
}
|
||||||
|
}
|
72
app/Extensions/Features/MinecraftEula.php
Normal file
72
app/Extensions/Features/MinecraftEula.php
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<?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();
|
||||||
|
$content = $fileRepository->setServer($server)->getContent('eula.txt');
|
||||||
|
$content = preg_replace('/(eula=)false/', '\1true', $content);
|
||||||
|
$fileRepository->setServer($server)->putContent('eula.txt', $content);
|
||||||
|
$powerRepository->setServer($server)->send('restart');
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title('Docker image updated')
|
||||||
|
->body('Restart the server.')
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Notification::make()
|
||||||
|
->title('Error')
|
||||||
|
->body($e->getMessage())
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function register(Application $app): self
|
||||||
|
{
|
||||||
|
return new self($app);
|
||||||
|
}
|
||||||
|
}
|
76
app/Extensions/Features/PIDLimit.php
Normal file
76
app/Extensions/Features/PIDLimit.php
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
64
app/Extensions/Features/SteamDiskSpace.php
Normal file
64
app/Extensions/Features/SteamDiskSpace.php
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
@ -70,6 +70,7 @@ class AllocationsRelationManager extends RelationManager
|
|||||||
->label(trans('admin/server.ip_address'))
|
->label(trans('admin/server.ip_address'))
|
||||||
->inlineLabel()
|
->inlineLabel()
|
||||||
->ipv4()
|
->ipv4()
|
||||||
|
->live()
|
||||||
->afterStateUpdated(fn (Set $set) => $set('allocation_ports', []))
|
->afterStateUpdated(fn (Set $set) => $set('allocation_ports', []))
|
||||||
->required(),
|
->required(),
|
||||||
TextInput::make('allocation_alias')
|
TextInput::make('allocation_alias')
|
||||||
@ -83,6 +84,7 @@ class AllocationsRelationManager extends RelationManager
|
|||||||
->label(trans('admin/server.ports'))
|
->label(trans('admin/server.ports'))
|
||||||
->inlineLabel()
|
->inlineLabel()
|
||||||
->live()
|
->live()
|
||||||
|
->disabled(fn (Get $get) => empty($get('allocation_ip')))
|
||||||
->afterStateUpdated(fn ($state, Set $set, Get $get) => $set('allocation_ports',
|
->afterStateUpdated(fn ($state, Set $set, Get $get) => $set('allocation_ports',
|
||||||
CreateServer::retrieveValidPorts($this->getOwnerRecord()->node, $state, $get('allocation_ip')))
|
CreateServer::retrieveValidPorts($this->getOwnerRecord()->node, $state, $get('allocation_ip')))
|
||||||
)
|
)
|
||||||
|
@ -5,6 +5,7 @@ namespace App\Filament\Server\Pages;
|
|||||||
use App\Enums\ConsoleWidgetPosition;
|
use App\Enums\ConsoleWidgetPosition;
|
||||||
use App\Enums\ContainerStatus;
|
use App\Enums\ContainerStatus;
|
||||||
use App\Exceptions\Http\Server\ServerStateConflictException;
|
use App\Exceptions\Http\Server\ServerStateConflictException;
|
||||||
|
use App\Extensions\Features\FeatureProvider;
|
||||||
use App\Filament\Server\Widgets\ServerConsole;
|
use App\Filament\Server\Widgets\ServerConsole;
|
||||||
use App\Filament\Server\Widgets\ServerCpuChart;
|
use App\Filament\Server\Widgets\ServerCpuChart;
|
||||||
use App\Filament\Server\Widgets\ServerMemoryChart;
|
use App\Filament\Server\Widgets\ServerMemoryChart;
|
||||||
@ -13,8 +14,9 @@ use App\Filament\Server\Widgets\ServerOverview;
|
|||||||
use App\Livewire\AlertBanner;
|
use App\Livewire\AlertBanner;
|
||||||
use App\Models\Permission;
|
use App\Models\Permission;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Concerns\InteractsWithActions;
|
||||||
use Filament\Facades\Filament;
|
use Filament\Facades\Filament;
|
||||||
|
use Filament\Actions\Action;
|
||||||
use Filament\Pages\Page;
|
use Filament\Pages\Page;
|
||||||
use Filament\Support\Enums\Size;
|
use Filament\Support\Enums\Size;
|
||||||
use Filament\Widgets\Widget;
|
use Filament\Widgets\Widget;
|
||||||
@ -47,6 +49,30 @@ 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
|
public function getWidgetData(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Filament\Server\Resources\DatabaseResource\Pages;
|
namespace App\Filament\Server\Resources\DatabaseResource\Pages;
|
||||||
|
|
||||||
use App\Facades\Activity;
|
|
||||||
use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction;
|
use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction;
|
||||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
||||||
use App\Filament\Server\Resources\DatabaseResource;
|
use App\Filament\Server\Resources\DatabaseResource;
|
||||||
@ -82,12 +81,7 @@ class ListDatabases extends ListRecords
|
|||||||
ViewAction::make()
|
ViewAction::make()
|
||||||
->modalHeading(fn (Database $database) => 'Viewing ' . $database->database),
|
->modalHeading(fn (Database $database) => 'Viewing ' . $database->database),
|
||||||
DeleteAction::make()
|
DeleteAction::make()
|
||||||
->after(function (Database $database) {
|
->using(fn (Database $database, DatabaseManagementService $service) => $service->delete($database)),
|
||||||
Activity::event('server:database.delete')
|
|
||||||
->subject($database)
|
|
||||||
->property('name', $database->database)
|
|
||||||
->log();
|
|
||||||
}),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,8 @@ use Filament\Resources\Pages\Page;
|
|||||||
use Filament\Resources\Pages\PageRegistration;
|
use Filament\Resources\Pages\PageRegistration;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Schemas\Schema;
|
||||||
use Filament\Support\Enums\Alignment;
|
use Filament\Support\Enums\Alignment;
|
||||||
|
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||||
|
use Illuminate\Http\Client\ConnectionException;
|
||||||
use Illuminate\Routing\Route;
|
use Illuminate\Routing\Route;
|
||||||
use Illuminate\Support\Facades\Route as RouteFacade;
|
use Illuminate\Support\Facades\Route as RouteFacade;
|
||||||
use Livewire\Attributes\Locked;
|
use Livewire\Attributes\Locked;
|
||||||
@ -118,37 +120,37 @@ class EditFiles extends Page
|
|||||||
// TODO MonacoEditor::make('editor')
|
// TODO MonacoEditor::make('editor')
|
||||||
// ->hiddenLabel()
|
// ->hiddenLabel()
|
||||||
// ->showPlaceholder(false)
|
// ->showPlaceholder(false)
|
||||||
// ->default(function () {
|
|
||||||
// try {
|
|
||||||
// return $this->getDaemonFileRepository()->getContent($this->path, config('panel.files.max_edit_size'));
|
|
||||||
// } catch (FileSizeTooLargeException) {
|
|
||||||
// AlertBanner::make()
|
|
||||||
// ->title('File too large!')
|
|
||||||
// ->body('<code>' . $this->path . '</code> Max is ' . convert_bytes_to_readable(config('panel.files.max_edit_size')))
|
|
||||||
// ->danger()
|
// ->danger()
|
||||||
|
// ->body('<code>' . $this->path . '</code> Max is ' . convert_bytes_to_readable(config('panel.files.max_edit_size')))
|
||||||
|
// ->title('File too large!')
|
||||||
|
// AlertBanner::make()
|
||||||
|
// } catch (FileSizeTooLargeException) {
|
||||||
|
// try {
|
||||||
|
// ->default(function () {
|
||||||
|
// return $this->getDaemonFileRepository()->getContent($this->path, config('panel.files.max_edit_size'));
|
||||||
// ->closable()
|
// ->closable()
|
||||||
// ->send();
|
// ->send();
|
||||||
//
|
//
|
||||||
// $this->redirect(ListFiles::getUrl());
|
|
||||||
// } catch (FileNotFoundException) {
|
// } catch (FileNotFoundException) {
|
||||||
// AlertBanner::make()
|
// AlertBanner::make()
|
||||||
|
// $this->redirect(ListFiles::getUrl());
|
||||||
// ->title('File Not found!')
|
// ->title('File Not found!')
|
||||||
// ->body('<code>' . $this->path . '</code>')
|
|
||||||
// ->danger()
|
// ->danger()
|
||||||
// ->closable()
|
// ->body('<code>' . $this->path . '</code>')
|
||||||
// ->send();
|
// ->send();
|
||||||
|
// ->closable()
|
||||||
//
|
//
|
||||||
// $this->redirect(ListFiles::getUrl());
|
// $this->redirect(ListFiles::getUrl());
|
||||||
// } catch (FileNotEditableException) {
|
// } catch (FileNotEditableException) {
|
||||||
// AlertBanner::make()
|
|
||||||
// ->title('Could not edit directory!')
|
// ->title('Could not edit directory!')
|
||||||
|
// AlertBanner::make()
|
||||||
// ->body('<code>' . $this->path . '</code>')
|
// ->body('<code>' . $this->path . '</code>')
|
||||||
// ->danger()
|
|
||||||
// ->closable()
|
// ->closable()
|
||||||
// ->send();
|
// ->send();
|
||||||
|
// ->danger()
|
||||||
//
|
//
|
||||||
// $this->redirect(ListFiles::getUrl());
|
|
||||||
// }
|
// }
|
||||||
|
// $this->redirect(ListFiles::getUrl());
|
||||||
// })
|
// })
|
||||||
// ->language(fn (Get $get) => $get('lang'))
|
// ->language(fn (Get $get) => $get('lang'))
|
||||||
// ->view('filament.plugins.monaco-editor'),
|
// ->view('filament.plugins.monaco-editor'),
|
||||||
|
@ -11,7 +11,6 @@ use App\Models\Server;
|
|||||||
use App\Repositories\Daemon\DaemonFileRepository;
|
use App\Repositories\Daemon\DaemonFileRepository;
|
||||||
use App\Filament\Components\Tables\Columns\BytesColumn;
|
use App\Filament\Components\Tables\Columns\BytesColumn;
|
||||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
||||||
use App\Livewire\AlertBanner;
|
|
||||||
use Filament\Actions\Action as HeaderAction;
|
use Filament\Actions\Action as HeaderAction;
|
||||||
use Filament\Facades\Filament;
|
use Filament\Facades\Filament;
|
||||||
use Filament\Forms\Components\CheckboxList;
|
use Filament\Forms\Components\CheckboxList;
|
||||||
@ -36,7 +35,6 @@ use Filament\Actions\EditAction;
|
|||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
use Illuminate\Http\Client\ConnectionException;
|
|
||||||
use Illuminate\Http\UploadedFile;
|
use Illuminate\Http\UploadedFile;
|
||||||
use Illuminate\Routing\Route;
|
use Illuminate\Routing\Route;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
@ -53,24 +51,11 @@ class ListFiles extends ListRecords
|
|||||||
|
|
||||||
private DaemonFileRepository $fileRepository;
|
private DaemonFileRepository $fileRepository;
|
||||||
|
|
||||||
private bool $isDisabled = false;
|
|
||||||
|
|
||||||
public function mount(?string $path = null): void
|
public function mount(?string $path = null): void
|
||||||
{
|
{
|
||||||
parent::mount();
|
parent::mount();
|
||||||
|
|
||||||
$this->path = $path ?? '/';
|
$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
|
public function getBreadcrumbs(): array
|
||||||
@ -130,21 +115,18 @@ class ListFiles extends ListRecords
|
|||||||
->actions([
|
->actions([
|
||||||
Action::make('view')
|
Action::make('view')
|
||||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server))
|
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server))
|
||||||
->disabled($this->isDisabled)
|
|
||||||
->label('Open')
|
->label('Open')
|
||||||
->icon('tabler-eye')
|
->icon('tabler-eye')
|
||||||
->visible(fn (File $file) => $file->is_directory)
|
->visible(fn (File $file) => $file->is_directory)
|
||||||
->url(fn (File $file) => self::getUrl(['path' => join_paths($this->path, $file->name)])),
|
->url(fn (File $file) => self::getUrl(['path' => join_paths($this->path, $file->name)])),
|
||||||
EditAction::make('edit')
|
EditAction::make('edit')
|
||||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server))
|
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server))
|
||||||
->disabled($this->isDisabled)
|
|
||||||
->icon('tabler-edit')
|
->icon('tabler-edit')
|
||||||
->visible(fn (File $file) => $file->canEdit())
|
->visible(fn (File $file) => $file->canEdit())
|
||||||
->url(fn (File $file) => EditFiles::getUrl(['path' => join_paths($this->path, $file->name)])),
|
->url(fn (File $file) => EditFiles::getUrl(['path' => join_paths($this->path, $file->name)])),
|
||||||
ActionGroup::make([
|
ActionGroup::make([
|
||||||
Action::make('rename')
|
Action::make('rename')
|
||||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||||
->disabled($this->isDisabled)
|
|
||||||
->label('Rename')
|
->label('Rename')
|
||||||
->icon('tabler-forms')
|
->icon('tabler-forms')
|
||||||
->schema([
|
->schema([
|
||||||
@ -173,7 +155,6 @@ class ListFiles extends ListRecords
|
|||||||
}),
|
}),
|
||||||
Action::make('copy')
|
Action::make('copy')
|
||||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
||||||
->disabled($this->isDisabled)
|
|
||||||
->label('Copy')
|
->label('Copy')
|
||||||
->icon('tabler-copy')
|
->icon('tabler-copy')
|
||||||
->visible(fn (File $file) => $file->is_file)
|
->visible(fn (File $file) => $file->is_file)
|
||||||
@ -193,14 +174,12 @@ class ListFiles extends ListRecords
|
|||||||
}),
|
}),
|
||||||
Action::make('download')
|
Action::make('download')
|
||||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server))
|
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server))
|
||||||
->disabled($this->isDisabled)
|
|
||||||
->label('Download')
|
->label('Download')
|
||||||
->icon('tabler-download')
|
->icon('tabler-download')
|
||||||
->visible(fn (File $file) => $file->is_file)
|
->visible(fn (File $file) => $file->is_file)
|
||||||
->url(fn (File $file) => DownloadFiles::getUrl(['path' => join_paths($this->path, $file->name)]), true),
|
->url(fn (File $file) => DownloadFiles::getUrl(['path' => join_paths($this->path, $file->name)]), true),
|
||||||
Action::make('move')
|
Action::make('move')
|
||||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||||
->disabled($this->isDisabled)
|
|
||||||
->label('Move')
|
->label('Move')
|
||||||
->icon('tabler-replace')
|
->icon('tabler-replace')
|
||||||
->schema([
|
->schema([
|
||||||
@ -236,7 +215,6 @@ class ListFiles extends ListRecords
|
|||||||
}),
|
}),
|
||||||
Action::make('permissions')
|
Action::make('permissions')
|
||||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||||
->disabled($this->isDisabled)
|
|
||||||
->label('Permissions')
|
->label('Permissions')
|
||||||
->icon('tabler-license')
|
->icon('tabler-license')
|
||||||
->schema([
|
->schema([
|
||||||
@ -293,7 +271,6 @@ class ListFiles extends ListRecords
|
|||||||
}),
|
}),
|
||||||
Action::make('archive')
|
Action::make('archive')
|
||||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
|
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
|
||||||
->disabled($this->isDisabled)
|
|
||||||
->label('Archive')
|
->label('Archive')
|
||||||
->icon('tabler-archive')
|
->icon('tabler-archive')
|
||||||
->schema([
|
->schema([
|
||||||
@ -321,7 +298,6 @@ class ListFiles extends ListRecords
|
|||||||
}),
|
}),
|
||||||
Action::make('unarchive')
|
Action::make('unarchive')
|
||||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
|
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
|
||||||
->disabled($this->isDisabled)
|
|
||||||
->label('Unarchive')
|
->label('Unarchive')
|
||||||
->icon('tabler-archive')
|
->icon('tabler-archive')
|
||||||
->visible(fn (File $file) => $file->isArchive())
|
->visible(fn (File $file) => $file->isArchive())
|
||||||
@ -343,7 +319,6 @@ class ListFiles extends ListRecords
|
|||||||
]),
|
]),
|
||||||
DeleteAction::make()
|
DeleteAction::make()
|
||||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server))
|
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server))
|
||||||
->disabled($this->isDisabled)
|
|
||||||
->label('')
|
->label('')
|
||||||
->icon('tabler-trash')
|
->icon('tabler-trash')
|
||||||
->requiresConfirmation()
|
->requiresConfirmation()
|
||||||
@ -358,83 +333,77 @@ class ListFiles extends ListRecords
|
|||||||
->log();
|
->log();
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
->bulkActions([
|
$location = rtrim($data['location'], '/');
|
||||||
BulkActionGroup::make([
|
])
|
||||||
BulkAction::make('move')
|
->action(function (Collection $files, $data) {
|
||||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
->content(fn (Get $get) => resolve_path('./' . join_paths($this->path, $get('location') ?? ''))),
|
||||||
->disabled($this->isDisabled)
|
Placeholder::make('new_location')
|
||||||
->schema([
|
->live(),
|
||||||
TextInput::make('location')
|
->required()
|
||||||
->label('Directory')
|
->hint('Enter the new directory, relative to the current directory.')
|
||||||
->hint('Enter the new directory, relative to the current directory.')
|
->label('Directory')
|
||||||
->required()
|
->form([
|
||||||
->live(),
|
TextInput::make('location')
|
||||||
TextEntry::make('new_location')
|
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||||
->state(fn (Get $get) => resolve_path('./' . join_paths($this->path, $get('location') ?? ''))),
|
->groupedBulkActions([
|
||||||
])
|
BulkAction::make('move')
|
||||||
->action(function (Collection $files, $data) {
|
|
||||||
$location = rtrim($data['location'], '/');
|
|
||||||
|
|
||||||
$files = $files->map(fn ($file) => ['to' => join_paths($location, $file['name']), 'from' => $file['name']])->toArray();
|
$files = $files->map(fn ($file) => ['to' => join_paths($location, $file['name']), 'from' => $file['name']])->toArray();
|
||||||
$this->getDaemonFileRepository()
|
$this->getDaemonFileRepository()->renameFiles($this->path, $files);
|
||||||
->renameFiles($this->path, $files);
|
|
||||||
|
|
||||||
Activity::event('server:file.rename')
|
Activity::event('server:file.rename')
|
||||||
->property('directory', $this->path)
|
->property('directory', $this->path)
|
||||||
->property('files', $files)
|
->property('files', $files)
|
||||||
->log();
|
->log();
|
||||||
|
|
||||||
Notification::make()
|
$files = $files->map(fn ($file) => $file['name'])->toArray();
|
||||||
->title(count($files) . ' Files were moved to ' . resolve_path(join_paths($this->path, $location)))
|
->action(function ($data, Collection $files) {
|
||||||
->success()
|
])
|
||||||
->send();
|
->suffix('.tar.gz'),
|
||||||
}),
|
->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z')
|
||||||
BulkAction::make('archive')
|
->label('Archive name')
|
||||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
|
->form([
|
||||||
->disabled($this->isDisabled)
|
TextInput::make('name')
|
||||||
->schema([
|
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
|
||||||
TextInput::make('name')
|
BulkAction::make('archive')
|
||||||
->label('Archive name')
|
->success()
|
||||||
->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z')
|
->title(count($files) . ' Files were moved to ' . resolve_path(join_paths($this->path, $location)))
|
||||||
->suffix('.tar.gz'),
|
Notification::make()
|
||||||
])
|
}),
|
||||||
->action(function ($data, Collection $files) {
|
->send();
|
||||||
$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')
|
Activity::event('server:file.compress')
|
||||||
->property('name', $archive['name'])
|
->property('name', $archive['name'])
|
||||||
->property('directory', $this->path)
|
->property('directory', $this->path)
|
||||||
->property('files', $files)
|
->property('files', $files)
|
||||||
->log();
|
->log();
|
||||||
|
|
||||||
Notification::make()
|
Notification::make()
|
||||||
->title('Archive created')
|
->title('Archive created')
|
||||||
->body($archive['name'])
|
->body($archive['name'])
|
||||||
->success()
|
->success()
|
||||||
->send();
|
->send();
|
||||||
|
|
||||||
return redirect(ListFiles::getUrl(['path' => $this->path]));
|
return redirect(ListFiles::getUrl(['path' => $this->path]));
|
||||||
}),
|
}),
|
||||||
DeleteBulkAction::make()
|
DeleteBulkAction::make()
|
||||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server))
|
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server))
|
||||||
->disabled($this->isDisabled)
|
->action(function (Collection $files) {
|
||||||
->action(function (Collection $files) {
|
$files = $files->map(fn ($file) => $file['name'])->toArray();
|
||||||
$files = $files->map(fn ($file) => $file['name'])->toArray();
|
$this->getDaemonFileRepository()->deleteFiles($this->path, $files);
|
||||||
$this->getDaemonFileRepository()->deleteFiles($this->path, $files);
|
|
||||||
|
|
||||||
Activity::event('server:file.delete')
|
Activity::event('server:file.delete')
|
||||||
->property('directory', $this->path)
|
->property('directory', $this->path)
|
||||||
->property('files', $files)
|
->property('files', $files)
|
||||||
->log();
|
->log();
|
||||||
|
|
||||||
Notification::make()
|
Notification::make()
|
||||||
->title(count($files) . ' Files deleted.')
|
->title(count($files) . ' Files deleted.')
|
||||||
->success()
|
->success()
|
||||||
->send();
|
->send();
|
||||||
}),
|
}),
|
||||||
]),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -446,7 +415,6 @@ class ListFiles extends ListRecords
|
|||||||
return [
|
return [
|
||||||
HeaderAction::make('new_file')
|
HeaderAction::make('new_file')
|
||||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
||||||
->disabled($this->isDisabled)
|
|
||||||
->label('New File')
|
->label('New File')
|
||||||
->color('gray')
|
->color('gray')
|
||||||
->keyBindings('')
|
->keyBindings('')
|
||||||
@ -478,7 +446,6 @@ class ListFiles extends ListRecords
|
|||||||
]),
|
]),
|
||||||
HeaderAction::make('new_folder')
|
HeaderAction::make('new_folder')
|
||||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
||||||
->disabled($this->isDisabled)
|
|
||||||
->label('New Folder')
|
->label('New Folder')
|
||||||
->color('gray')
|
->color('gray')
|
||||||
->action(function ($data) {
|
->action(function ($data) {
|
||||||
@ -495,7 +462,6 @@ class ListFiles extends ListRecords
|
|||||||
]),
|
]),
|
||||||
HeaderAction::make('upload')
|
HeaderAction::make('upload')
|
||||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
||||||
->disabled($this->isDisabled)
|
|
||||||
->label('Upload')
|
->label('Upload')
|
||||||
->action(function ($data) {
|
->action(function ($data) {
|
||||||
if (count($data['files']) > 0 && !isset($data['url'])) {
|
if (count($data['files']) > 0 && !isset($data['url'])) {
|
||||||
@ -545,7 +511,6 @@ class ListFiles extends ListRecords
|
|||||||
]),
|
]),
|
||||||
HeaderAction::make('search')
|
HeaderAction::make('search')
|
||||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server))
|
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server))
|
||||||
->disabled($this->isDisabled)
|
|
||||||
->label('Global Search')
|
->label('Global Search')
|
||||||
->modalSubmitActionLabel('Search')
|
->modalSubmitActionLabel('Search')
|
||||||
->schema([
|
->schema([
|
||||||
|
@ -36,26 +36,22 @@ class SettingsController extends ClientApiController
|
|||||||
$name = $request->input('name');
|
$name = $request->input('name');
|
||||||
$description = $request->has('description') ? (string) $request->input('description') : $server->description;
|
$description = $request->has('description') ? (string) $request->input('description') : $server->description;
|
||||||
|
|
||||||
$server->name = $name;
|
|
||||||
|
|
||||||
if (config('panel.editable_server_descriptions')) {
|
|
||||||
$server->description = $description;
|
|
||||||
}
|
|
||||||
|
|
||||||
$server->save();
|
|
||||||
|
|
||||||
if ($server->name !== $name) {
|
if ($server->name !== $name) {
|
||||||
Activity::event('server:settings.rename')
|
Activity::event('server:settings.rename')
|
||||||
->property(['old' => $server->name, 'new' => $name])
|
->property(['old' => $server->name, 'new' => $name])
|
||||||
->log();
|
->log();
|
||||||
|
$server->name = $name;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($server->description !== $description) {
|
if ($server->description !== $description && config('panel.editable_server_descriptions')) {
|
||||||
Activity::event('server:settings.description')
|
Activity::event('server:settings.description')
|
||||||
->property(['old' => $server->description, 'new' => $description])
|
->property(['old' => $server->description, 'new' => $description])
|
||||||
->log();
|
->log();
|
||||||
|
$server->description = $description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$server->save();
|
||||||
|
|
||||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ namespace App\Models;
|
|||||||
use App\Contracts\Validatable;
|
use App\Contracts\Validatable;
|
||||||
use App\Exceptions\Service\Egg\HasChildrenException;
|
use App\Exceptions\Service\Egg\HasChildrenException;
|
||||||
use App\Exceptions\Service\HasActiveServersException;
|
use App\Exceptions\Service\HasActiveServersException;
|
||||||
|
use App\Extensions\Features\FeatureProvider;
|
||||||
use App\Traits\HasValidation;
|
use App\Traits\HasValidation;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
@ -70,19 +71,6 @@ class Egg extends Model implements Validatable
|
|||||||
*/
|
*/
|
||||||
public const EXPORT_VERSION = 'PLCN_v1';
|
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.
|
* Fields that are not mass assignable.
|
||||||
*/
|
*/
|
||||||
@ -172,6 +160,12 @@ 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
|
* Returns the install script for the egg; if egg is copying from another
|
||||||
* it will return the copied script.
|
* it will return the copied script.
|
||||||
|
@ -153,16 +153,10 @@ class File extends Model
|
|||||||
try {
|
try {
|
||||||
$fileRepository = (new DaemonFileRepository())->setServer(self::$server);
|
$fileRepository = (new DaemonFileRepository())->setServer(self::$server);
|
||||||
|
|
||||||
$contents = [];
|
if (!is_null(self::$searchTerm)) {
|
||||||
|
$contents = cache()->remember('file_search_' . self::$path . '_' . self::$searchTerm, now()->addMinute(), fn () => $fileRepository->search(self::$searchTerm, self::$path));
|
||||||
try {
|
} else {
|
||||||
if (!is_null(self::$searchTerm)) {
|
$contents = $fileRepository->getDirectory(self::$path ?? '/');
|
||||||
$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'])) {
|
if (isset($contents['error'])) {
|
||||||
@ -199,8 +193,12 @@ class File extends Model
|
|||||||
$message = $message->after('cURL error 7: ')->before(' after ');
|
$message = $message->after('cURL error 7: ')->before(' after ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($exception instanceof ConnectionException) {
|
||||||
|
$message = str('Node connection failed');
|
||||||
|
}
|
||||||
|
|
||||||
AlertBanner::make()
|
AlertBanner::make()
|
||||||
->title('Could not load files')
|
->title('Could not load files!')
|
||||||
->body($message->toString())
|
->body($message->toString())
|
||||||
->danger()
|
->danger()
|
||||||
->send();
|
->send();
|
||||||
|
@ -4,12 +4,13 @@ namespace App\Models;
|
|||||||
|
|
||||||
use App\Contracts\Validatable;
|
use App\Contracts\Validatable;
|
||||||
use App\Traits\HasValidation;
|
use App\Traits\HasValidation;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
class Permission extends Model implements Validatable
|
class Permission extends Model implements Validatable
|
||||||
{
|
{
|
||||||
use HasValidation;
|
use HasFactory, HasValidation;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The resource name for this model when it is transformed into an
|
* The resource name for this model when it is transformed into an
|
||||||
|
@ -4,6 +4,7 @@ namespace App\Models;
|
|||||||
|
|
||||||
use App\Enums\RolePermissionModels;
|
use App\Enums\RolePermissionModels;
|
||||||
use App\Enums\RolePermissionPrefixes;
|
use App\Enums\RolePermissionPrefixes;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Spatie\Permission\Models\Role as BaseRole;
|
use Spatie\Permission\Models\Role as BaseRole;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -17,6 +18,8 @@ use Spatie\Permission\Models\Role as BaseRole;
|
|||||||
*/
|
*/
|
||||||
class Role extends BaseRole
|
class Role extends BaseRole
|
||||||
{
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
public const RESOURCE_NAME = 'role';
|
public const RESOURCE_NAME = 'role';
|
||||||
|
|
||||||
public const ROOT_ADMIN = 'Root Admin';
|
public const ROOT_ADMIN = 'Root Admin';
|
||||||
|
@ -15,6 +15,11 @@ use App\Extensions\Avatar\Providers\UiAvatarsProvider;
|
|||||||
use App\Extensions\OAuth\Providers\GitlabProvider;
|
use App\Extensions\OAuth\Providers\GitlabProvider;
|
||||||
use App\Models;
|
use App\Models;
|
||||||
use App\Extensions\Captcha\Providers\TurnstileProvider;
|
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\AuthentikProvider;
|
||||||
use App\Extensions\OAuth\Providers\CommonProvider;
|
use App\Extensions\OAuth\Providers\CommonProvider;
|
||||||
use App\Extensions\OAuth\Providers\DiscordProvider;
|
use App\Extensions\OAuth\Providers\DiscordProvider;
|
||||||
@ -121,6 +126,13 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
GravatarProvider::register();
|
GravatarProvider::register();
|
||||||
UiAvatarsProvider::register();
|
UiAvatarsProvider::register();
|
||||||
|
|
||||||
|
// Default Feature providers
|
||||||
|
GSLToken::register($app);
|
||||||
|
JavaVersion::register($app);
|
||||||
|
MinecraftEula::register($app);
|
||||||
|
PIDLimit::register($app);
|
||||||
|
SteamDiskSpace::register($app);
|
||||||
|
|
||||||
FilamentColor::register([
|
FilamentColor::register([
|
||||||
'danger' => Color::Red,
|
'danger' => Color::Red,
|
||||||
'gray' => Color::Zinc,
|
'gray' => Color::Zinc,
|
||||||
|
@ -118,11 +118,18 @@ class DatabaseManagementService
|
|||||||
*/
|
*/
|
||||||
public function delete(Database $database): ?bool
|
public function delete(Database $database): ?bool
|
||||||
{
|
{
|
||||||
$database->dropDatabase($database->database);
|
return $this->connection->transaction(function () use ($database) {
|
||||||
$database->dropUser($database->username, $database->remote);
|
$database->dropDatabase($database->database);
|
||||||
$database->flush();
|
$database->dropUser($database->username, $database->remote);
|
||||||
|
$database->flush();
|
||||||
|
|
||||||
return $database->delete();
|
Activity::event('server:database.delete')
|
||||||
|
->subject($database)
|
||||||
|
->property('name', $database->database)
|
||||||
|
->log();
|
||||||
|
|
||||||
|
return $database->delete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -101,6 +101,9 @@ class ServerConfigurationStructureService
|
|||||||
'egg' => [
|
'egg' => [
|
||||||
'id' => $server->egg->uuid,
|
'id' => $server->egg->uuid,
|
||||||
'file_denylist' => $server->egg->inherit_file_denylist,
|
'file_denylist' => $server->egg->inherit_file_denylist,
|
||||||
|
'features' => collect($server->egg->features())->mapWithKeys(fn ($feature) => [
|
||||||
|
$feature->getId() => $feature->getListeners(),
|
||||||
|
])->all(),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -46,6 +46,7 @@ class EggTransformer extends BaseTransformer
|
|||||||
'name' => $model->name,
|
'name' => $model->name,
|
||||||
'author' => $model->author,
|
'author' => $model->author,
|
||||||
'description' => $model->description,
|
'description' => $model->description,
|
||||||
|
'features' => $model->features,
|
||||||
// "docker_image" is deprecated, but left here to avoid breaking too many things at once
|
// "docker_image" is deprecated, but left here to avoid breaking too many things at once
|
||||||
// in external software. We'll remove it down the road once things have gotten the chance
|
// in external software. We'll remove it down the road once things have gotten the chance
|
||||||
// to upgrade to using "docker_images".
|
// to upgrade to using "docker_images".
|
||||||
|
@ -54,6 +54,8 @@
|
|||||||
"mockery/mockery": "^1.6.11",
|
"mockery/mockery": "^1.6.11",
|
||||||
"nunomaduro/collision": "^8.6",
|
"nunomaduro/collision": "^8.6",
|
||||||
"pestphp/pest": "^3.7",
|
"pestphp/pest": "^3.7",
|
||||||
|
"pestphp/pest-plugin-faker": "^3.0",
|
||||||
|
"pestphp/pest-plugin-livewire": "^3.0",
|
||||||
"spatie/laravel-ignition": "^2.9"
|
"spatie/laravel-ignition": "^2.9"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
|
133
composer.lock
generated
133
composer.lock
generated
@ -13857,6 +13857,137 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-04-16T22:59:48+00:00"
|
"time": "2025-04-16T22:59:48+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "pestphp/pest-plugin-faker",
|
||||||
|
"version": "v3.0.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/pestphp/pest-plugin-faker.git",
|
||||||
|
"reference": "48343e2806cfc12a042dead90ffff4a043167e3e"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/pestphp/pest-plugin-faker/zipball/48343e2806cfc12a042dead90ffff4a043167e3e",
|
||||||
|
"reference": "48343e2806cfc12a042dead90ffff4a043167e3e",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"fakerphp/faker": "^1.23.1",
|
||||||
|
"pestphp/pest": "^3.0.0",
|
||||||
|
"php": "^8.2"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"pestphp/pest-dev-tools": "^3.0.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"src/Faker.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Pest\\Faker\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"description": "The Pest Faker Plugin",
|
||||||
|
"keywords": [
|
||||||
|
"faker",
|
||||||
|
"framework",
|
||||||
|
"pest",
|
||||||
|
"php",
|
||||||
|
"plugin",
|
||||||
|
"test",
|
||||||
|
"testing",
|
||||||
|
"unit"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/pestphp/pest-plugin-faker/tree/v3.0.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nunomaduro",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.patreon.com/nunomaduro",
|
||||||
|
"type": "patreon"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2024-09-08T23:56:08+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "pestphp/pest-plugin-livewire",
|
||||||
|
"version": "v3.0.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/pestphp/pest-plugin-livewire.git",
|
||||||
|
"reference": "e2f2edb0a7d414d6837d87908a0e148256d3bf89"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/pestphp/pest-plugin-livewire/zipball/e2f2edb0a7d414d6837d87908a0e148256d3bf89",
|
||||||
|
"reference": "e2f2edb0a7d414d6837d87908a0e148256d3bf89",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"livewire/livewire": "^3.5.6",
|
||||||
|
"pestphp/pest": "^3.0.0",
|
||||||
|
"php": "^8.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"orchestra/testbench": "^9.4.0",
|
||||||
|
"pestphp/pest-dev-tools": "^3.0.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"src/Autoload.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Pest\\Livewire\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"description": "The Pest Livewire Plugin",
|
||||||
|
"keywords": [
|
||||||
|
"framework",
|
||||||
|
"livewire",
|
||||||
|
"pest",
|
||||||
|
"php",
|
||||||
|
"plugin",
|
||||||
|
"test",
|
||||||
|
"testing",
|
||||||
|
"unit"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/pestphp/pest-plugin-livewire/tree/v3.0.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nunomaduro",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.patreon.com/nunomaduro",
|
||||||
|
"type": "patreon"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2024-09-09T00:05:59+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "pestphp/pest-plugin-mutate",
|
"name": "pestphp/pest-plugin-mutate",
|
||||||
"version": "v3.0.5",
|
"version": "v3.0.5",
|
||||||
@ -16017,5 +16148,5 @@
|
|||||||
"platform-overrides": {
|
"platform-overrides": {
|
||||||
"php": "8.2"
|
"php": "8.2"
|
||||||
},
|
},
|
||||||
"plugin-api-version": "2.6.0"
|
"plugin-api-version": "2.3.0"
|
||||||
}
|
}
|
||||||
|
18
database/Factories/PermissionFactory.php
Normal file
18
database/Factories/PermissionFactory.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Permission;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
class PermissionFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = Permission::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
22
database/Factories/RoleFactory.php
Normal file
22
database/Factories/RoleFactory.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Role;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
|
class RoleFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = Role::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => $this->faker->name(),
|
||||||
|
'guard_name' => $this->faker->name(),
|
||||||
|
'created_at' => Carbon::now(),
|
||||||
|
'updated_at' => Carbon::now(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -4,11 +4,11 @@
|
|||||||
"version": "PLCN_v1",
|
"version": "PLCN_v1",
|
||||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-sponge--sponge-vanilla.json"
|
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-sponge--sponge-vanilla.json"
|
||||||
},
|
},
|
||||||
"exported_at": "2025-03-18T12:35:50+00:00",
|
"exported_at": "2025-04-25T06:05:10+00:00",
|
||||||
"name": "Sponge (SpongeVanilla)",
|
"name": "Sponge",
|
||||||
"author": "panel@example.com",
|
"author": "panel@example.com",
|
||||||
"uuid": "f0d2f88f-1ff3-42a0-b03f-ac44c5571e6d",
|
"uuid": "f0d2f88f-1ff3-42a0-b03f-ac44c5571e6d",
|
||||||
"description": "SpongeVanilla is the SpongeAPI implementation for Vanilla Minecraft.",
|
"description": "A community-driven open source Minecraft: Java Edition modding platform.",
|
||||||
"tags": [
|
"tags": [
|
||||||
"minecraft"
|
"minecraft"
|
||||||
],
|
],
|
||||||
@ -34,28 +34,42 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"installation": {
|
"installation": {
|
||||||
"script": "#!\/bin\/ash\r\n# Sponge Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n\r\ncd \/mnt\/server\r\n\r\ncurl -sSL \"https:\/\/repo.spongepowered.org\/maven\/org\/spongepowered\/spongevanilla\/${SPONGE_VERSION}\/spongevanilla-${SPONGE_VERSION}.jar\" -o ${SERVER_JARFILE}",
|
"script": "#!\/bin\/ash\r\n# Sponge Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n\r\ncd \/mnt\/server\r\n\r\nif [ $MINECRAFT_VERSION = 'latest' ] || [ -z $MINECRAFT_VERSION ]; then\r\n TARGET_VERSION_JSON=$(curl -sSL https:\/\/dl-api.spongepowered.org\/v2\/groups\/org.spongepowered\/artifacts\/${SPONGE_TYPE}\/latest?recommended=true)\r\n if [ -z \"${TARGET_VERSION_JSON}\" ]; then\r\n echo -e \"Failed to find latest recommended version!\"\r\n exit 1\r\n fi\r\n echo -e \"Found latest version for ${SPONGE_TYPE}\"\r\nelse\r\n if [ $SPONGE_TYPE = 'spongevanilla' ]; then \r\n VERSIONS_JSON=$(curl -sSL https:\/\/dl-api.spongepowered.org\/v2\/groups\/org.spongepowered\/artifacts\/${SPONGE_TYPE}\/versions?tags=,minecraft:${MINECRAFT_VERSION}&offset=0&limit=1)\r\n else\r\n FORGETAG='forge'\r\n if [ $SPONGE_TYPE = 'spongeneo' ]; then\r\n FORGETAG='neoforge'\r\n fi\r\n VERSIONS_JSON=$(curl -sSL https:\/\/dl-api.spongepowered.org\/v2\/groups\/org.spongepowered\/artifacts\/${SPONGE_TYPE}\/versions?tags=,minecraft:${MINECRAFT_VERSION},${FORGETAG}:${FORGE_VERSION}&offset=0&limit=1)\r\n fi\r\n \r\n if [ -z \"${VERSIONS_JSON}\" ]; then\r\n echo -e \"Failed to find recommended ${MINECRAFT_VERSION} version for ${SPONGE_TYPE} ${FORGE_VERSION}!\"\r\n exit 1\r\n fi\r\n \r\n VERSION_KEY=$(echo $VERSIONS_JSON | jq -r '.artifacts | to_entries[0].key')\r\n TARGET_VERSION_JSON=$(curl -sSL https:\/\/dl-api.spongepowered.org\/v2\/groups\/org.spongepowered\/artifacts\/${SPONGE_TYPE}\/versions\/${VERSION_KEY})\r\n \r\n if [ -z \"${TARGET_VERSION_JSON}\" ]; then\r\n echo -e \"Failed to find ${VERSION_KEY} for ${SPONGE_TYPE} ${FORGE_VERSION}!\"\r\n exit 1\r\n fi\r\n\r\n echo -e \"Found ${MINECRAFT_VERSION} for ${SPONGE_TYPE}\"\r\nfi\r\n\r\nTARGET_VERSION=`echo $TARGET_VERSION_JSON | jq '.assets[] | select(.classifier == \"universal\")'`\r\nif [ -z \"${TARGET_VERSION}\" ]; then\r\n TARGET_VERSION=`echo $TARGET_VERSION_JSON | jq '.assets[] | select(.classifier == \"\" and .extension == \"jar\")'`\r\nfi\r\n\r\nif [ -z \"${TARGET_VERSION}\" ]; then\r\n echo -e \"Failed to get download url data from the selected version\"\r\n exit 1\r\nfi\r\n\r\nSPONGE_URL=$(echo $TARGET_VERSION | jq -r '.downloadUrl')\r\nCHECKSUM=$(echo $TARGET_VERSION | jq -r '.md5')\r\necho -e \"Found file at ${SPONGE_URL} with checksum ${CHECKSUM}\"\r\n\r\necho -e \"running: curl -o ${SERVER_JARFILE} ${SPONGE_URL}\"\r\ncurl -o ${SERVER_JARFILE} ${SPONGE_URL}\r\n\r\nif [ $(basename $(md5sum ${SERVER_JARFILE})) = ${CHECKSUM} ] ; then\r\n echo \"Checksum passed\"\r\nelse\r\n echo \"Checksum failed\"\r\nfi\r\n\r\necho -e \"Install Complete\"",
|
||||||
"container": "ghcr.io\/parkervcp\/installers:alpine",
|
"container": "ghcr.io\/parkervcp\/installers:alpine",
|
||||||
"entrypoint": "ash"
|
"entrypoint": "ash"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"variables": [
|
"variables": [
|
||||||
{
|
{
|
||||||
"name": "Sponge Version",
|
"sort": 3,
|
||||||
"description": "The version of SpongeVanilla to download and use.",
|
"name": "Forge\/Neoforge Version",
|
||||||
"env_variable": "SPONGE_VERSION",
|
"description": "The modding api target version if set to `spongeforge` or `spongeneo`. Leave blank if using `spongevanilla`",
|
||||||
"default_value": "1.12.2-7.3.0",
|
"env_variable": "FORGE_VERSION",
|
||||||
|
"default_value": "",
|
||||||
|
"user_viewable": true,
|
||||||
|
"user_editable": true,
|
||||||
|
"rules": [
|
||||||
|
"string"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sort": 1,
|
||||||
|
"name": "Minecraft Version",
|
||||||
|
"description": "The version of Minecraft to target. Use \"latest\" to install the latest version. Go to Settings > Reinstall Server to apply.",
|
||||||
|
"env_variable": "MINECRAFT_VERSION",
|
||||||
|
"default_value": "latest",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": [
|
"rules": [
|
||||||
"required",
|
"required",
|
||||||
"regex:\/^([a-zA-Z0-9.\\-_]+)$\/"
|
"string",
|
||||||
],
|
"between:3,15"
|
||||||
"sort": 1
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"sort": 4,
|
||||||
"name": "Server Jar File",
|
"name": "Server Jar File",
|
||||||
"description": "The name of the Jarfile to use when running SpongeVanilla.",
|
"description": "The name of the Jarfile to use when running Sponge.",
|
||||||
"env_variable": "SERVER_JARFILE",
|
"env_variable": "SERVER_JARFILE",
|
||||||
"default_value": "server.jar",
|
"default_value": "server.jar",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
@ -63,8 +77,20 @@
|
|||||||
"rules": [
|
"rules": [
|
||||||
"required",
|
"required",
|
||||||
"regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
|
"regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
|
||||||
],
|
]
|
||||||
"sort": 2
|
},
|
||||||
|
{
|
||||||
|
"sort": 2,
|
||||||
|
"name": "Sponge Type",
|
||||||
|
"description": "SpongeVanilla if you are only using Sponge plugins.\nSpongeForge when using Forge mods and Sponge plugins.\nSpongeNeo when using NeoForge mods and Sponge plugins.",
|
||||||
|
"env_variable": "SPONGE_TYPE",
|
||||||
|
"default_value": "spongevanilla",
|
||||||
|
"user_viewable": true,
|
||||||
|
"user_editable": true,
|
||||||
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"in:spongevanilla,spongeforge,spongeneo"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ else
|
|||||||
echo -e "APP_INSTALLED=false" >> /pelican-data/.env
|
echo -e "APP_INSTALLED=false" >> /pelican-data/.env
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir /pelican-data/database /var/www/html/storage/logs/supervisord 2>/dev/null
|
mkdir -p /pelican-data/database /pelican-data/storage/avatars /var/www/html/storage/logs/supervisord 2>/dev/null
|
||||||
|
|
||||||
if ! grep -q "APP_KEY=" .env || grep -q "APP_KEY=$" .env; then
|
if ! grep -q "APP_KEY=" .env || grep -q "APP_KEY=$" .env; then
|
||||||
echo "Generating APP_KEY..."
|
echo "Generating APP_KEY..."
|
||||||
|
@ -129,6 +129,9 @@
|
|||||||
case 'install output':
|
case 'install output':
|
||||||
handleConsoleOutput(args[0]);
|
handleConsoleOutput(args[0]);
|
||||||
break;
|
break;
|
||||||
|
case 'feature match':
|
||||||
|
Livewire.dispatch('mount-feature', { data: args[0] });
|
||||||
|
break;
|
||||||
case 'status':
|
case 'status':
|
||||||
handlePowerChangeEvent(args[0]);
|
handlePowerChangeEvent(args[0]);
|
||||||
|
|
||||||
|
@ -4,4 +4,7 @@
|
|||||||
:data="$this->getWidgetData()"
|
:data="$this->getWidgetData()"
|
||||||
:widgets="$this->getVisibleWidgets()"
|
:widgets="$this->getVisibleWidgets()"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<x-filament-actions::modals />
|
||||||
|
|
||||||
</x-filament-panels::page>
|
</x-filament-panels::page>
|
||||||
|
179
tests/Feature/SettingsControllerTest.php
Normal file
179
tests/Feature/SettingsControllerTest.php
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Enums\ServerState;
|
||||||
|
use App\Http\Controllers\Api\Client\Servers\SettingsController;
|
||||||
|
use App\Models\Permission;
|
||||||
|
use App\Repositories\Daemon\DaemonServerRepository;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
pest()->group('API');
|
||||||
|
|
||||||
|
covers(SettingsController::class);
|
||||||
|
|
||||||
|
it('server name cannot be changed', function () {
|
||||||
|
[$user, $server] = generateTestAccount([Permission::ACTION_WEBSOCKET_CONNECT]);
|
||||||
|
$originalName = $server->name;
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->post("/api/client/servers/$server->uuid/settings/rename", [
|
||||||
|
'name' => 'Test Server Name',
|
||||||
|
])
|
||||||
|
->assertStatus(Response::HTTP_FORBIDDEN);
|
||||||
|
|
||||||
|
$server = $server->refresh();
|
||||||
|
expect()->toLogActivities(0)
|
||||||
|
->and($server->name)->toBe($originalName);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('server description can be changed', function () {
|
||||||
|
[$user, $server] = generateTestAccount([Permission::ACTION_SETTINGS_DESCRIPTION]);
|
||||||
|
$originalDescription = $server->description;
|
||||||
|
|
||||||
|
$newDescription = 'Test Server Description';
|
||||||
|
$this->actingAs($user)
|
||||||
|
->post("/api/client/servers/$server->uuid/settings/description", [
|
||||||
|
'description' => $newDescription,
|
||||||
|
])
|
||||||
|
->assertStatus(Response::HTTP_NO_CONTENT);
|
||||||
|
|
||||||
|
$server = $server->refresh();
|
||||||
|
$logged = \App\Models\ActivityLog::first();
|
||||||
|
expect()->toLogActivities(1)
|
||||||
|
->and($logged->properties['old'])->toBe($originalDescription)
|
||||||
|
->and($logged->properties['new'])->toBe($newDescription)
|
||||||
|
->and($server->description)->not()->toBe($originalDescription);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('server description cannot be changed', function () {
|
||||||
|
[$user, $server] = generateTestAccount([Permission::ACTION_SETTINGS_DESCRIPTION]);
|
||||||
|
Config::set('panel.editable_server_descriptions', false);
|
||||||
|
$originalDescription = $server->description;
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->post("/api/client/servers/$server->uuid/settings/description", [
|
||||||
|
'description' => 'Test Description',
|
||||||
|
])
|
||||||
|
->assertStatus(Response::HTTP_NO_CONTENT);
|
||||||
|
|
||||||
|
$server = $server->refresh();
|
||||||
|
expect()->toLogActivities(0)
|
||||||
|
->and($server->description)->toBe($originalDescription);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('server name can be changed', function () {
|
||||||
|
[$user, $server] = generateTestAccount([Permission::ACTION_WEBSOCKET_CONNECT, Permission::ACTION_SETTINGS_RENAME]);
|
||||||
|
$originalName = $server->name;
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->post("/api/client/servers/$server->uuid/settings/rename", [
|
||||||
|
'name' => 'Test Server Name',
|
||||||
|
])
|
||||||
|
->assertStatus(Response::HTTP_NO_CONTENT);
|
||||||
|
|
||||||
|
$server = $server->refresh();
|
||||||
|
expect()->toLogActivities(1)
|
||||||
|
->and($server->name)->not()->toBe($originalName);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unauthorized user cannot change docker image in use by server', function () {
|
||||||
|
[$user, $server] = generateTestAccount([Permission::ACTION_WEBSOCKET_CONNECT]);
|
||||||
|
$originalImage = $server->image;
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->put("/api/client/servers/$server->uuid/settings/docker-image", [
|
||||||
|
'docker_image' => 'ghcr.io/pelican-dev/yolks:java_21',
|
||||||
|
])
|
||||||
|
->assertStatus(Response::HTTP_FORBIDDEN);
|
||||||
|
|
||||||
|
$server = $server->refresh();
|
||||||
|
expect()->toLogActivities(0)
|
||||||
|
->and($server->image)->toBe($originalImage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cannot change docker image to image not allowed by egg', function () {
|
||||||
|
|
||||||
|
[$user, $server] = generateTestAccount([Permission::ACTION_STARTUP_DOCKER_IMAGE]);
|
||||||
|
$server->image = 'ghcr.io/parkervcp/yolks:java_17';
|
||||||
|
$server->save();
|
||||||
|
|
||||||
|
$newImage = 'ghcr.io/parkervcp/fake:image';
|
||||||
|
|
||||||
|
$server = $server->refresh();
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->putJson("/api/client/servers/$server->uuid/settings/docker-image", [
|
||||||
|
'docker_image' => $newImage,
|
||||||
|
])
|
||||||
|
->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||||
|
|
||||||
|
$server->refresh();
|
||||||
|
expect()->toLogActivities(0)
|
||||||
|
->and($server->image)->not()->toBe($newImage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can change docker image in use by server', function () {
|
||||||
|
[$user, $server] = generateTestAccount([Permission::ACTION_STARTUP_DOCKER_IMAGE]);
|
||||||
|
$oldImage = 'ghcr.io/parkervcp/yolks:java_17';
|
||||||
|
$server->image = $oldImage;
|
||||||
|
$server->save();
|
||||||
|
|
||||||
|
$newImage = 'ghcr.io/parkervcp/yolks:java_21';
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->putJson("/api/client/servers/$server->uuid/settings/docker-image", [
|
||||||
|
'docker_image' => $newImage,
|
||||||
|
])
|
||||||
|
->assertStatus(Response::HTTP_NO_CONTENT);
|
||||||
|
|
||||||
|
$server = $server->refresh();
|
||||||
|
|
||||||
|
$logItem = \App\Models\ActivityLog::first();
|
||||||
|
expect()->toLogActivities(1)
|
||||||
|
->and($logItem->properties['old'])->toBe($oldImage)
|
||||||
|
->and($logItem->properties['new'])->toBe($newImage)
|
||||||
|
->and($server->image)->toBe($newImage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unable to change the docker image set by administrator', function () {
|
||||||
|
[$user, $server] = generateTestAccount([Permission::ACTION_STARTUP_DOCKER_IMAGE]);
|
||||||
|
$oldImage = 'ghcr.io/parkervcp/yolks:java_custom';
|
||||||
|
$server->image = $oldImage;
|
||||||
|
$server->save();
|
||||||
|
|
||||||
|
$newImage = 'ghcr.io/parkervcp/yolks:java_8';
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->putJson("/api/client/servers/$server->uuid/settings/docker-image", [
|
||||||
|
'docker_image' => $newImage,
|
||||||
|
])
|
||||||
|
->assertStatus(Response::HTTP_BAD_REQUEST);
|
||||||
|
|
||||||
|
$server = $server->refresh();
|
||||||
|
|
||||||
|
expect()->toLogActivities(0)
|
||||||
|
->and($server->image)->toBe($oldImage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can be reinstalled', function () {
|
||||||
|
[$user, $server] = generateTestAccount([Permission::ACTION_SETTINGS_REINSTALL]);
|
||||||
|
expect($server->isInstalled())->toBeTrue();
|
||||||
|
|
||||||
|
$service = \Mockery::mock(DaemonServerRepository::class);
|
||||||
|
$this->app->instance(DaemonServerRepository::class, $service);
|
||||||
|
|
||||||
|
$service->expects('setServer')
|
||||||
|
->with(\Mockery::on(function ($value) use ($server) {
|
||||||
|
return $value->uuid === $server->uuid;
|
||||||
|
}))
|
||||||
|
->andReturnSelf()
|
||||||
|
->getMock()
|
||||||
|
->expects('reinstall')
|
||||||
|
->andReturnUndefined();
|
||||||
|
|
||||||
|
$this->actingAs($user)->postJson("/api/client/servers/$server->uuid/settings/reinstall")
|
||||||
|
->assertStatus(Response::HTTP_ACCEPTED);
|
||||||
|
|
||||||
|
$server = $server->refresh();
|
||||||
|
expect()->toLogActivities(1)
|
||||||
|
->and($server->status)->toBe(ServerState::Installing);
|
||||||
|
});
|
49
tests/Filament/Admin/ListEggsTest.php
Normal file
49
tests/Filament/Admin/ListEggsTest.php
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Enums\RolePermissionModels;
|
||||||
|
use App\Filament\Admin\Resources\EggResource\Pages\ListEggs;
|
||||||
|
use App\Models\Egg;
|
||||||
|
use App\Models\Permission;
|
||||||
|
use App\Models\Role;
|
||||||
|
|
||||||
|
use function Pest\Livewire\livewire;
|
||||||
|
|
||||||
|
it('root admin can see all eggs', function () {
|
||||||
|
$eggs = Egg::all();
|
||||||
|
[$admin] = generateTestAccount([]);
|
||||||
|
$admin = $admin->syncRoles(Role::getRootAdmin());
|
||||||
|
|
||||||
|
$this->actingAs($admin);
|
||||||
|
livewire(ListEggs::class)
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertCountTableRecords($eggs->count())
|
||||||
|
->assertCanSeeTableRecords($eggs);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('non root admin cannot see any eggs', function () {
|
||||||
|
$role = Role::factory()->create(['name' => 'Node Viewer', 'guard_name' => 'web']);
|
||||||
|
// Node Permission is on purpose, we check the wrong permissions.
|
||||||
|
$permission = Permission::factory()->create(['name' => RolePermissionModels::Node->viewAny(), 'guard_name' => 'web']);
|
||||||
|
$role->permissions()->attach($permission);
|
||||||
|
[$user] = generateTestAccount([]);
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
livewire(ListEggs::class)
|
||||||
|
->assertForbidden();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('non root admin with permissions can see eggs', function () {
|
||||||
|
$role = Role::factory()->create(['name' => 'Egg Viewer', 'guard_name' => 'web']);
|
||||||
|
$permission = Permission::factory()->create(['name' => RolePermissionModels::Egg->viewAny(), 'guard_name' => 'web']);
|
||||||
|
$role->permissions()->attach($permission);
|
||||||
|
|
||||||
|
$eggs = Egg::all();
|
||||||
|
[$user] = generateTestAccount([]);
|
||||||
|
$user = $user->syncRoles($role);
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
livewire(ListEggs::class)
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertCountTableRecords($eggs->count())
|
||||||
|
->assertCanSeeTableRecords($eggs);
|
||||||
|
});
|
67
tests/Filament/Admin/ListNodesTest.php
Normal file
67
tests/Filament/Admin/ListNodesTest.php
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Enums\RolePermissionModels;
|
||||||
|
use App\Filament\Admin\Resources\NodeResource\Pages\ListNodes;
|
||||||
|
use App\Models\Node;
|
||||||
|
use App\Models\Permission;
|
||||||
|
use App\Models\Role;
|
||||||
|
use App\Models\Server;
|
||||||
|
use Filament\Actions\CreateAction;
|
||||||
|
use Filament\Tables\Actions\CreateAction as TableCreateAction;
|
||||||
|
|
||||||
|
use function Pest\Livewire\livewire;
|
||||||
|
|
||||||
|
it('root admin can see all nodes', function () {
|
||||||
|
[$admin] = generateTestAccount([]);
|
||||||
|
$admin = $admin->syncRoles(Role::getRootAdmin());
|
||||||
|
$nodes = Node::all();
|
||||||
|
|
||||||
|
$this->actingAs($admin);
|
||||||
|
livewire(ListNodes::class)
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertCountTableRecords($nodes->count())
|
||||||
|
->assertCanSeeTableRecords($nodes);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('non root admin cannot see any nodes', function () {
|
||||||
|
$role = Role::factory()->create(['name' => 'Egg Viewer', 'guard_name' => 'web']);
|
||||||
|
// Egg Permission is on purpose, we check the wrong permissions.
|
||||||
|
$permission = Permission::factory()->create(['name' => RolePermissionModels::Egg->viewAny(), 'guard_name' => 'web']);
|
||||||
|
$role->permissions()->attach($permission);
|
||||||
|
[$user] = generateTestAccount();
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
livewire(ListNodes::class)
|
||||||
|
->assertForbidden();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('non root admin with permissions can see nodes', function () {
|
||||||
|
$role = Role::factory()->create(['name' => 'Node Viewer', 'guard_name' => 'web']);
|
||||||
|
$permission = Permission::factory()->create(['name' => RolePermissionModels::Node->viewAny(), 'guard_name' => 'web']);
|
||||||
|
$role->permissions()->attach($permission);
|
||||||
|
|
||||||
|
[$user] = generateTestAccount();
|
||||||
|
$nodes = Node::all();
|
||||||
|
$user = $user->syncRoles($role);
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
livewire(ListNodes::class)
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertCountTableRecords($nodes->count())
|
||||||
|
->assertCanSeeTableRecords($nodes);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays the create button in the table instead of the header when 0 nodes', function () {
|
||||||
|
[$admin] = generateTestAccount([]);
|
||||||
|
$admin = $admin->syncRoles(Role::getRootAdmin());
|
||||||
|
|
||||||
|
// Nuke servers & nodes
|
||||||
|
Server::truncate();
|
||||||
|
Node::truncate();
|
||||||
|
|
||||||
|
$this->actingAs($admin);
|
||||||
|
livewire(ListNodes::class)
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertHeaderMissing(CreateAction::class)
|
||||||
|
->assertActionExists(TableCreateAction::class);
|
||||||
|
});
|
132
tests/Pest.php
132
tests/Pest.php
@ -24,10 +24,26 @@
|
|||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use App\Models\ActivityLog;
|
||||||
|
use App\Models\Allocation;
|
||||||
|
use App\Models\Egg;
|
||||||
|
use App\Models\Node;
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Models\Subuser;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Tests\Integration\IntegrationTestCase;
|
||||||
|
use Ramsey\Uuid\Uuid;
|
||||||
|
|
||||||
expect()->extend('toBeOne', function () {
|
expect()->extend('toBeOne', function () {
|
||||||
return $this->toBe(1);
|
return $this->toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect()->extend('toLogActivities', function (int $times) {
|
||||||
|
expect(ActivityLog::count())->toBe($times);
|
||||||
|
});
|
||||||
|
|
||||||
|
uses(IntegrationTestCase::class)->in('Feature', 'Filament');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Functions
|
| Functions
|
||||||
@ -43,3 +59,119 @@ function something()
|
|||||||
{
|
{
|
||||||
// ..
|
// ..
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a user and a server for that user. If an array of permissions is passed it
|
||||||
|
* is assumed that the user is actually a subuser of the server.
|
||||||
|
*
|
||||||
|
* @param string[] $permissions
|
||||||
|
* @return array{\App\Models\User, \App\Models\Server}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a server model in the databases for the purpose of testing. If an attribute
|
||||||
|
* is passed in that normally requires this function to create a model no model will be
|
||||||
|
* created and that attribute's value will be used.
|
||||||
|
*
|
||||||
|
* The returned server model will have all the relationships loaded onto it.
|
||||||
|
*/
|
||||||
|
function createServerModel(array $attributes = []): Server
|
||||||
|
{
|
||||||
|
if (isset($attributes['user_id'])) {
|
||||||
|
$attributes['owner_id'] = $attributes['user_id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($attributes['owner_id'])) {
|
||||||
|
/** @var \App\Models\User $user */
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$attributes['owner_id'] = $user->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($attributes['node_id'])) {
|
||||||
|
/** @var \App\Models\Node $node */
|
||||||
|
$node = Node::factory()->create();
|
||||||
|
$attributes['node_id'] = $node->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($attributes['allocation_id'])) {
|
||||||
|
/** @var \App\Models\Allocation $allocation */
|
||||||
|
$allocation = Allocation::factory()->create(['node_id' => $attributes['node_id']]);
|
||||||
|
$attributes['allocation_id'] = $allocation->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($attributes['egg_id'])) {
|
||||||
|
$egg = getBungeecordEgg();
|
||||||
|
|
||||||
|
$attributes['egg_id'] = $egg->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($attributes['user_id']);
|
||||||
|
|
||||||
|
/** @var \App\Models\Server $server */
|
||||||
|
$server = Server::factory()->create($attributes);
|
||||||
|
|
||||||
|
Allocation::query()->where('id', $server->allocation_id)->update(['server_id' => $server->id]);
|
||||||
|
|
||||||
|
return $server->fresh([
|
||||||
|
'user', 'node', 'allocation', 'egg',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a user and a server for that user. If an array of permissions is passed it
|
||||||
|
* is assumed that the user is actually a subuser of the server.
|
||||||
|
*
|
||||||
|
* @param string[] $permissions
|
||||||
|
* @return array{\App\Models\User, \App\Models\Server}
|
||||||
|
*/
|
||||||
|
function generateTestAccount(array $permissions = []): array
|
||||||
|
{
|
||||||
|
/** @var \App\Models\User $user */
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
if (empty($permissions)) {
|
||||||
|
return [$user, createServerModel(['user_id' => $user->id])];
|
||||||
|
}
|
||||||
|
|
||||||
|
$server = createServerModel();
|
||||||
|
|
||||||
|
Subuser::query()->create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'server_id' => $server->id,
|
||||||
|
'permissions' => $permissions,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [$user, $server];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clones a given egg allowing us to make modifications that don't affect other
|
||||||
|
* tests that rely on the egg existing in the correct state.
|
||||||
|
*/
|
||||||
|
function cloneEggAndVariables(Egg $egg): Egg
|
||||||
|
{
|
||||||
|
$model = $egg->replicate(['id', 'uuid']);
|
||||||
|
$model->uuid = Uuid::uuid4()->toString();
|
||||||
|
$model->push();
|
||||||
|
|
||||||
|
/** @var \App\Models\Egg $model */
|
||||||
|
$model = $model->fresh();
|
||||||
|
|
||||||
|
foreach ($egg->variables as $variable) {
|
||||||
|
$variable->replicate(['id', 'egg_id'])->forceFill(['egg_id' => $model->id])->push();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $model->fresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Almost every test just assumes it is using BungeeCord — this is the critical
|
||||||
|
* egg model for all tests unless specified otherwise.
|
||||||
|
*/
|
||||||
|
function getBungeecordEgg(): Egg
|
||||||
|
{
|
||||||
|
/** @var \App\Models\Egg $egg */
|
||||||
|
$egg = Egg::query()->where('author', 'panel@example.com')->where('name', 'Bungeecord')->firstOrFail();
|
||||||
|
|
||||||
|
return $egg;
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user