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
|
||||
|
||||
public/assets/manifest.json
|
||||
/database/*.sqlite
|
||||
/database/*.sqlite-journal
|
||||
/database/*.sqlite*
|
||||
filament-monaco-editor/
|
||||
_ide_helper*
|
||||
/.phpstorm.meta.php
|
||||
|
24
Dockerfile
24
Dockerfile
@ -1,16 +1,9 @@
|
||||
# syntax=docker.io/docker/dockerfile:1.13-labs
|
||||
# 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".
|
||||
|
||||
# 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
|
||||
##
|
||||
# If you want to build this locally you want to run `docker build -f Dockerfile.dev`
|
||||
##
|
||||
|
||||
# ================================
|
||||
# Stage 1-1: Composer Install
|
||||
@ -82,15 +75,14 @@ RUN chown root:www-data ./ \
|
||||
&& chmod 750 ./ \
|
||||
# Files should not have execute set, but directories need it
|
||||
&& 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/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 -s /pelican-data/storage /var/www/html/storage/app/public/avatars \
|
||||
# Create necessary directories
|
||||
&& mkdir -p /pelican-data /var/run/supervisord /etc/supercronic \
|
||||
# Finally allow www-data write permissions where necessary
|
||||
&& 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
|
||||
|
||||
|
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 User = 'user';
|
||||
case Webhook = 'webhook';
|
||||
|
||||
public function viewAny(): string
|
||||
{
|
||||
return RolePermissionPrefixes::ViewAny->value . ' ' . $this->value;
|
||||
}
|
||||
|
||||
public function view(): string
|
||||
{
|
||||
return RolePermissionPrefixes::View->value . ' ' . $this->value;
|
||||
}
|
||||
|
||||
public function create(): string
|
||||
{
|
||||
return RolePermissionPrefixes::Create->value . ' ' . $this->value;
|
||||
}
|
||||
|
||||
public function update(): string
|
||||
{
|
||||
return RolePermissionPrefixes::Update->value . ' ' . $this->value;
|
||||
}
|
||||
}
|
||||
|
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'))
|
||||
->inlineLabel()
|
||||
->ipv4()
|
||||
->live()
|
||||
->afterStateUpdated(fn (Set $set) => $set('allocation_ports', []))
|
||||
->required(),
|
||||
TextInput::make('allocation_alias')
|
||||
@ -83,6 +84,7 @@ class AllocationsRelationManager extends RelationManager
|
||||
->label(trans('admin/server.ports'))
|
||||
->inlineLabel()
|
||||
->live()
|
||||
->disabled(fn (Get $get) => empty($get('allocation_ip')))
|
||||
->afterStateUpdated(fn ($state, Set $set, Get $get) => $set('allocation_ports',
|
||||
CreateServer::retrieveValidPorts($this->getOwnerRecord()->node, $state, $get('allocation_ip')))
|
||||
)
|
||||
|
@ -5,6 +5,7 @@ namespace App\Filament\Server\Pages;
|
||||
use App\Enums\ConsoleWidgetPosition;
|
||||
use App\Enums\ContainerStatus;
|
||||
use App\Exceptions\Http\Server\ServerStateConflictException;
|
||||
use App\Extensions\Features\FeatureProvider;
|
||||
use App\Filament\Server\Widgets\ServerConsole;
|
||||
use App\Filament\Server\Widgets\ServerCpuChart;
|
||||
use App\Filament\Server\Widgets\ServerMemoryChart;
|
||||
@ -13,8 +14,9 @@ use App\Filament\Server\Widgets\ServerOverview;
|
||||
use App\Livewire\AlertBanner;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\Concerns\InteractsWithActions;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Support\Enums\Size;
|
||||
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
|
||||
{
|
||||
return [
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Filament\Server\Resources\DatabaseResource\Pages;
|
||||
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction;
|
||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
||||
use App\Filament\Server\Resources\DatabaseResource;
|
||||
@ -82,12 +81,7 @@ class ListDatabases extends ListRecords
|
||||
ViewAction::make()
|
||||
->modalHeading(fn (Database $database) => 'Viewing ' . $database->database),
|
||||
DeleteAction::make()
|
||||
->after(function (Database $database) {
|
||||
Activity::event('server:database.delete')
|
||||
->subject($database)
|
||||
->property('name', $database->database)
|
||||
->log();
|
||||
}),
|
||||
->using(fn (Database $database, DatabaseManagementService $service) => $service->delete($database)),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,8 @@ use Filament\Resources\Pages\Page;
|
||||
use Filament\Resources\Pages\PageRegistration;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Enums\Alignment;
|
||||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Facades\Route as RouteFacade;
|
||||
use Livewire\Attributes\Locked;
|
||||
@ -118,37 +120,37 @@ class EditFiles extends Page
|
||||
// TODO MonacoEditor::make('editor')
|
||||
// ->hiddenLabel()
|
||||
// ->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()
|
||||
// ->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()
|
||||
// ->send();
|
||||
//
|
||||
// $this->redirect(ListFiles::getUrl());
|
||||
// } catch (FileNotFoundException) {
|
||||
// AlertBanner::make()
|
||||
// $this->redirect(ListFiles::getUrl());
|
||||
// ->title('File Not found!')
|
||||
// ->body('<code>' . $this->path . '</code>')
|
||||
// ->danger()
|
||||
// ->closable()
|
||||
// ->body('<code>' . $this->path . '</code>')
|
||||
// ->send();
|
||||
// ->closable()
|
||||
//
|
||||
// $this->redirect(ListFiles::getUrl());
|
||||
// } catch (FileNotEditableException) {
|
||||
// AlertBanner::make()
|
||||
// ->title('Could not edit directory!')
|
||||
// AlertBanner::make()
|
||||
// ->body('<code>' . $this->path . '</code>')
|
||||
// ->danger()
|
||||
// ->closable()
|
||||
// ->send();
|
||||
// ->danger()
|
||||
//
|
||||
// $this->redirect(ListFiles::getUrl());
|
||||
// }
|
||||
// $this->redirect(ListFiles::getUrl());
|
||||
// })
|
||||
// ->language(fn (Get $get) => $get('lang'))
|
||||
// ->view('filament.plugins.monaco-editor'),
|
||||
|
@ -11,7 +11,6 @@ use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonFileRepository;
|
||||
use App\Filament\Components\Tables\Columns\BytesColumn;
|
||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
||||
use App\Livewire\AlertBanner;
|
||||
use Filament\Actions\Action as HeaderAction;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
@ -36,7 +35,6 @@ use Filament\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Carbon;
|
||||
@ -53,24 +51,11 @@ class ListFiles extends ListRecords
|
||||
|
||||
private DaemonFileRepository $fileRepository;
|
||||
|
||||
private bool $isDisabled = false;
|
||||
|
||||
public function mount(?string $path = null): void
|
||||
{
|
||||
parent::mount();
|
||||
|
||||
$this->path = $path ?? '/';
|
||||
|
||||
try {
|
||||
$this->getDaemonFileRepository()->getDirectory('/');
|
||||
} catch (ConnectionException) {
|
||||
$this->isDisabled = true;
|
||||
|
||||
AlertBanner::make('node_connection_error')
|
||||
->title('Could not connect to the node!')
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
@ -130,21 +115,18 @@ class ListFiles extends ListRecords
|
||||
->actions([
|
||||
Action::make('view')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->label('Open')
|
||||
->icon('tabler-eye')
|
||||
->visible(fn (File $file) => $file->is_directory)
|
||||
->url(fn (File $file) => self::getUrl(['path' => join_paths($this->path, $file->name)])),
|
||||
EditAction::make('edit')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->icon('tabler-edit')
|
||||
->visible(fn (File $file) => $file->canEdit())
|
||||
->url(fn (File $file) => EditFiles::getUrl(['path' => join_paths($this->path, $file->name)])),
|
||||
ActionGroup::make([
|
||||
Action::make('rename')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->label('Rename')
|
||||
->icon('tabler-forms')
|
||||
->schema([
|
||||
@ -173,7 +155,6 @@ class ListFiles extends ListRecords
|
||||
}),
|
||||
Action::make('copy')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->label('Copy')
|
||||
->icon('tabler-copy')
|
||||
->visible(fn (File $file) => $file->is_file)
|
||||
@ -193,14 +174,12 @@ class ListFiles extends ListRecords
|
||||
}),
|
||||
Action::make('download')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->label('Download')
|
||||
->icon('tabler-download')
|
||||
->visible(fn (File $file) => $file->is_file)
|
||||
->url(fn (File $file) => DownloadFiles::getUrl(['path' => join_paths($this->path, $file->name)]), true),
|
||||
Action::make('move')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->label('Move')
|
||||
->icon('tabler-replace')
|
||||
->schema([
|
||||
@ -236,7 +215,6 @@ class ListFiles extends ListRecords
|
||||
}),
|
||||
Action::make('permissions')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->label('Permissions')
|
||||
->icon('tabler-license')
|
||||
->schema([
|
||||
@ -293,7 +271,6 @@ class ListFiles extends ListRecords
|
||||
}),
|
||||
Action::make('archive')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->label('Archive')
|
||||
->icon('tabler-archive')
|
||||
->schema([
|
||||
@ -321,7 +298,6 @@ class ListFiles extends ListRecords
|
||||
}),
|
||||
Action::make('unarchive')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->label('Unarchive')
|
||||
->icon('tabler-archive')
|
||||
->visible(fn (File $file) => $file->isArchive())
|
||||
@ -343,7 +319,6 @@ class ListFiles extends ListRecords
|
||||
]),
|
||||
DeleteAction::make()
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->label('')
|
||||
->icon('tabler-trash')
|
||||
->requiresConfirmation()
|
||||
@ -358,48 +333,44 @@ class ListFiles extends ListRecords
|
||||
->log();
|
||||
}),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
BulkAction::make('move')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->schema([
|
||||
TextInput::make('location')
|
||||
->label('Directory')
|
||||
->hint('Enter the new directory, relative to the current directory.')
|
||||
->required()
|
||||
->live(),
|
||||
TextEntry::make('new_location')
|
||||
->state(fn (Get $get) => resolve_path('./' . join_paths($this->path, $get('location') ?? ''))),
|
||||
$location = rtrim($data['location'], '/');
|
||||
])
|
||||
->action(function (Collection $files, $data) {
|
||||
$location = rtrim($data['location'], '/');
|
||||
->content(fn (Get $get) => resolve_path('./' . join_paths($this->path, $get('location') ?? ''))),
|
||||
Placeholder::make('new_location')
|
||||
->live(),
|
||||
->required()
|
||||
->hint('Enter the new directory, relative to the current directory.')
|
||||
->label('Directory')
|
||||
->form([
|
||||
TextInput::make('location')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->groupedBulkActions([
|
||||
BulkAction::make('move')
|
||||
|
||||
$files = $files->map(fn ($file) => ['to' => join_paths($location, $file['name']), 'from' => $file['name']])->toArray();
|
||||
$this->getDaemonFileRepository()
|
||||
->renameFiles($this->path, $files);
|
||||
$this->getDaemonFileRepository()->renameFiles($this->path, $files);
|
||||
|
||||
Activity::event('server:file.rename')
|
||||
->property('directory', $this->path)
|
||||
->property('files', $files)
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title(count($files) . ' Files were moved to ' . resolve_path(join_paths($this->path, $location)))
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
BulkAction::make('archive')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label('Archive name')
|
||||
->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z')
|
||||
->suffix('.tar.gz'),
|
||||
])
|
||||
->action(function ($data, Collection $files) {
|
||||
$files = $files->map(fn ($file) => $file['name'])->toArray();
|
||||
->action(function ($data, Collection $files) {
|
||||
])
|
||||
->suffix('.tar.gz'),
|
||||
->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z')
|
||||
->label('Archive name')
|
||||
->form([
|
||||
TextInput::make('name')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
|
||||
BulkAction::make('archive')
|
||||
->success()
|
||||
->title(count($files) . ' Files were moved to ' . resolve_path(join_paths($this->path, $location)))
|
||||
Notification::make()
|
||||
}),
|
||||
->send();
|
||||
|
||||
$archive = $this->getDaemonFileRepository()->compressFiles($this->path, $files, $data['name']);
|
||||
|
||||
@ -419,7 +390,6 @@ class ListFiles extends ListRecords
|
||||
}),
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->action(function (Collection $files) {
|
||||
$files = $files->map(fn ($file) => $file['name'])->toArray();
|
||||
$this->getDaemonFileRepository()->deleteFiles($this->path, $files);
|
||||
@ -434,7 +404,6 @@ class ListFiles extends ListRecords
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -446,7 +415,6 @@ class ListFiles extends ListRecords
|
||||
return [
|
||||
HeaderAction::make('new_file')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->label('New File')
|
||||
->color('gray')
|
||||
->keyBindings('')
|
||||
@ -478,7 +446,6 @@ class ListFiles extends ListRecords
|
||||
]),
|
||||
HeaderAction::make('new_folder')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->label('New Folder')
|
||||
->color('gray')
|
||||
->action(function ($data) {
|
||||
@ -495,7 +462,6 @@ class ListFiles extends ListRecords
|
||||
]),
|
||||
HeaderAction::make('upload')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->label('Upload')
|
||||
->action(function ($data) {
|
||||
if (count($data['files']) > 0 && !isset($data['url'])) {
|
||||
@ -545,7 +511,6 @@ class ListFiles extends ListRecords
|
||||
]),
|
||||
HeaderAction::make('search')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->label('Global Search')
|
||||
->modalSubmitActionLabel('Search')
|
||||
->schema([
|
||||
|
@ -36,26 +36,22 @@ class SettingsController extends ClientApiController
|
||||
$name = $request->input('name');
|
||||
$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) {
|
||||
Activity::event('server:settings.rename')
|
||||
->property(['old' => $server->name, 'new' => $name])
|
||||
->log();
|
||||
$server->name = $name;
|
||||
}
|
||||
|
||||
if ($server->description !== $description) {
|
||||
if ($server->description !== $description && config('panel.editable_server_descriptions')) {
|
||||
Activity::event('server:settings.description')
|
||||
->property(['old' => $server->description, 'new' => $description])
|
||||
->log();
|
||||
$server->description = $description;
|
||||
}
|
||||
|
||||
$server->save();
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ namespace App\Models;
|
||||
use App\Contracts\Validatable;
|
||||
use App\Exceptions\Service\Egg\HasChildrenException;
|
||||
use App\Exceptions\Service\HasActiveServersException;
|
||||
use App\Extensions\Features\FeatureProvider;
|
||||
use App\Traits\HasValidation;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@ -70,19 +71,6 @@ class Egg extends Model implements Validatable
|
||||
*/
|
||||
public const EXPORT_VERSION = 'PLCN_v1';
|
||||
|
||||
/**
|
||||
* Different features that can be enabled on any given egg. These are used internally
|
||||
* to determine which types of frontend functionality should be shown to the user. Eggs
|
||||
* will automatically inherit features from a parent egg if they are already configured
|
||||
* to copy configuration values from said egg.
|
||||
*
|
||||
* To skip copying the features, an empty array value should be passed in ("[]") rather
|
||||
* than leaving it null.
|
||||
*/
|
||||
public const FEATURE_EULA_POPUP = 'eula';
|
||||
|
||||
public const FEATURE_FASTDL = 'fastdl';
|
||||
|
||||
/**
|
||||
* Fields that are not mass assignable.
|
||||
*/
|
||||
@ -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
|
||||
* it will return the copied script.
|
||||
|
@ -153,17 +153,11 @@ class File extends Model
|
||||
try {
|
||||
$fileRepository = (new DaemonFileRepository())->setServer(self::$server);
|
||||
|
||||
$contents = [];
|
||||
|
||||
try {
|
||||
if (!is_null(self::$searchTerm)) {
|
||||
$contents = cache()->remember('file_search_' . self::$path . '_' . self::$searchTerm, now()->addMinute(), fn () => $fileRepository->search(self::$searchTerm, self::$path));
|
||||
} else {
|
||||
$contents = $fileRepository->getDirectory(self::$path ?? '/');
|
||||
}
|
||||
} catch (ConnectionException $exception) {
|
||||
report($exception);
|
||||
}
|
||||
|
||||
if (isset($contents['error'])) {
|
||||
throw new Exception($contents['error']);
|
||||
@ -199,8 +193,12 @@ class File extends Model
|
||||
$message = $message->after('cURL error 7: ')->before(' after ');
|
||||
}
|
||||
|
||||
if ($exception instanceof ConnectionException) {
|
||||
$message = str('Node connection failed');
|
||||
}
|
||||
|
||||
AlertBanner::make()
|
||||
->title('Could not load files')
|
||||
->title('Could not load files!')
|
||||
->body($message->toString())
|
||||
->danger()
|
||||
->send();
|
||||
|
@ -4,12 +4,13 @@ namespace App\Models;
|
||||
|
||||
use App\Contracts\Validatable;
|
||||
use App\Traits\HasValidation;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class Permission extends Model implements Validatable
|
||||
{
|
||||
use HasValidation;
|
||||
use HasFactory, HasValidation;
|
||||
|
||||
/**
|
||||
* 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\RolePermissionPrefixes;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Spatie\Permission\Models\Role as BaseRole;
|
||||
|
||||
/**
|
||||
@ -17,6 +18,8 @@ use Spatie\Permission\Models\Role as BaseRole;
|
||||
*/
|
||||
class Role extends BaseRole
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public const RESOURCE_NAME = 'role';
|
||||
|
||||
public const ROOT_ADMIN = 'Root Admin';
|
||||
|
@ -15,6 +15,11 @@ use App\Extensions\Avatar\Providers\UiAvatarsProvider;
|
||||
use App\Extensions\OAuth\Providers\GitlabProvider;
|
||||
use App\Models;
|
||||
use App\Extensions\Captcha\Providers\TurnstileProvider;
|
||||
use App\Extensions\Features\GSLToken;
|
||||
use App\Extensions\Features\JavaVersion;
|
||||
use App\Extensions\Features\MinecraftEula;
|
||||
use App\Extensions\Features\PIDLimit;
|
||||
use App\Extensions\Features\SteamDiskSpace;
|
||||
use App\Extensions\OAuth\Providers\AuthentikProvider;
|
||||
use App\Extensions\OAuth\Providers\CommonProvider;
|
||||
use App\Extensions\OAuth\Providers\DiscordProvider;
|
||||
@ -121,6 +126,13 @@ class AppServiceProvider extends ServiceProvider
|
||||
GravatarProvider::register();
|
||||
UiAvatarsProvider::register();
|
||||
|
||||
// Default Feature providers
|
||||
GSLToken::register($app);
|
||||
JavaVersion::register($app);
|
||||
MinecraftEula::register($app);
|
||||
PIDLimit::register($app);
|
||||
SteamDiskSpace::register($app);
|
||||
|
||||
FilamentColor::register([
|
||||
'danger' => Color::Red,
|
||||
'gray' => Color::Zinc,
|
||||
|
@ -118,11 +118,18 @@ class DatabaseManagementService
|
||||
*/
|
||||
public function delete(Database $database): ?bool
|
||||
{
|
||||
return $this->connection->transaction(function () use ($database) {
|
||||
$database->dropDatabase($database->database);
|
||||
$database->dropUser($database->username, $database->remote);
|
||||
$database->flush();
|
||||
|
||||
Activity::event('server:database.delete')
|
||||
->subject($database)
|
||||
->property('name', $database->database)
|
||||
->log();
|
||||
|
||||
return $database->delete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -101,6 +101,9 @@ class ServerConfigurationStructureService
|
||||
'egg' => [
|
||||
'id' => $server->egg->uuid,
|
||||
'file_denylist' => $server->egg->inherit_file_denylist,
|
||||
'features' => collect($server->egg->features())->mapWithKeys(fn ($feature) => [
|
||||
$feature->getId() => $feature->getListeners(),
|
||||
])->all(),
|
||||
],
|
||||
];
|
||||
|
||||
|
@ -46,6 +46,7 @@ class EggTransformer extends BaseTransformer
|
||||
'name' => $model->name,
|
||||
'author' => $model->author,
|
||||
'description' => $model->description,
|
||||
'features' => $model->features,
|
||||
// "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
|
||||
// to upgrade to using "docker_images".
|
||||
|
@ -54,6 +54,8 @@
|
||||
"mockery/mockery": "^1.6.11",
|
||||
"nunomaduro/collision": "^8.6",
|
||||
"pestphp/pest": "^3.7",
|
||||
"pestphp/pest-plugin-faker": "^3.0",
|
||||
"pestphp/pest-plugin-livewire": "^3.0",
|
||||
"spatie/laravel-ignition": "^2.9"
|
||||
},
|
||||
"autoload": {
|
||||
|
133
composer.lock
generated
133
composer.lock
generated
@ -13857,6 +13857,137 @@
|
||||
],
|
||||
"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",
|
||||
"version": "v3.0.5",
|
||||
@ -16017,5 +16148,5 @@
|
||||
"platform-overrides": {
|
||||
"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",
|
||||
"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",
|
||||
"name": "Sponge (SpongeVanilla)",
|
||||
"exported_at": "2025-04-25T06:05:10+00:00",
|
||||
"name": "Sponge",
|
||||
"author": "panel@example.com",
|
||||
"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": [
|
||||
"minecraft"
|
||||
],
|
||||
@ -34,28 +34,42 @@
|
||||
},
|
||||
"scripts": {
|
||||
"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",
|
||||
"entrypoint": "ash"
|
||||
}
|
||||
},
|
||||
"variables": [
|
||||
{
|
||||
"name": "Sponge Version",
|
||||
"description": "The version of SpongeVanilla to download and use.",
|
||||
"env_variable": "SPONGE_VERSION",
|
||||
"default_value": "1.12.2-7.3.0",
|
||||
"sort": 3,
|
||||
"name": "Forge\/Neoforge Version",
|
||||
"description": "The modding api target version if set to `spongeforge` or `spongeneo`. Leave blank if using `spongevanilla`",
|
||||
"env_variable": "FORGE_VERSION",
|
||||
"default_value": "",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sort": 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_editable": true,
|
||||
"rules": [
|
||||
"required",
|
||||
"regex:\/^([a-zA-Z0-9.\\-_]+)$\/"
|
||||
],
|
||||
"sort": 1
|
||||
"string",
|
||||
"between:3,15"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sort": 4,
|
||||
"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",
|
||||
"default_value": "server.jar",
|
||||
"user_viewable": true,
|
||||
@ -63,8 +77,20 @@
|
||||
"rules": [
|
||||
"required",
|
||||
"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
|
||||
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
|
||||
echo "Generating APP_KEY..."
|
||||
|
@ -129,6 +129,9 @@
|
||||
case 'install output':
|
||||
handleConsoleOutput(args[0]);
|
||||
break;
|
||||
case 'feature match':
|
||||
Livewire.dispatch('mount-feature', { data: args[0] });
|
||||
break;
|
||||
case 'status':
|
||||
handlePowerChangeEvent(args[0]);
|
||||
|
||||
|
@ -4,4 +4,7 @@
|
||||
:data="$this->getWidgetData()"
|
||||
:widgets="$this->getVisibleWidgets()"
|
||||
/>
|
||||
|
||||
<x-filament-actions::modals />
|
||||
|
||||
</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 () {
|
||||
return $this->toBe(1);
|
||||
});
|
||||
|
||||
expect()->extend('toLogActivities', function (int $times) {
|
||||
expect(ActivityLog::count())->toBe($times);
|
||||
});
|
||||
|
||||
uses(IntegrationTestCase::class)->in('Feature', 'Filament');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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