Compare commits

..

41 Commits

Author SHA1 Message Date
github-actions[bot]
9ac03160c3 ci(release): bump version 2024-11-13 23:17:46 +00:00
Boy132
fe4668a517 Update web installer (again) (#705)
* update web installer (again)

* set default values for mysql/ mariadb and redis

* add own step for queue setup

* create admin user in submit

* disable redis for queue if cache isn't redis

* remove separate user step and make session own step

* use `request()->isSecure()`
2024-11-13 18:15:48 -05:00
Lance Pioch
6125b07afa Remove old admin area (#648)
* Remove old admin

* Remove controller test

* Remove unused exceptions

* Remove unused files

* More small tweaks

* Fix doc block

* Remove unused service

* Restore these

* Add back autoDeploy

* Revert "Add back autoDeploy"

This reverts commit 630c1e08ac.

* Add these back

* Add back exception

* Remove ApiController again

---------

Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
Co-authored-by: Boy132 <mail@boy132.de>
Co-authored-by: notCharles <charles@pelican.dev>
2024-11-13 17:05:48 -05:00
Boy132
9717aa4b5f Cleanup SoftwareVersionService (#704)
* cleanup SoftwareVersionService

* fix old admin area

* show latest wings version on EditNode page

* even more cleanup
2024-11-13 16:26:10 -05:00
MartinOscar
9491322d8c Merge pull request #708 from pelican-dev/charles/fixbulk
Prevent Select All on Allocations
2024-11-13 22:25:21 +01:00
notCharles
8ed6bb4d8b pint 2024-11-13 16:22:12 -05:00
notCharles
a787af7a06 Prevent Select All
Prevent Select all on allocations, prevent people from trying to delete 30,000 ports at once ....
2024-11-13 16:21:27 -05:00
MartinOscar
d9016702d6 Merge pull request #707 from pelican-dev/charles/fixnode
Change 'exception'
2024-11-13 22:07:45 +01:00
notCharles
d565441b6a Change 'exception'
Remove the exception and just report the whole error.
2024-11-13 15:58:20 -05:00
Michael (Parker) Parker
cb522b24ef Merge pull request #706 from parkervcp/update/egg_version
use correct case for import
2024-11-09 13:59:38 -05:00
Michael (Parker) Parker
b85b17f080 use correct case for import
use lower case `v` instead of upper case `V`
2024-11-09 13:53:50 -05:00
Lance Pioch
47bd7289b1 Clear webhook cache when webhooks are deleted (#695)
* Clear webhook cache when webhooks are deleted

* fix: type casts

---------

Co-authored-by: Vehikl <go@vehikl.com>
2024-11-07 17:26:47 -05:00
Boy132
a9b76a0f51 Improve egg import error handling (#703)
* make sure read & write are successful

* show exception message in notification
2024-11-07 17:15:47 -05:00
MartinOscar
8eebb82eba Fix AutoDeploy & KeyCreationService (#701)
* Fix AutoDeploy & KeyCreationService

* Get rid of 2nd param & unset perm
2024-11-07 17:15:41 -05:00
Boy132
b3501be6ec Refactor api key permissions (#361)
* use RESOURCE_NAME for requests

* use RESOURCE_NAME for transformers

* add permissions field to api key

* add migration for new permissions field

* update tests

* remove debug log

* set column type to "json"

* remove default attribute to fix tests

* fix default value for permissions

* fix after merge

* fix after merge

* allow to "register" custom permissions

* add "role" to default resource names

* fix after merge

* fix phpstan

* fix migrations
2024-11-06 09:09:10 +01:00
Michael (Parker) Parker
ac67656d82 Merge pull request #700 from BlockyBlockling/skip-caddy-fix
Fixing Docker Environment variable only getting checked for existence instead of value
2024-11-04 11:51:05 -05:00
BlockyBlockling
968239beb3 Update entrypoint.sh
Fixed Syntax after last change
2024-11-04 13:07:57 +01:00
BlockyBlockling
7514206186 Update entrypoint.sh
Adding :- Syntax which ensures that, if SKIP_CADDY is unset, it will be treated as an empty string, which will not match "true". This avoids potential issues with unbound variables in some shell configurations where set -u (treating unset variables as an error) is enabled.

(ChatGPT)
2024-11-04 13:07:20 +01:00
BlockyBlockling
1a8321c937 Update entrypoint.sh
Fixing that its only checking for the existence of the environment variable „SKIP_CADDY“ instead of checking for its value
2024-11-04 12:43:40 +01:00
MartinOscar
340ae8099b Fix trusted proxies settings & Move ips to config & Add ipv6 (#692)
* Fix blank proxy & Move hardcoded cloudflare ips

* Add cloudflare's ipv6

* Pull from url innstead of hardcoded

* Remove Service
2024-11-01 18:16:59 -04:00
Boy132
9d02aeb130 Replace reCAPTCHA with Turnstile (#589)
* add laravel turnstile

* add config & settings for turnstile

* publish view to center captcha

* completely replace reCAPTCHA

* update FailedCaptcha event

* add back config for domain verification

* don't set language so browser lang is used
2024-11-01 18:15:04 -04:00
Charles
cf57c28c40 Update Webhooks to match other resources (#686)
* Move these

Move List/Create to their own pages to follow the flow of the other resources.

* Move EditPage aswell

* Move Save

* Labels

* Change Edit/Delete

---------

Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2024-11-01 18:14:20 -04:00
Boy132
382dcb3868 Fix redis connection check (#698) 2024-11-01 18:10:36 +01:00
Boy132
f793b49a81 Add egg filter to server mounts list (#697) 2024-11-01 18:10:24 +01:00
Lance Pioch
41ddae1ba0 Update ci.yaml (#643) 2024-10-31 05:39:42 -04:00
MartinOscar
e717e20996 Merge pull request #687 from RMartinOscar/fix/HealthVersion
Fix Node Health not refreshing live & Add tooltip
2024-10-30 01:58:37 +01:00
Lance Pioch
b5145b016b Update app/Models/Node.php 2024-10-29 19:53:12 -04:00
Lance Pioch
95a8f72058 Update app/Models/Node.php 2024-10-29 19:52:51 -04:00
Lance Pioch
19548338ee Update app/Models/Node.php 2024-10-29 19:52:32 -04:00
RMartinOscar
a8356fc5d2 Polishing & throw curl error 2024-10-29 20:36:44 +00:00
Boy132
7a447b04d5 Make sure roles always use web guard name (#690) 2024-10-29 18:29:25 +01:00
RMartinOscar
45699e1614 Set refresh rate 10s & Add tooltip for unreachable node 2024-10-29 15:01:30 +00:00
RMartinOscar
cde3546889 Add poll & tooltip 2024-10-29 03:28:51 +00:00
MartinOscar
3f9c1dbc3c Add prune & event blacklist (#682)
* Add prune & event blacklist

* Pinted 3times with --dirty bruh

* Add to Settings

* Fix prune & description

* Prune Logs not Configuration
2024-10-28 18:44:32 -04:00
Charles
bc2df22d78 Add unique (#685)
Usernames have to be unique, trying to make a new user with an existing username results in a 500, this fixes it.
2024-10-28 18:23:29 -04:00
Michael (Parker) Parker
1a3dc5c743 Update Egg Export Version to PLCN_V1 (#676)
* Update Egg Export Version to PLCN_V1

resolves #675

* correct version tag

* remove trailing space
2024-10-27 18:04:21 -04:00
Charles
fdd1b3798c add whereNull (#680)
Add where null to not include allocations already assigned to a server.
2024-10-27 18:01:09 -04:00
Charles
288cbee32f Fix Docker image selection (#674)
* Fix Docker image selection

Should address issue 672

Closes #672

* Fix Docker image selection in CreateServer page

---------

Co-authored-by: MartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2024-10-27 11:22:12 -04:00
MartinOscar
a70a060350 Add Soft Deletes to webhooks config table (#670) 2024-10-27 00:42:08 -04:00
MartinOscar
590569a131 Remove duplicated spa in AdminPanelProvider (#668) 2024-10-26 23:25:21 -04:00
Charles
7acc8782bb Make description required. (#667) 2024-10-26 22:06:34 -04:00
337 changed files with 1521 additions and 21876 deletions

View File

@@ -52,11 +52,11 @@ crond -L /var/log/crond -l 5
export SUPERVISORD_CADDY=false
## disable caddy if SKIP_CADDY is set
if [[ -z $SKIP_CADDY ]]; then
if [[ "${SKIP_CADDY:-}" == "true" ]]; then
echo "Starting PHP-FPM only"
else
echo "Starting PHP-FPM and Caddy"
export SUPERVISORD_CADDY=true
else
echo "Starting PHP-FPM only"
fi
chown -R www-data:www-data /pelican-data/.env /pelican-data/database

View File

@@ -87,7 +87,7 @@ jobs:
fail-fast: false
matrix:
php: [8.2, 8.3]
database: ["mariadb:10.3", "mariadb:10.11", "mariadb:11.4"]
database: ["mariadb:10.6", "mariadb:10.11", "mariadb:11.4"]
services:
database:
image: ${{ matrix.database }}

View File

@@ -26,8 +26,8 @@ class InfoCommand extends Command
{
$this->output->title('Version Information');
$this->table([], [
['Panel Version', $this->versionService->versionData()['version']],
['Latest Version', $this->versionService->getPanel()],
['Panel Version', $this->versionService->currentPanelVersion()],
['Latest Version', $this->versionService->latestPanelVersion()],
['Up-to-Date', $this->versionService->isLatestPanel() ? 'Yes' : $this->formatText('No', 'bg=red')],
], 'compact');

View File

@@ -9,6 +9,7 @@ use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand;
use App\Console\Commands\Schedule\ProcessRunnableCommand;
use App\Jobs\NodeStatistics;
use App\Models\ActivityLog;
use App\Models\Webhook;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Database\Console\PruneCommand;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@@ -48,5 +49,9 @@ class Kernel extends ConsoleKernel
if (config('activity.prune_days')) {
$schedule->command(PruneCommand::class, ['--model' => [ActivityLog::class]])->daily();
}
if (config('panel.webhook.prune_days')) {
$schedule->command(PruneCommand::class, ['--model' => [Webhook::class]])->daily();
}
}
}

View File

@@ -12,7 +12,7 @@ class FailedCaptcha extends Event
/**
* Create a new event instance.
*/
public function __construct(public string $ip, public string $domain)
public function __construct(public string $ip, public ?string $message)
{
}
}

View File

@@ -1,7 +0,0 @@
<?php
namespace App\Exceptions;
class AccountNotFoundException extends \Exception
{
}

View File

@@ -1,7 +0,0 @@
<?php
namespace App\Exceptions;
class AutoDeploymentException extends \Exception
{
}

View File

@@ -10,7 +10,6 @@ use Illuminate\Http\Request;
use Psr\Log\LoggerInterface;
use Illuminate\Http\Response;
use Illuminate\Container\Container;
use Prologue\Alerts\AlertsMessageBag;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
class DisplayException extends PanelException implements HttpExceptionInterface
@@ -67,9 +66,6 @@ class DisplayException extends PanelException implements HttpExceptionInterface
return response()->json(Handler::toArray($this), $this->getStatusCode(), $this->getHeaders());
}
// @phpstan-ignore-next-line
app(AlertsMessageBag::class)->danger($this->getMessage())->flash();
return redirect()->back()->withInput();
}

View File

@@ -7,9 +7,6 @@ use GuzzleHttp\Exception\GuzzleException;
use App\Exceptions\DisplayException;
use Illuminate\Support\Facades\Context;
/**
* @method \GuzzleHttp\Exception\GuzzleException getPrevious()
*/
class DaemonConnectionException extends DisplayException
{
private int $statusCode = Response::HTTP_GATEWAY_TIMEOUT;

View File

@@ -1,9 +0,0 @@
<?php
namespace App\Exceptions\Http\Server;
use App\Exceptions\DisplayException;
class FileTypeNotEditableException extends DisplayException
{
}

View File

@@ -1,9 +0,0 @@
<?php
namespace App\Exceptions\Repository\Daemon;
use App\Exceptions\Repository\RepositoryException;
class InvalidPowerSignalException extends RepositoryException
{
}

View File

@@ -1,9 +0,0 @@
<?php
namespace App\Exceptions\Repository;
use App\Exceptions\PanelException;
class RepositoryException extends PanelException
{
}

View File

@@ -1,9 +0,0 @@
<?php
namespace App\Exceptions\Service\Allocation;
use App\Exceptions\PanelException;
class AllocationDoesNotBelongToServerException extends PanelException
{
}

View File

@@ -1,9 +0,0 @@
<?php
namespace App\Exceptions\Service\Egg;
use App\Exceptions\DisplayException;
class BadJsonFormatException extends DisplayException
{
}

View File

@@ -1,9 +0,0 @@
<?php
namespace App\Exceptions\Service\Egg;
use App\Exceptions\DisplayException;
class NoParentConfigurationFoundException extends DisplayException
{
}

View File

@@ -1,9 +0,0 @@
<?php
namespace App\Exceptions\Service\Schedule\Task;
use App\Exceptions\DisplayException;
class TaskIntervalTooLongException extends DisplayException
{
}

View File

@@ -1,9 +0,0 @@
<?php
namespace App\Exceptions\Service\Server;
use App\Exceptions\PanelException;
class RequiredVariableMissingException extends PanelException
{
}

View File

@@ -1,9 +0,0 @@
<?php
namespace App\Exceptions\Transformer;
use App\Exceptions\PanelException;
class InvalidTransformerLevelException extends PanelException
{
}

View File

@@ -1,13 +0,0 @@
<?php
namespace App\Extensions\Facades;
use Illuminate\Support\Facades\Facade;
class Theme extends Facade
{
protected static function getFacadeAccessor(): string
{
return 'extensions.themes';
}
}

View File

@@ -1,21 +0,0 @@
<?php
namespace App\Extensions\Themes;
class Theme
{
public function js(string $path): string
{
return sprintf('<script src="%s"></script>' . PHP_EOL, $this->getUrl($path));
}
public function css(string $path): string
{
return sprintf('<link media="all" type="text/css" rel="stylesheet" href="%s"/>' . PHP_EOL, $this->getUrl($path));
}
protected function getUrl(string $path): string
{
return '/themes/panel/' . ltrim($path, '/');
}
}

View File

@@ -1,14 +0,0 @@
<?php
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
use App\Services\Activity\ActivityLogBatchService;
class LogBatch extends Facade
{
protected static function getFacadeAccessor(): string
{
return ActivityLogBatchService::class;
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Filament\Pages\Auth;
use Coderflex\FilamentTurnstile\Forms\Components\Turnstile;
use Filament\Pages\Auth\Login as BaseLogin;
class Login extends BaseLogin
{
protected function getForms(): array
{
return [
'form' => $this->form(
$this->makeForm()
->schema([
$this->getEmailFormComponent(),
$this->getPasswordFormComponent(),
$this->getRememberFormComponent(),
Turnstile::make('captcha')
->hidden(!config('turnstile.turnstile_enabled'))
->validationMessages([
'required' => config('turnstile.error_messages.turnstile_check_message'),
]),
])
->statePath('data'),
),
];
}
protected function throwFailureValidationException(): never
{
$this->dispatch('reset-captcha');
parent::throwFailureValidationException();
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Filament\Pages;
use App\Filament\Resources\NodeResource\Pages\CreateNode;
use App\Filament\Resources\NodeResource\Pages\ListNodes;
use App\Models\Egg;
use App\Models\Node;
@@ -39,8 +40,8 @@ class Dashboard extends Page
{
return [
'inDevelopment' => config('app.version') === 'canary',
'version' => $this->softwareVersionService->versionData()['version'],
'latestVersion' => $this->softwareVersionService->getPanel(),
'version' => $this->softwareVersionService->currentPanelVersion(),
'latestVersion' => $this->softwareVersionService->latestPanelVersion(),
'isLatest' => $this->softwareVersionService->isLatestPanel(),
'eggsCount' => Egg::query()->count(),
'nodesList' => ListNodes::getUrl(),
@@ -65,13 +66,13 @@ class Dashboard extends Page
CreateAction::make()
->label(trans('dashboard/index.sections.intro-first-node.button_label'))
->icon('tabler-server-2')
->url(route('filament.admin.resources.nodes.create')),
->url(CreateNode::getUrl()),
],
'supportActions' => [
CreateAction::make()
->label(trans('dashboard/index.sections.intro-support.button_donate'))
->icon('tabler-cash')
->url($this->softwareVersionService->getDonations(), true)
->url('https://pelican.dev/donate', true)
->color('success'),
],
'helpActions' => [

View File

@@ -3,12 +3,12 @@
namespace App\Filament\Pages\Installer;
use App\Filament\Pages\Dashboard;
use App\Filament\Pages\Installer\Steps\AdminUserStep;
use App\Filament\Pages\Installer\Steps\CompletedStep;
use App\Filament\Pages\Installer\Steps\CacheStep;
use App\Filament\Pages\Installer\Steps\DatabaseStep;
use App\Filament\Pages\Installer\Steps\EnvironmentStep;
use App\Filament\Pages\Installer\Steps\RedisStep;
use App\Filament\Pages\Installer\Steps\QueueStep;
use App\Filament\Pages\Installer\Steps\RequirementsStep;
use App\Filament\Pages\Installer\Steps\SessionStep;
use App\Models\User;
use App\Services\Users\UserCreationService;
use App\Traits\CheckMigrationsTrait;
@@ -19,7 +19,6 @@ use Filament\Forms\Components\Wizard;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Notifications\Notification;
use Filament\Pages\SimplePage;
use Filament\Support\Enums\MaxWidth;
@@ -43,8 +42,6 @@ class PanelInstaller extends SimplePage implements HasForms
protected static string $view = 'filament.pages.installer';
private User $user;
public function getMaxWidth(): MaxWidth|string
{
return MaxWidth::SevenExtraLarge;
@@ -70,10 +67,9 @@ class PanelInstaller extends SimplePage implements HasForms
RequirementsStep::make(),
EnvironmentStep::make($this),
DatabaseStep::make($this),
RedisStep::make($this)
->hidden(fn (Get $get) => $get('env_general.SESSION_DRIVER') != 'redis' && $get('env_general.QUEUE_CONNECTION') != 'redis' && $get('env_general.CACHE_STORE') != 'redis'),
AdminUserStep::make($this),
CompletedStep::make(),
CacheStep::make($this),
QueueStep::make($this),
SessionStep::make(),
])
->persistStepInQueryString()
->nextAction(fn (Action $action) => $action->keyBindings('enter'))
@@ -95,14 +91,17 @@ class PanelInstaller extends SimplePage implements HasForms
return 'data';
}
public function submit(): Redirector|RedirectResponse
public function submit(UserCreationService $userCreationService): Redirector|RedirectResponse
{
// Disable installer
$this->writeToEnvironment(['APP_INSTALLED' => 'true']);
// Login user
$this->user ??= User::all()->filter(fn ($user) => $user->isRootAdmin())->first();
auth()->guard()->login($this->user, true);
// Create admin user & login
$user = $this->createAdminUser($userCreationService);
auth()->guard()->login($user, true);
// Write session data at the very end to avoid "page expired" errors
$this->writeToEnv('env_session');
// Redirect to admin panel
return redirect(Dashboard::getUrl());
@@ -112,6 +111,7 @@ class PanelInstaller extends SimplePage implements HasForms
{
try {
$variables = array_get($this->data, $key);
$variables = array_filter($variables); // Filter array to remove NULL values
$this->writeToEnvironment($variables);
} catch (Exception $exception) {
report($exception);
@@ -161,12 +161,13 @@ class PanelInstaller extends SimplePage implements HasForms
}
}
public function createAdminUser(UserCreationService $userCreationService): void
public function createAdminUser(UserCreationService $userCreationService): User
{
try {
$userData = array_get($this->data, 'user');
$userData['root_admin'] = true;
$this->user = $userCreationService->handle($userData);
return $userCreationService->handle($userData);
} catch (Exception $exception) {
report($exception);

View File

@@ -1,34 +0,0 @@
<?php
namespace App\Filament\Pages\Installer\Steps;
use App\Filament\Pages\Installer\PanelInstaller;
use App\Services\Users\UserCreationService;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
class AdminUserStep
{
public static function make(PanelInstaller $installer): Step
{
return Step::make('user')
->label('Admin User')
->schema([
TextInput::make('user.email')
->label('Admin E-Mail')
->required()
->email()
->placeholder('admin@example.com'),
TextInput::make('user.username')
->label('Admin Username')
->required()
->placeholder('admin'),
TextInput::make('user.password')
->label('Admin Password')
->required()
->password()
->revealable(),
])
->afterValidation(fn (UserCreationService $service) => $installer->createAdminUser($service));
}
}

View File

@@ -0,0 +1,123 @@
<?php
namespace App\Filament\Pages\Installer\Steps;
use App\Filament\Pages\Installer\PanelInstaller;
use Exception;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Components\Wizard\Step;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Notifications\Notification;
use Filament\Support\Exceptions\Halt;
use Illuminate\Foundation\Application;
use Illuminate\Redis\RedisManager;
class CacheStep
{
public const CACHE_DRIVERS = [
'file' => 'Filesystem',
'redis' => 'Redis',
];
public static function make(PanelInstaller $installer): Step
{
return Step::make('cache')
->label('Cache')
->columns()
->schema([
ToggleButtons::make('env_cache.CACHE_STORE')
->label('Cache Driver')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for caching. We recommend "Filesystem".')
->required()
->inline()
->options(self::CACHE_DRIVERS)
->default(config('cache.default'))
->columnSpanFull()
->live()
->afterStateUpdated(function ($state, Set $set, Get $get) {
if ($state !== 'redis') {
$set('env_cache.REDIS_HOST', null);
$set('env_cache.REDIS_PORT', null);
$set('env_cache.REDIS_USERNAME', null);
$set('env_cache.REDIS_PASSWORD', null);
} else {
$set('env_cache.REDIS_HOST', $get('env_cache.REDIS_HOST') ?? '127.0.0.1');
$set('env_cache.REDIS_PORT', $get('env_cache.REDIS_PORT') ?? '6379');
$set('env_cache.REDIS_USERNAME', null);
}
}),
TextInput::make('env_cache.REDIS_HOST')
->label('Redis Host')
->placeholder('127.0.0.1')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The host of your redis server. Make sure it is reachable.')
->required(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis')
->default(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis' ? config('database.redis.default.host') : null)
->visible(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis'),
TextInput::make('env_cache.REDIS_PORT')
->label('Redis Port')
->placeholder('6379')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The port of your redis server.')
->required(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis')
->default(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis' ? config('database.redis.default.port') : null)
->visible(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis'),
TextInput::make('env_cache.REDIS_USERNAME')
->label('Redis Username')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The name of your redis user. Can be empty')
->default(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis' ? config('database.redis.default.username') : null)
->visible(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis'),
TextInput::make('env_cache.REDIS_PASSWORD')
->label('Redis Password')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The password for your redis user. Can be empty.')
->password()
->revealable()
->default(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis' ? config('database.redis.default.password') : null)
->visible(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis'),
])
->afterValidation(function (Get $get, Application $app) use ($installer) {
$driver = $get('env_cache.CACHE_STORE');
if (!self::testConnection($app, $driver, $get('env_cache.REDIS_HOST'), $get('env_cache.REDIS_PORT'), $get('env_cache.REDIS_USERNAME'), $get('env_cache.REDIS_PASSWORD'))) {
throw new Halt('Redis connection failed');
}
$installer->writeToEnv('env_cache');
});
}
private static function testConnection(Application $app, string $driver, ?string $host, null|string|int $port, ?string $username, ?string $password): bool
{
if ($driver !== 'redis') {
return true;
}
try {
$redis = new RedisManager($app, 'predis', [
'default' => [
'host' => $host,
'port' => $port,
'username' => $username,
'password' => $password,
],
]);
$redis->connection()->command('ping');
} catch (Exception $exception) {
Notification::make()
->title('Redis connection failed')
->body($exception->getMessage())
->danger()
->send();
return false;
}
return true;
}
}

View File

@@ -1,34 +0,0 @@
<?php
namespace App\Filament\Pages\Installer\Steps;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
use Illuminate\Support\HtmlString;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
class CompletedStep
{
public static function make(): Step
{
return Step::make('complete')
->label('Setup complete')
->schema([
Placeholder::make('')
->content(new HtmlString('The setup is nearly complete!<br>As last step you need to create a new cronjob that runs every minute to process specific tasks, such as session cleanup and scheduled tasks, and also create a queue worker.')),
TextInput::make('crontab')
->label(new HtmlString('Run the following command to setup your crontab. Note that <code>www-data</code> is your webserver user. On some systems this username might be different!'))
->disabled()
->hintAction(CopyAction::make())
->default('(crontab -l -u www-data 2>/dev/null; echo "* * * * * php ' . base_path() . '/artisan schedule:run >> /dev/null 2>&1") | crontab -u www-data -'),
TextInput::make('queueService')
->label(new HtmlString('To setup the queue worker service you simply have to run the following command.'))
->disabled()
->hintAction(CopyAction::make())
->default('sudo php ' . base_path() . '/artisan p:environment:queue-service'),
Placeholder::make('')
->content('After you finished these two last tasks you can click on "Finish" and use your new panel! Have fun!'),
]);
}
}

View File

@@ -5,62 +5,92 @@ namespace App\Filament\Pages\Installer\Steps;
use App\Filament\Pages\Installer\PanelInstaller;
use Exception;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Components\Wizard\Step;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Notifications\Notification;
use Filament\Support\Exceptions\Halt;
use Illuminate\Support\Facades\DB;
class DatabaseStep
{
public const DATABASE_DRIVERS = [
'sqlite' => 'SQLite',
'mariadb' => 'MariaDB',
'mysql' => 'MySQL',
];
public static function make(PanelInstaller $installer): Step
{
return Step::make('database')
->label('Database')
->columns()
->schema([
TextInput::make('env_database.DB_DATABASE')
->label(fn (Get $get) => $get('env_general.DB_CONNECTION') === 'sqlite' ? 'Database Path' : 'Database Name')
->columnSpanFull()
ToggleButtons::make('env_database.DB_CONNECTION')
->label('Database Driver')
->hintIcon('tabler-question-mark')
->hintIconTooltip(fn (Get $get) => $get('env_general.DB_CONNECTION') === 'sqlite' ? 'The path of your .sqlite file relative to the database folder.' : 'The name of the panel database.')
->hintIconTooltip('The driver used for the panel database. We recommend "SQLite".')
->required()
->default(fn (Get $get) => env('DB_DATABASE', $get('env_general.DB_CONNECTION') === 'sqlite' ? 'database.sqlite' : 'panel')),
->inline()
->options(self::DATABASE_DRIVERS)
->default(config('database.default'))
->live()
->afterStateUpdated(function ($state, Set $set, Get $get) {
$set('env_database.DB_DATABASE', $state === 'sqlite' ? 'database.sqlite' : 'panel');
if ($state === 'sqlite') {
$set('env_database.DB_HOST', null);
$set('env_database.DB_PORT', null);
$set('env_database.DB_USERNAME', null);
$set('env_database.DB_PASSWORD', null);
} else {
$set('env_database.DB_HOST', $get('env_database.DB_HOST') ?? '127.0.0.1');
$set('env_database.DB_PORT', $get('env_database.DB_PORT') ?? '3306');
$set('env_database.DB_USERNAME', $get('env_database.DB_USERNAME') ?? 'pelican');
}
}),
TextInput::make('env_database.DB_DATABASE')
->label(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite' ? 'Database Path' : 'Database Name')
->placeholder(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite' ? 'database.sqlite' : 'panel')
->hintIcon('tabler-question-mark')
->hintIconTooltip(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite' ? 'The path of your .sqlite file relative to the database folder.' : 'The name of the panel database.')
->required()
->default('database.sqlite'),
TextInput::make('env_database.DB_HOST')
->label('Database Host')
->placeholder('127.0.0.1')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The host of your database. Make sure it is reachable.')
->required(fn (Get $get) => $get('env_general.DB_CONNECTION') !== 'sqlite')
->default(fn (Get $get) => $get('env_general.DB_CONNECTION') !== 'sqlite' ? env('DB_HOST', '127.0.0.1') : null)
->hidden(fn (Get $get) => $get('env_general.DB_CONNECTION') === 'sqlite'),
->required(fn (Get $get) => $get('env_database.DB_CONNECTION') !== 'sqlite')
->hidden(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite'),
TextInput::make('env_database.DB_PORT')
->label('Database Port')
->placeholder('3306')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The port of your database.')
->required(fn (Get $get) => $get('env_general.DB_CONNECTION') !== 'sqlite')
->numeric()
->minValue(1)
->maxValue(65535)
->default(fn (Get $get) => $get('env_general.DB_CONNECTION') !== 'sqlite' ? env('DB_PORT', 3306) : null)
->hidden(fn (Get $get) => $get('env_general.DB_CONNECTION') === 'sqlite'),
->required(fn (Get $get) => $get('env_database.DB_CONNECTION') !== 'sqlite')
->hidden(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite'),
TextInput::make('env_database.DB_USERNAME')
->label('Database Username')
->placeholder('pelican')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The name of your database user.')
->required(fn (Get $get) => $get('env_general.DB_CONNECTION') !== 'sqlite')
->default(fn (Get $get) => $get('env_general.DB_CONNECTION') !== 'sqlite' ? env('DB_USERNAME', 'pelican') : null)
->hidden(fn (Get $get) => $get('env_general.DB_CONNECTION') === 'sqlite'),
->required(fn (Get $get) => $get('env_database.DB_CONNECTION') !== 'sqlite')
->hidden(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite'),
TextInput::make('env_database.DB_PASSWORD')
->label('Database Password')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The password of your database user. Can be empty.')
->password()
->revealable()
->default(fn (Get $get) => $get('env_general.DB_CONNECTION') !== 'sqlite' ? env('DB_PASSWORD') : null)
->hidden(fn (Get $get) => $get('env_general.DB_CONNECTION') === 'sqlite'),
->hidden(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite'),
])
->afterValidation(function (Get $get) use ($installer) {
$driver = $get('env_general.DB_CONNECTION');
$driver = $get('env_database.DB_CONNECTION');
if (!self::testConnection($driver, $get('env_database.DB_HOST'), $get('env_database.DB_PORT'), $get('env_database.DB_DATABASE'), $get('env_database.DB_USERNAME'), $get('env_database.DB_PASSWORD'))) {
throw new Halt('Database connection failed');

View File

@@ -3,40 +3,12 @@
namespace App\Filament\Pages\Installer\Steps;
use App\Filament\Pages\Installer\PanelInstaller;
use App\Traits\EnvironmentWriterTrait;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Components\Wizard\Step;
use Filament\Forms\Set;
class EnvironmentStep
{
use EnvironmentWriterTrait;
public const CACHE_DRIVERS = [
'file' => 'Filesystem',
'redis' => 'Redis',
];
public const SESSION_DRIVERS = [
'file' => 'Filesystem',
'database' => 'Database',
'cookie' => 'Cookie',
'redis' => 'Redis',
];
public const QUEUE_DRIVERS = [
'database' => 'Database',
'sync' => 'Sync',
'redis' => 'Redis',
];
public const DATABASE_DRIVERS = [
'sqlite' => 'SQLite',
'mariadb' => 'MariaDB',
'mysql' => 'MySQL',
];
public static function make(PanelInstaller $installer): Step
{
return Step::make('environment')
@@ -54,44 +26,26 @@ class EnvironmentStep
->hintIcon('tabler-question-mark')
->hintIconTooltip('This will be the URL you access your Panel from.')
->required()
->default(url(''))
->live()
->afterStateUpdated(fn ($state, Set $set) => $set('env_general.SESSION_SECURE_COOKIE', str_starts_with($state, 'https://') ? 'true' : 'false')),
TextInput::make('env_general.SESSION_SECURE_COOKIE')
->hidden()
->default(str_starts_with(url(''), 'https://') ? 'true' : 'false'),
ToggleButtons::make('env_general.CACHE_STORE')
->label('Cache Driver')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for caching. We recommend "Filesystem".')
->required()
->inline()
->options(self::CACHE_DRIVERS)
->default(config('cache.default', 'file')),
ToggleButtons::make('env_general.SESSION_DRIVER')
->label('Session Driver')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for storing sessions. We recommend "Filesystem" or "Database".')
->required()
->inline()
->options(self::SESSION_DRIVERS)
->default(config('session.driver', 'file')),
ToggleButtons::make('env_general.QUEUE_CONNECTION')
->label('Queue Driver')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for handling queues. We recommend "Database".')
->required()
->inline()
->options(self::QUEUE_DRIVERS)
->default(config('queue.default', 'database')),
ToggleButtons::make('env_general.DB_CONNECTION')
->label('Database Driver')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for the panel database. We recommend "SQLite".')
->required()
->inline()
->options(self::DATABASE_DRIVERS)
->default(config('database.default', 'sqlite')),
->default(url('')),
Fieldset::make('adminuser')
->label('Admin User')
->columns(3)
->schema([
TextInput::make('user.email')
->label('E-Mail')
->required()
->email()
->placeholder('admin@example.com'),
TextInput::make('user.username')
->label('Username')
->required()
->placeholder('admin'),
TextInput::make('user.password')
->label('Password')
->required()
->password()
->revealable(),
]),
])
->afterValidation(fn () => $installer->writeToEnv('env_general'));
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Filament\Pages\Installer\Steps;
use App\Filament\Pages\Installer\PanelInstaller;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Components\Wizard\Step;
use Filament\Forms\Get;
use Illuminate\Support\HtmlString;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
class QueueStep
{
public const QUEUE_DRIVERS = [
'database' => 'Database',
'redis' => 'Redis',
'sync' => 'Sync',
];
public static function make(PanelInstaller $installer): Step
{
return Step::make('queue')
->label('Queue')
->columns()
->schema([
ToggleButtons::make('env_queue.QUEUE_CONNECTION')
->label('Queue Driver')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for handling queues. We recommend "Database".')
->required()
->inline()
->options(self::QUEUE_DRIVERS)
->disableOptionWhen(fn ($value, Get $get) => $value === 'redis' && $get('env_cache.CACHE_STORE') !== 'redis')
->default(config('queue.default')),
Toggle::make('done')
->label('I have done both steps below.')
->accepted(fn () => !file_exists('/.dockerenv'))
->inline(false)
->validationMessages([
'accepted' => 'You need to do both steps before continuing!',
])
->hidden(fn () => file_exists('/.dockerenv')),
TextInput::make('crontab')
->label(new HtmlString('Run the following command to set up your crontab. Note that <code>www-data</code> is your webserver user. On some systems this username might be different!'))
->disabled()
->hintAction(CopyAction::make())
->default('(crontab -l -u www-data 2>/dev/null; echo "* * * * * php ' . base_path() . '/artisan schedule:run >> /dev/null 2>&1") | crontab -u www-data -')
->hidden(fn () => file_exists('/.dockerenv'))
->columnSpanFull(),
TextInput::make('queueService')
->label(new HtmlString('To setup the queue worker service you simply have to run the following command.'))
->disabled()
->hintAction(CopyAction::make())
->default('sudo php ' . base_path() . '/artisan p:environment:queue-service')
->hidden(fn () => file_exists('/.dockerenv'))
->columnSpanFull(),
])
->afterValidation(function () use ($installer) {
$installer->writeToEnv('env_queue');
});
}
}

View File

@@ -1,82 +0,0 @@
<?php
namespace App\Filament\Pages\Installer\Steps;
use App\Filament\Pages\Installer\PanelInstaller;
use App\Traits\EnvironmentWriterTrait;
use Exception;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
use Filament\Forms\Get;
use Filament\Notifications\Notification;
use Filament\Support\Exceptions\Halt;
use Illuminate\Support\Facades\Redis;
class RedisStep
{
use EnvironmentWriterTrait;
public static function make(PanelInstaller $installer): Step
{
return Step::make('redis')
->label('Redis')
->columns()
->schema([
TextInput::make('env_redis.REDIS_HOST')
->label('Redis Host')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The host of your redis server. Make sure it is reachable.')
->required()
->default(config('database.redis.default.host')),
TextInput::make('env_redis.REDIS_PORT')
->label('Redis Port')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The port of your redis server.')
->required()
->default(config('database.redis.default.port')),
TextInput::make('env_redis.REDIS_USERNAME')
->label('Redis Username')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The name of your redis user. Can be empty')
->default(config('database.redis.default.username')),
TextInput::make('env_redis.REDIS_PASSWORD')
->label('Redis Password')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The password for your redis user. Can be empty.')
->password()
->revealable()
->default(config('database.redis.default.password')),
])
->afterValidation(function (Get $get) use ($installer) {
if (!self::testConnection($get('env_redis.REDIS_HOST'), $get('env_redis.REDIS_PORT'), $get('env_redis.REDIS_USERNAME'), $get('env_redis.REDIS_PASSWORD'))) {
throw new Halt('Redis connection failed');
}
$installer->writeToEnv('env_redis');
});
}
private static function testConnection(string $host, null|string|int $port, ?string $username, ?string $password): bool
{
try {
config()->set('database.redis._panel_install_test', [
'host' => $host,
'port' => $port,
'username' => $username,
'password' => $password,
]);
Redis::connection('_panel_install_test')->command('ping');
} catch (Exception $exception) {
Notification::make()
->title('Redis connection failed')
->body($exception->getMessage())
->danger()
->send();
return false;
}
return true;
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Filament\Pages\Installer\Steps;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Components\Wizard\Step;
use Filament\Forms\Get;
class SessionStep
{
public const SESSION_DRIVERS = [
'file' => 'Filesystem',
'database' => 'Database',
'cookie' => 'Cookie',
'redis' => 'Redis',
];
public static function make(): Step
{
return Step::make('session')
->label('Session')
->schema([
ToggleButtons::make('env_session.SESSION_DRIVER')
->label('Session Driver')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for storing sessions. We recommend "Filesystem" or "Database".')
->required()
->inline()
->options(self::SESSION_DRIVERS)
->disableOptionWhen(fn ($value, Get $get) => $value === 'redis' && $get('env_cache.CACHE_STORE') !== 'redis')
->default(config('session.driver')),
TextInput::make('env_session.SESSION_SECURE_COOKIE')
->hidden()
->default(request()->isSecure()),
]);
}
}

View File

@@ -8,6 +8,7 @@ use App\Traits\EnvironmentWriterTrait;
use Exception;
use Filament\Actions\Action;
use Filament\Forms\Components\Actions\Action as FormAction;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\Tabs\Tab;
@@ -24,8 +25,11 @@ use Filament\Notifications\Notification;
use Filament\Pages\Concerns\HasUnsavedDataChangesAlert;
use Filament\Pages\Concerns\InteractsWithHeaderActions;
use Filament\Pages\Page;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Notification as MailNotification;
use Illuminate\Support\HtmlString;
/**
* @property Form $form
@@ -67,10 +71,11 @@ class Settings extends Page implements HasForms
->label('General')
->icon('tabler-home')
->schema($this->generalSettings()),
Tab::make('recaptcha')
->label('reCAPTCHA')
Tab::make('captcha')
->label('Captcha')
->icon('tabler-shield')
->schema($this->recaptchaSettings()),
->schema($this->captchaSettings())
->columns(3),
Tab::make('mail')
->label('Mail')
->icon('tabler-mail')
@@ -146,7 +151,7 @@ class Settings extends Page implements HasForms
->separator()
->splitKeys(['Tab', ' '])
->placeholder('New IP or IP Range')
->default(env('TRUSTED_PROXIES', config('trustedproxy.proxies')))
->default(env('TRUSTED_PROXIES', implode(',', config('trustedproxy.proxies'))))
->hintActions([
FormAction::make('clear')
->label('Clear')
@@ -159,56 +164,71 @@ class Settings extends Page implements HasForms
->label('Set to Cloudflare IPs')
->icon('tabler-brand-cloudflare')
->authorize(fn () => auth()->user()->can('update settings'))
->action(fn (Set $set) => $set('TRUSTED_PROXIES', [
'173.245.48.0/20',
'103.21.244.0/22',
'103.22.200.0/22',
'103.31.4.0/22',
'141.101.64.0/18',
'108.162.192.0/18',
'190.93.240.0/20',
'188.114.96.0/20',
'197.234.240.0/22',
'198.41.128.0/17',
'162.158.0.0/15',
'104.16.0.0/13',
'104.24.0.0/14',
'172.64.0.0/13',
'131.0.72.0/22',
])),
->action(function (Client $client, Set $set) {
$ips = collect();
try {
$response = $client->request(
'GET',
'https://api.cloudflare.com/client/v4/ips',
config('panel.guzzle')
);
if ($response->getStatusCode() === 200) {
$result = json_decode($response->getBody(), true)['result'];
foreach (['ipv4_cidrs', 'ipv6_cidrs'] as $value) {
$ips->push(...data_get($result, $value));
}
$ips->unique();
}
} catch (GuzzleException $e) {
}
$set('TRUSTED_PROXIES', $ips->values()->all());
}),
]),
];
}
private function recaptchaSettings(): array
private function captchaSettings(): array
{
return [
Toggle::make('RECAPTCHA_ENABLED')
->label('Enable reCAPTCHA?')
Toggle::make('TURNSTILE_ENABLED')
->label('Enable Turnstile Captcha?')
->inline(false)
->columnSpan(1)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->live()
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('RECAPTCHA_ENABLED', (bool) $state))
->default(env('RECAPTCHA_ENABLED', config('recaptcha.enabled'))),
TextInput::make('RECAPTCHA_DOMAIN')
->label('Domain')
->afterStateUpdated(fn ($state, Set $set) => $set('TURNSTILE_ENABLED', (bool) $state))
->default(env('TURNSTILE_ENABLED', config('turnstile.turnstile_enabled'))),
Placeholder::make('info')
->columnSpan(2)
->content(new HtmlString('<p>You can generate the keys on your <u><a href="https://developers.cloudflare.com/turnstile/get-started/#get-a-sitekey-and-secret-key" target="_blank">Cloudflare Dashboard</a></u>. A Cloudflare account is required.</p>')),
TextInput::make('TURNSTILE_SITE_KEY')
->label('Site Key')
->required()
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
->default(env('RECAPTCHA_DOMAIN', config('recaptcha.domain'))),
TextInput::make('RECAPTCHA_WEBSITE_KEY')
->label('Website Key')
->required()
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
->default(env('RECAPTCHA_WEBSITE_KEY', config('recaptcha.website_key'))),
TextInput::make('RECAPTCHA_SECRET_KEY')
->visible(fn (Get $get) => $get('TURNSTILE_ENABLED'))
->default(env('TURNSTILE_SITE_KEY', config('turnstile.turnstile_site_key')))
->placeholder('1x00000000000000000000AA'),
TextInput::make('TURNSTILE_SECRET_KEY')
->label('Secret Key')
->required()
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
->default(env('RECAPTCHA_SECRET_KEY', config('recaptcha.secret_key'))),
->visible(fn (Get $get) => $get('TURNSTILE_ENABLED'))
->default(env('TURNSTILE_SECRET_KEY', config('turnstile.secret_key')))
->placeholder('1x0000000000000000000000000000000AA'),
Toggle::make('TURNSTILE_VERIFY_DOMAIN')
->label('Verify domain?')
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->visible(fn (Get $get) => $get('TURNSTILE_ENABLED'))
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('TURNSTILE_VERIFY_DOMAIN', (bool) $state))
->default(env('TURNSTILE_VERIFY_DOMAIN', config('turnstile.turnstile_verify_domain'))),
];
}
@@ -540,7 +560,21 @@ class Settings extends Page implements HasForms
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_EDITABLE_SERVER_DESCRIPTIONS', (bool) $state))
->default(env('PANEL_EDITABLE_SERVER_DESCRIPTIONS', config('panel.editable_server_descriptions'))),
]),
Section::make('Webhook')
->description('Configure how often old webhook logs should be pruned.')
->columns()
->collapsible()
->collapsed()
->schema([
TextInput::make('APP_WEBHOOK_PRUNE_DAYS')
->label('Prune age')
->required()
->numeric()
->minValue(1)
->maxValue(365)
->suffix('Days')
->default(env('APP_WEBHOOK_PRUNE_DAYS', config('panel.webhook.prune_days'))),
]),
];
}

View File

@@ -11,6 +11,7 @@ use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Form;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Database\Eloquent\Model;
class CreateApiKey extends CreateRecord
{
@@ -41,7 +42,7 @@ class CreateApiKey extends CreateRecord
'md' => 2,
])
->schema(
collect(ApiKey::RESOURCES)->map(fn ($resource) => ToggleButtons::make("r_$resource")
collect(ApiKey::getPermissionList())->map(fn ($resource) => ToggleButtons::make('permissions_' . $resource)
->label(str($resource)->replace('_', ' ')->title())->inline()
->options([
0 => 'None',
@@ -87,4 +88,20 @@ class CreateApiKey extends CreateRecord
->columnSpanFull(),
]);
}
protected function handleRecordCreation(array $data): Model
{
$permissions = [];
foreach (ApiKey::getPermissionList() as $permission) {
if (isset($data['permissions_' . $permission])) {
$permissions[$permission] = intval($data['permissions_' . $permission]);
unset($data['permissions_' . $permission]);
}
}
$data['permissions'] = $permissions;
return parent::handleRecordCreation($data);
}
}

View File

@@ -142,6 +142,7 @@ class ListEggs extends ListRecords
} catch (Exception $exception) {
Notification::make()
->title('Import Failed')
->body($exception->getMessage())
->danger()
->send();
@@ -158,6 +159,7 @@ class ListEggs extends ListRecords
} catch (Exception $exception) {
Notification::make()
->title('Import Failed')
->body($exception->getMessage())
->danger()
->send();

View File

@@ -4,6 +4,7 @@ namespace App\Filament\Resources\NodeResource\Pages;
use App\Filament\Resources\NodeResource;
use App\Models\Node;
use App\Services\Helpers\SoftwareVersionService;
use App\Services\Nodes\NodeAutoDeployService;
use App\Services\Nodes\NodeUpdateService;
use Filament\Actions;
@@ -55,7 +56,7 @@ class EditNode extends EditRecord
->schema([
Placeholder::make('')
->label('Wings Version')
->content(fn (Node $node) => $node->systemInformation()['version'] ?? 'Unknown'),
->content(fn (Node $node, SoftwareVersionService $versionService) => ($node->systemInformation()['version'] ?? 'Unknown') . ' (Latest: ' . $versionService->latestWingsVersion() . ')'),
Placeholder::make('')
->label('CPU Threads')
->content(fn (Node $node) => $node->systemInformation()['cpu_count'] ?? 0),

View File

@@ -48,6 +48,7 @@ class AllocationsRelationManager extends RelationManager
// All assigned allocations
->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->server_id === null)
->searchable()
->selectCurrentPageOnly() //Prevent people from trying to nuke 30,000 ports at once.... -,-
->columns([
TextColumn::make('id'),
TextColumn::make('port')
@@ -65,12 +66,6 @@ class AllocationsRelationManager extends RelationManager
->searchable()
->label('IP'),
])
->filters([
//
])
->actions([
//
])
->headerActions([
Tables\Actions\Action::make('create new allocation')->label('Create Allocations')
->form(fn () => [

View File

@@ -6,7 +6,6 @@ use App\Enums\RolePermissionModels;
use App\Enums\RolePermissionPrefixes;
use App\Filament\Resources\RoleResource\Pages;
use App\Models\Role;
use Filament\Facades\Filament;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\Component;
@@ -71,7 +70,7 @@ class RoleResource extends Resource
->disabled(fn (Get $get) => $get('name') === Role::ROOT_ADMIN),
TextInput::make('guard_name')
->label('Guard Name')
->default(Filament::getCurrentPanel()?->getAuthGuard() ?? '')
->default(Role::DEFAULT_GUARD_NAME)
->nullable()
->hidden(),
Fieldset::make('Permissions')

View File

@@ -787,6 +787,7 @@ class CreateServer extends CreateRecord
->schema([
Select::make('select_image')
->label('Image Name')
->live()
->afterStateUpdated(fn (Set $set, $state) => $set('image', $state))
->options(function ($state, Get $get, Set $set) {
$egg = Egg::query()->find($get('egg_id'));
@@ -811,7 +812,7 @@ class CreateServer extends CreateRecord
TextInput::make('image')
->label('Image')
->debounce(500)
->required()
->afterStateUpdated(function ($state, Get $get, Set $set) {
$egg = Egg::query()->find($get('egg_id'));
$images = $egg->docker_images ?? [];

View File

@@ -5,16 +5,19 @@ namespace App\Filament\Resources\ServerResource\Pages;
use App\Enums\ContainerStatus;
use App\Enums\ServerState;
use App\Filament\Resources\ServerResource;
use App\Http\Controllers\Admin\ServersController;
use App\Filament\Resources\ServerResource\RelationManagers\AllocationsRelationManager;
use App\Models\Database;
use App\Models\Egg;
use App\Models\Mount;
use App\Models\Server;
use App\Models\ServerVariable;
use App\Services\Databases\DatabaseManagementService;
use App\Services\Databases\DatabasePasswordService;
use App\Services\Servers\RandomWordService;
use App\Services\Servers\ReinstallServerService;
use App\Services\Servers\ServerDeletionService;
use App\Services\Servers\SuspensionService;
use App\Services\Servers\ToggleInstallService;
use App\Services\Servers\TransferServerService;
use Closure;
use Exception;
@@ -25,6 +28,7 @@ use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\KeyValue;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Tabs;
@@ -417,6 +421,7 @@ class EditServer extends EditRecord
->schema([
Select::make('select_image')
->label('Image Name')
->live()
->afterStateUpdated(fn (Set $set, $state) => $set('image', $state))
->options(function ($state, Get $get, Set $set) {
$egg = Egg::query()->find($get('egg_id'));
@@ -436,7 +441,7 @@ class EditServer extends EditRecord
TextInput::make('image')
->label('Image')
->debounce(500)
->required()
->afterStateUpdated(function ($state, Get $get, Set $set) {
$egg = Egg::query()->find($get('egg_id'));
$images = $egg->docker_images ?? [];
@@ -450,7 +455,7 @@ class EditServer extends EditRecord
->placeholder('Enter a custom Image')
->columnSpan(2),
Forms\Components\KeyValue::make('docker_labels')
KeyValue::make('docker_labels')
->label('Container Labels')
->keyLabel('Label Name')
->valueLabel('Label Description')
@@ -596,8 +601,8 @@ class EditServer extends EditRecord
->schema([
CheckboxList::make('mounts')
->relationship('mounts')
->options(fn (Server $server) => $server->node->mounts->mapWithKeys(fn ($mount) => [$mount->id => $mount->name]))
->descriptions(fn (Server $server) => $server->node->mounts->mapWithKeys(fn ($mount) => [$mount->id => "$mount->source -> $mount->target"]))
->options(fn (Server $server) => $server->node->mounts->filter(fn (Mount $mount) => $mount->eggs->contains($server->egg))->mapWithKeys(fn (Mount $mount) => [$mount->id => $mount->name]))
->descriptions(fn (Server $server) => $server->node->mounts->mapWithKeys(fn (Mount $mount) => [$mount->id => "$mount->source -> $mount->target"]))
->label('Mounts')
->helperText(fn (Server $server) => $server->node->mounts->isNotEmpty() ? '' : 'No Mounts exist for this Node')
->columnSpanFull(),
@@ -673,8 +678,8 @@ class EditServer extends EditRecord
Action::make('toggleInstall')
->label('Toggle Install Status')
->disabled(fn (Server $server) => $server->isSuspended())
->action(function (ServersController $serversController, Server $server) {
$serversController->toggleInstall($server);
->action(function (ToggleInstallService $service, Server $server) {
$service->handle($server);
$this->refreshFormData(['status', 'docker']);
}),
@@ -760,7 +765,7 @@ class EditServer extends EditRecord
->modalHeading('Are you sure you want to reinstall this server?')
->modalDescription('!! This can result in unrecoverable data loss !!')
->disabled(fn (Server $server) => $server->isSuspended())
->action(fn (ServersController $serversController, Server $server) => $serversController->reinstallServer($server)),
->action(fn (ReinstallServerService $service, Server $server) => $service->handle($server)),
])->fullWidth(),
ToggleButtons::make('')
->hint('This will reinstall the server with the assigned egg install script.'),
@@ -826,7 +831,7 @@ class EditServer extends EditRecord
public function getRelationManagers(): array
{
return [
ServerResource\RelationManagers\AllocationsRelationManager::class,
AllocationsRelationManager::class,
];
}

View File

@@ -40,6 +40,7 @@ class AllocationsRelationManager extends RelationManager
public function table(Table $table): Table
{
return $table
->selectCurrentPageOnly()
->recordTitleAttribute('ip')
->recordTitle(fn (Allocation $allocation) => "$allocation->ip:$allocation->port")
->checkIfRecordIsSelectableUsing(fn (Allocation $record) => $record->id !== $this->getOwnerRecord()->allocation_id)
@@ -149,7 +150,7 @@ class AllocationsRelationManager extends RelationManager
->multiple()
->associateAnother(false)
->preloadRecordSelect()
->recordSelectOptionsQuery(fn ($query) => $query->whereBelongsTo($this->getOwnerRecord()->node))
->recordSelectOptionsQuery(fn ($query) => $query->whereBelongsTo($this->getOwnerRecord()->node)->whereNull('server_id'))
->label('Add Allocation'),
])
->bulkActions([

View File

@@ -91,6 +91,7 @@ class ListUsers extends ListRecords
TextInput::make('username')
->alphaNum()
->required()
->unique()
->maxLength(255),
TextInput::make('email')
->email()

View File

@@ -4,13 +4,7 @@ namespace App\Filament\Resources;
use App\Filament\Resources\WebhookResource\Pages;
use App\Models\WebhookConfiguration;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class WebhookResource extends Resource
{
@@ -22,41 +16,9 @@ class WebhookResource extends Resource
protected static ?string $label = 'Webhooks';
public static function form(Form $form): Form
public static function getNavigationBadge(): ?string
{
return $form
->schema([
TextInput::make('endpoint')->activeUrl()->required(),
TextInput::make('description')->nullable(),
CheckboxList::make('events')->lazy()->options(
fn () => WebhookConfiguration::filamentCheckboxList()
)
->searchable()
->bulkToggleable()
->columns(3)
->columnSpanFull()
->gridDirection('row')
->required(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('description'),
TextColumn::make('endpoint'),
])
->actions([
Tables\Actions\EditAction::make(),
]);
}
public static function getRelations(): array
{
return [
//
];
return static::getModel()::count() ?: null;
}
public static function getPages(): array

View File

@@ -3,9 +3,34 @@
namespace App\Filament\Resources\WebhookResource\Pages;
use App\Filament\Resources\WebhookResource;
use App\Models\WebhookConfiguration;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Pages\CreateRecord;
class CreateWebhookConfiguration extends CreateRecord
{
protected static string $resource = WebhookResource::class;
public function form(Form $form): Form
{
return $form
->schema([
TextInput::make('endpoint')
->activeUrl()
->required(),
TextInput::make('description')
->required(),
CheckboxList::make('events')
->lazy()
->options(fn () => WebhookConfiguration::filamentCheckboxList())
->searchable()
->bulkToggleable()
->columns(3)
->columnSpanFull()
->gridDirection('row')
->required(),
]);
}
}

View File

@@ -2,18 +2,56 @@
namespace App\Filament\Resources\WebhookResource\Pages;
use App\Models\WebhookConfiguration;
use App\Filament\Resources\WebhookResource;
use Filament\Actions;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Pages\EditRecord;
class EditWebhookConfiguration extends EditRecord
{
protected static string $resource = WebhookResource::class;
public function form(Form $form): Form
{
return $form
->schema([
TextInput::make('endpoint')
->label('Endpoint')
->activeUrl()
->required(),
TextInput::make('description')
->label('Description')
->required(),
CheckboxList::make('events')
->label('Events')
->lazy()
->options(fn () => WebhookConfiguration::filamentCheckboxList())
->searchable()
->bulkToggleable()
->columns(3)
->columnSpanFull()
->gridDirection('row')
->required(),
]);
}
protected function getFormActions(): array
{
return [];
}
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
Actions\DeleteAction::make()
->label('Delete')
->modalHeading('Are you sure you want to delete this?')
->modalDescription('')
->modalSubmitActionLabel('Delete'),
$this->getSaveFormAction()->formId('form'),
];
}
}

View File

@@ -3,17 +3,50 @@
namespace App\Filament\Resources\WebhookResource\Pages;
use App\Filament\Resources\WebhookResource;
use App\Models\WebhookConfiguration;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\CreateAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Actions\DeleteAction;
class ListWebhookConfigurations extends ListRecords
{
protected static string $resource = WebhookResource::class;
public function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('description')
->label('Description'),
TextColumn::make('endpoint')
->label('Endpoint'),
])
->actions([
DeleteAction::make()
->label('Delete'),
EditAction::make()
->label('Edit'),
])
->emptyStateIcon('tabler-webhook')
->emptyStateDescription('')
->emptyStateHeading('No Webhooks')
->emptyStateActions([
CreateAction::make('create')
->label('Create Webhook')
->button(),
]);
}
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
Actions\CreateAction::make()
->label('Create Webhook')
->hidden(fn () => WebhookConfiguration::count() <= 0),
];
}
}

View File

@@ -1,20 +0,0 @@
<?php
namespace App\Helpers;
use Carbon\CarbonImmutable;
final class Time
{
/**
* Gets the time offset from the provided timezone relative to UTC as a number. This
* is used in the database configuration since we can't always rely on there being support
* for named timezones in MySQL.
*
* Returns the timezone as a string like +08:00 or -05:00 depending on the app timezone.
*/
public static function getMySQLTimezoneOffset(string $timezone): string
{
return CarbonImmutable::now($timezone)->getTimezone()->toOffsetName();
}
}

View File

@@ -1,90 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use Illuminate\View\View;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Models\ApiKey;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Controllers\Controller;
use App\Services\Api\KeyCreationService;
use App\Http\Requests\Admin\Api\StoreApplicationApiKeyRequest;
class ApiController extends Controller
{
/**
* ApiController constructor.
*/
public function __construct(
private AlertsMessageBag $alert,
private KeyCreationService $keyCreationService,
) {
}
/**
* Render view showing all of a user's application API keys.
*/
public function index(Request $request): View
{
$keys = $request->user()->apiKeys()
->where('key_type', ApiKey::TYPE_APPLICATION)
->get();
return view('admin.api.index', [
'keys' => $keys,
]);
}
/**
* Render view allowing an admin to create a new application API key.
*
* @throws \ReflectionException
*/
public function create(): View
{
$resources = AdminAcl::getResourceList();
sort($resources);
return view('admin.api.new', [
'resources' => $resources,
'permissions' => [
'r' => AdminAcl::READ,
'rw' => AdminAcl::READ | AdminAcl::WRITE,
'n' => AdminAcl::NONE,
],
]);
}
/**
* Store the new key and redirect the user back to the application key listing.
*
* @throws \App\Exceptions\Model\DataValidationException
*/
public function store(StoreApplicationApiKeyRequest $request): RedirectResponse
{
$this->keyCreationService->setKeyType(ApiKey::TYPE_APPLICATION)->handle([
'memo' => $request->input('memo'),
'user_id' => $request->user()->id,
], $request->getKeyPermissions());
$this->alert->success('A new application API key has been generated for your account.')->flash();
return redirect()->route('admin.api.index');
}
/**
* Delete an application API key from the database.
*/
public function delete(Request $request, string $identifier): Response
{
$request->user()->apiKeys()
->where('key_type', ApiKey::TYPE_APPLICATION)
->where('identifier', $identifier)
->delete();
return response('', 204);
}
}

View File

@@ -1,25 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use Illuminate\View\View;
use App\Http\Controllers\Controller;
use App\Services\Helpers\SoftwareVersionService;
class BaseController extends Controller
{
/**
* BaseController constructor.
*/
public function __construct(private SoftwareVersionService $version)
{
}
/**
* Return the admin index view.
*/
public function index(): View
{
return view('admin.index', ['version' => $this->version]);
}
}

View File

@@ -1,126 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Models\Node;
use Illuminate\View\View;
use App\Models\DatabaseHost;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use App\Http\Controllers\Controller;
use App\Services\Databases\Hosts\HostUpdateService;
use App\Http\Requests\Admin\DatabaseHostFormRequest;
use App\Services\Databases\Hosts\HostCreationService;
use App\Services\Databases\Hosts\HostDeletionService;
class DatabaseController extends Controller
{
/**
* DatabaseController constructor.
*/
public function __construct(
private AlertsMessageBag $alert,
private HostCreationService $creationService,
private HostDeletionService $deletionService,
private HostUpdateService $updateService,
) {
}
/**
* Display database host index.
*/
public function index(): View
{
$hosts = DatabaseHost::query()
->withCount('databases')
->with('node')
->get();
return view('admin.databases.index', [
'nodes' => Node::all(),
'hosts' => $hosts,
]);
}
/**
* Display database host to user.
*/
public function view(DatabaseHost $host): View
{
$databases = $host->databases()->with('server')->paginate(25);
return view('admin.databases.view', [
'nodes' => Node::all(),
'host' => $host,
'databases' => $databases,
]);
}
/**
* Handle request to create a new database host.
*
* @throws \Throwable
*/
public function create(DatabaseHostFormRequest $request): RedirectResponse
{
try {
$host = $this->creationService->handle($request->normalize());
} catch (\Exception $exception) {
if ($exception instanceof \PDOException || $exception->getPrevious() instanceof \PDOException) {
$this->alert->danger(
sprintf('There was an error while trying to connect to the host or while executing a query: "%s"', $exception->getMessage())
)->flash();
return redirect()->route('admin.databases')->withInput($request->validated());
} else {
throw $exception;
}
}
$this->alert->success('Successfully created a new database host on the system.')->flash();
return redirect()->route('admin.databases.view', $host->id);
}
/**
* Handle updating database host.
*
* @throws \Throwable
*/
public function update(DatabaseHostFormRequest $request, DatabaseHost $host): RedirectResponse
{
$redirect = redirect()->route('admin.databases.view', $host->id);
try {
$this->updateService->handle($host->id, $request->normalize());
$this->alert->success('Database host was updated successfully.')->flash();
} catch (\Exception $exception) {
// Catch any SQL related exceptions and display them back to the user, otherwise just
// throw the exception like normal and move on with it.
if ($exception instanceof \PDOException || $exception->getPrevious() instanceof \PDOException) {
$this->alert->danger(
sprintf('There was an error while trying to connect to the host or while executing a query: "%s"', $exception->getMessage())
)->flash();
return $redirect->withInput($request->normalize());
} else {
throw $exception;
}
}
return $redirect;
}
/**
* Handle request to delete a database host.
*
* @throws \App\Exceptions\Service\HasActiveServersException
*/
public function delete(int $host): RedirectResponse
{
$this->deletionService->handle($host);
$this->alert->success('The requested database host has been deleted from the system.')->flash();
return redirect()->route('admin.databases');
}
}

View File

@@ -1,144 +0,0 @@
<?php
namespace App\Http\Controllers\Admin\Eggs;
use App\Exceptions\Service\Egg\NoParentConfigurationFoundException;
use Illuminate\View\View;
use App\Models\Egg;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\View\Factory as ViewFactory;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Egg\EggFormRequest;
use Ramsey\Uuid\Uuid;
class EggController extends Controller
{
/**
* EggController constructor.
*/
public function __construct(
protected AlertsMessageBag $alert,
protected ViewFactory $view
) {
}
/**
* Render eggs listing page.
*/
public function index(): View
{
return view('admin.eggs.index', [
'eggs' => Egg::all(),
]);
}
/**
* Handle a request to display the Egg creation page.
*/
public function create(): View
{
$eggs = Egg::all();
\JavaScript::put(['eggs' => $eggs->keyBy('id')]);
return view('admin.eggs.new', ['eggs' => $eggs]);
}
/**
* Handle request to store a new Egg.
*
* @throws \App\Exceptions\Model\DataValidationException
* @throws \App\Exceptions\Service\Egg\NoParentConfigurationFoundException
*/
public function store(EggFormRequest $request): RedirectResponse
{
$data = $request->validated();
$data['docker_images'] = $this->normalizeDockerImages($data['docker_images'] ?? null);
$data['author'] = $request->user()->email;
$data['config_from'] = array_get($data, 'config_from');
if (!is_null($data['config_from'])) {
$parentEgg = Egg::query()->find(array_get($data, 'config_from'));
throw_unless($parentEgg, new NoParentConfigurationFoundException(trans('exceptions.egg.invalid_copy_id')));
}
$egg = Egg::query()->create(array_merge($data, [
'uuid' => Uuid::uuid4()->toString(),
]));
$this->alert->success(trans('admin/eggs.notices.egg_created'))->flash();
return redirect()->route('admin.eggs.view', $egg->id);
}
/**
* Handle request to view a single Egg.
*/
public function view(Egg $egg): View
{
return view('admin.eggs.view', [
'egg' => $egg,
'images' => array_map(
fn ($key, $value) => $key === $value ? $value : "$key|$value",
array_keys($egg->docker_images),
$egg->docker_images,
),
]);
}
/**
* Handle request to update an Egg.
*
* @throws \App\Exceptions\Model\DataValidationException
* @throws \App\Exceptions\Service\Egg\NoParentConfigurationFoundException
*/
public function update(EggFormRequest $request, Egg $egg): RedirectResponse
{
$data = $request->validated();
$data['docker_images'] = $this->normalizeDockerImages($data['docker_images'] ?? null);
$eggId = array_get($data, 'config_from');
$copiedFromEgg = Egg::query()->find($eggId);
throw_unless($copiedFromEgg, new NoParentConfigurationFoundException(trans('exceptions.egg.invalid_copy_id')));
$egg->update($data);
$this->alert->success(trans('admin/eggs.notices.updated'))->flash();
return redirect()->route('admin.eggs.view', $egg->id);
}
/**
* Handle request to destroy an egg.
*
* @throws \App\Exceptions\Service\Egg\HasChildrenException
* @throws \App\Exceptions\Service\HasActiveServersException
*/
public function destroy(Egg $egg): RedirectResponse
{
$egg->delete();
$this->alert->success(trans('admin/eggs.notices.deleted'))->flash();
return redirect()->route('admin.eggs.view', $egg->id);
}
/**
* Normalizes a string of docker image data into the expected egg format.
*/
protected function normalizeDockerImages(?string $input = null): array
{
$data = array_map(fn ($value) => trim($value), explode("\n", $input ?? ''));
$images = [];
// Iterate over the image data provided and convert it into a name => image
// pairing that is used to improve the display on the front-end.
foreach ($data as $value) {
$parts = explode('|', $value, 2);
$images[$parts[0]] = empty($parts[1]) ? $parts[0] : $parts[1];
}
return $images;
}
}

View File

@@ -1,61 +0,0 @@
<?php
namespace App\Http\Controllers\Admin\Eggs;
use Illuminate\View\View;
use App\Models\Egg;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\View\Factory as ViewFactory;
use App\Http\Controllers\Controller;
use App\Services\Eggs\Scripts\InstallScriptService;
use App\Http\Requests\Admin\Egg\EggScriptFormRequest;
class EggScriptController extends Controller
{
/**
* EggScriptController constructor.
*/
public function __construct(
protected AlertsMessageBag $alert,
protected InstallScriptService $installScriptService,
protected ViewFactory $view
) {
}
/**
* Handle requests to render installation script for an Egg.
*/
public function index(int $egg): View
{
$egg = Egg::with('scriptFrom', 'configFrom')
->where('id', $egg)
->firstOrFail();
$copy = Egg::query()
->whereNull('copy_script_from')
->whereNot('id', $egg->id)
->firstOrFail();
$rely = Egg::query()->where('copy_script_from', $egg->id)->firstOrFail();
return view('admin.eggs.scripts', [
'copyFromOptions' => $copy,
'relyOnScript' => $rely,
'egg' => $egg,
]);
}
/**
* Handle a request to update the installation script for an Egg.
*
* @throws \App\Exceptions\Model\DataValidationException
*/
public function update(EggScriptFormRequest $request, Egg $egg): RedirectResponse
{
$this->installScriptService->handle($egg, $request->normalize());
$this->alert->success(trans('admin/eggs.notices.script_updated'))->flash();
return redirect()->route('admin.eggs.scripts', $egg);
}
}

View File

@@ -1,67 +0,0 @@
<?php
namespace App\Http\Controllers\Admin\Eggs;
use App\Models\Egg;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use App\Http\Controllers\Controller;
use Symfony\Component\HttpFoundation\Response;
use App\Services\Eggs\Sharing\EggExporterService;
use App\Services\Eggs\Sharing\EggImporterService;
use App\Http\Requests\Admin\Egg\EggImportFormRequest;
class EggShareController extends Controller
{
/**
* EggShareController constructor.
*/
public function __construct(
protected AlertsMessageBag $alert,
protected EggExporterService $exporterService,
protected EggImporterService $importerService,
) {
}
public function export(Egg $egg): Response
{
$filename = trim(preg_replace('/\W/', '-', kebab_case($egg->name)), '-');
return response($this->exporterService->handle($egg->id), 200, [
'Content-Transfer-Encoding' => 'binary',
'Content-Description' => 'File Transfer',
'Content-Disposition' => 'attachment; filename=egg-' . $filename . '.json',
'Content-Type' => 'application/json',
]);
}
/**
* Import a new egg using an XML file.
*
* @throws \App\Exceptions\Model\DataValidationException
* @throws \App\Exceptions\Service\Egg\BadJsonFormatException
* @throws \App\Exceptions\Service\InvalidFileUploadException
*/
public function import(EggImportFormRequest $request): RedirectResponse
{
$egg = $this->importerService->fromFile($request->file('import_file'));
$this->alert->success(trans('admin/eggs.notices.imported'))->flash();
return redirect()->route('admin.eggs.view', ['egg' => $egg->id]);
}
/**
* Update an existing Egg using a new imported file.
*
* @throws \App\Exceptions\Model\DataValidationException
* @throws \App\Exceptions\Service\Egg\BadJsonFormatException
* @throws \App\Exceptions\Service\InvalidFileUploadException
*/
public function update(EggImportFormRequest $request, Egg $egg): RedirectResponse
{
$this->importerService->fromFile($request->file('import_file'), $egg);
$this->alert->success(trans('admin/eggs.notices.updated_via_import'))->flash();
return redirect()->route('admin.eggs.view', ['egg' => $egg]);
}
}

View File

@@ -1,83 +0,0 @@
<?php
namespace App\Http\Controllers\Admin\Eggs;
use Illuminate\View\View;
use App\Models\Egg;
use App\Models\EggVariable;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\View\Factory as ViewFactory;
use App\Http\Controllers\Controller;
use App\Services\Eggs\Variables\VariableUpdateService;
use App\Http\Requests\Admin\Egg\EggVariableFormRequest;
use App\Services\Eggs\Variables\VariableCreationService;
class EggVariableController extends Controller
{
/**
* EggVariableController constructor.
*/
public function __construct(
protected AlertsMessageBag $alert,
protected VariableCreationService $creationService,
protected VariableUpdateService $updateService,
protected ViewFactory $view
) {
}
/**
* Handle request to view the variables attached to an Egg.
*/
public function view(int $egg): View
{
$egg = Egg::with('variables')->findOrFail($egg);
return view('admin.eggs.variables', ['egg' => $egg]);
}
/**
* Handle a request to create a new Egg variable.
*
* @throws \App\Exceptions\Model\DataValidationException
* @throws \App\Exceptions\Service\Egg\Variable\BadValidationRuleException
* @throws \App\Exceptions\Service\Egg\Variable\ReservedVariableNameException
*/
public function store(EggVariableFormRequest $request, Egg $egg): RedirectResponse
{
$this->creationService->handle($egg->id, $request->normalize());
$this->alert->success(trans('admin/eggs.variables.notices.variable_created'))->flash();
return redirect()->route('admin.eggs.variables', $egg->id);
}
/**
* Handle a request to update an existing Egg variable.
*
* @throws \App\Exceptions\DisplayException
* @throws \App\Exceptions\Model\DataValidationException
* @throws \App\Exceptions\Service\Egg\Variable\ReservedVariableNameException
*/
public function update(EggVariableFormRequest $request, Egg $egg, EggVariable $variable): RedirectResponse
{
$this->updateService->handle($variable, $request->normalize());
$this->alert->success(trans('admin/eggs.variables.notices.variable_updated', [
'variable' => $variable->name,
]))->flash();
return redirect()->route('admin.eggs.variables', $egg->id);
}
/**
* Handle a request to delete an existing Egg variable from the Panel.
*/
public function destroy(int $egg, EggVariable $variable): RedirectResponse
{
$variable->delete();
$this->alert->success(trans('admin/eggs.variables.notices.variable_deleted', [
'variable' => $variable->name,
]))->flash();
return redirect()->route('admin.eggs.variables', $egg);
}
}

View File

@@ -1,32 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Models\Node;
use App\Http\Controllers\Controller;
use App\Services\Nodes\NodeAutoDeployService;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
class NodeAutoDeployController extends Controller
{
/**
* NodeAutoDeployController constructor.
*/
public function __construct(
private readonly NodeAutoDeployService $nodeAutoDeployService
) {
}
/**
* Handles the API request and returns the deployment command.
*
* @throws \App\Exceptions\Model\DataValidationException
*/
public function __invoke(Request $request, Node $node): JsonResponse
{
$command = $this->nodeAutoDeployService->handle($request, $node);
return new JsonResponse(['command' => $command]);
}
}

View File

@@ -1,26 +0,0 @@
<?php
namespace App\Http\Controllers\Admin\Nodes;
use Illuminate\View\View;
use App\Models\Node;
use Spatie\QueryBuilder\QueryBuilder;
use App\Http\Controllers\Controller;
class NodeController extends Controller
{
/**
* Returns a listing of nodes on the system.
*/
public function index(): View
{
$nodes = QueryBuilder::for(
Node::query()->withCount('servers')
)
->allowedFilters(['uuid', 'name'])
->allowedSorts(['id'])
->paginate(25);
return view('admin.nodes.index', ['nodes' => $nodes]);
}
}

View File

@@ -1,101 +0,0 @@
<?php
namespace App\Http\Controllers\Admin\Nodes;
use Illuminate\View\View;
use App\Models\Node;
use Illuminate\Support\Collection;
use App\Models\Allocation;
use App\Http\Controllers\Controller;
use App\Traits\Controllers\JavascriptInjection;
use App\Services\Helpers\SoftwareVersionService;
class NodeViewController extends Controller
{
use JavascriptInjection;
public const THRESHOLD_PERCENTAGE_LOW = 75;
public const THRESHOLD_PERCENTAGE_MEDIUM = 90;
/**
* NodeViewController constructor.
*/
public function __construct(
private SoftwareVersionService $versionService,
) {
}
/**
* Returns index view for a specific node on the system.
*/
public function index(Node $node): View
{
$node->loadCount('servers');
return view('admin.nodes.view.index', [
'node' => $node,
'version' => $this->versionService,
]);
}
/**
* Returns the settings page for a specific node.
*/
public function settings(Node $node): View
{
return view('admin.nodes.view.settings', [
'node' => $node,
]);
}
/**
* Return the node configuration page for a specific node.
*/
public function configuration(Node $node): View
{
return view('admin.nodes.view.configuration', compact('node'));
}
/**
* Return the node allocation management page.
*/
public function allocations(Node $node): View
{
$node->setRelation(
'allocations',
$node->allocations()
->orderByRaw('server_id IS NOT NULL DESC, server_id IS NULL')
->orderByRaw('INET_ATON(ip) ASC')
->orderBy('port')
->with('server:id,name')
->paginate(50)
);
$this->plainInject(['node' => Collection::wrap($node)->only(['id'])]);
return view('admin.nodes.view.allocation', [
'node' => $node,
'allocations' => Allocation::query()->where('node_id', $node->id)
->groupBy('ip')
->orderByRaw('INET_ATON(ip) ASC')
->get(['ip']),
]);
}
/**
* Return a listing of servers that exist for this specific node.
*/
public function servers(Node $node): View
{
$this->plainInject([
'node' => Collection::wrap($node->makeVisible(['daemon_token_id', 'daemon_token']))
->only(['scheme', 'fqdn', 'daemon_listen', 'daemon_token_id', 'daemon_token']),
]);
return view('admin.nodes.view.servers', [
'node' => $node,
'servers' => $node->servers()->with(['user', 'egg'])->paginate(25),
]);
}
}

View File

@@ -1,40 +0,0 @@
<?php
namespace App\Http\Controllers\Admin\Nodes;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use App\Models\Node;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
use App\Repositories\Daemon\DaemonConfigurationRepository;
class SystemInformationController extends Controller
{
/**
* SystemInformationController constructor.
*/
public function __construct(private DaemonConfigurationRepository $repository)
{
}
/**
* Returns system information from the Daemon.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
*/
public function __invoke(Request $request, Node $node): JsonResponse
{
$data = $this->repository->setNode($node)->getSystemInformation();
return new JsonResponse([
'version' => $data['version'] ?? '',
'system' => [
'type' => Str::title($data['os'] ?? 'Unknown'),
'arch' => $data['architecture'] ?? '--',
'release' => $data['kernel_version'] ?? '--',
'cpus' => $data['cpu_count'] ?? 0,
],
]);
}
}

View File

@@ -1,165 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use Illuminate\View\View;
use Illuminate\Http\Request;
use App\Models\Node;
use Illuminate\Http\Response;
use App\Models\Allocation;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\View\Factory as ViewFactory;
use App\Http\Controllers\Controller;
use App\Services\Nodes\NodeUpdateService;
use Illuminate\Cache\Repository as CacheRepository;
use App\Services\Nodes\NodeCreationService;
use App\Services\Nodes\NodeDeletionService;
use App\Services\Allocations\AssignmentService;
use App\Services\Helpers\SoftwareVersionService;
use App\Http\Requests\Admin\Node\NodeFormRequest;
use App\Http\Requests\Admin\Node\AllocationFormRequest;
use App\Http\Requests\Admin\Node\AllocationAliasFormRequest;
class NodesController extends Controller
{
/**
* NodesController constructor.
*/
public function __construct(
protected AlertsMessageBag $alert,
protected AssignmentService $assignmentService,
protected CacheRepository $cache,
protected NodeCreationService $creationService,
protected NodeDeletionService $deletionService,
protected NodeUpdateService $updateService,
protected SoftwareVersionService $versionService,
protected ViewFactory $view
) {
}
/**
* Displays create new node page.
*/
public function create(): View|RedirectResponse
{
return view('admin.nodes.new');
}
/**
* Post controller to create a new node on the system.
*
* @throws \App\Exceptions\Model\DataValidationException
*/
public function store(NodeFormRequest $request): RedirectResponse
{
$node = $this->creationService->handle($request->normalize());
$this->alert->info(trans('admin/node.notices.node_created'))->flash();
return redirect()->route('admin.nodes.view.allocation', $node->id);
}
/**
* Updates settings for a node.
*
* @throws \App\Exceptions\DisplayException
* @throws \App\Exceptions\Model\DataValidationException
*/
public function updateSettings(NodeFormRequest $request, Node $node): RedirectResponse
{
$this->updateService->handle($node, $request->normalize(), $request->input('reset_secret') === 'on');
$this->alert->success(trans('admin/node.notices.node_updated'))->flash();
return redirect()->route('admin.nodes.view.settings', $node->id)->withInput();
}
/**
* Removes a single allocation from a node.
*
* @throws \App\Exceptions\Service\Allocation\ServerUsingAllocationException
*/
public function allocationRemoveSingle(int $node, Allocation $allocation): Response
{
$allocation->delete();
return response('', 204);
}
/**
* Removes multiple individual allocations from a node.
*
* @throws \App\Exceptions\Service\Allocation\ServerUsingAllocationException
*/
public function allocationRemoveMultiple(Request $request, int $node): Response
{
$allocations = $request->input('allocations');
foreach ($allocations as $rawAllocation) {
$allocation = new Allocation();
$allocation->id = $rawAllocation['id'];
$this->allocationRemoveSingle($node, $allocation);
}
return response('', 204);
}
/**
* Remove all allocations for a specific IP at once on a node.
*/
public function allocationRemoveBlock(Request $request, int $node): RedirectResponse
{
/** @var Node $node */
$node = Node::query()->findOrFail($node);
$node->allocations()
->where('ip', $request->input('ip'))
->whereNull('server_id')
->delete();
$this->alert->success(trans('admin/node.notices.unallocated_deleted', ['ip' => $request->input('ip')]))
->flash();
return redirect()->route('admin.nodes.view.allocation', $node);
}
/**
* Sets an alias for a specific allocation on a node.
*
* @throws \App\Exceptions\Model\DataValidationException
*/
public function allocationSetAlias(AllocationAliasFormRequest $request): \Symfony\Component\HttpFoundation\Response
{
$allocation = Allocation::query()->findOrFail($request->input('allocation_id'));
$alias = (empty($request->input('alias'))) ? null : $request->input('alias');
$allocation->update(['ip_alias' => $alias]);
return response('', 204);
}
/**
* Creates new allocations on a node.
*
* @throws \App\Exceptions\Service\Allocation\CidrOutOfRangeException
* @throws \App\Exceptions\Service\Allocation\InvalidPortMappingException
* @throws \App\Exceptions\Service\Allocation\PortOutOfRangeException
* @throws \App\Exceptions\Service\Allocation\TooManyPortsInRangeException
*/
public function createAllocation(AllocationFormRequest $request, Node $node): RedirectResponse
{
$this->assignmentService->handle($node, $request->normalize());
$this->alert->success(trans('admin/node.notices.allocations_added'))->flash();
return redirect()->route('admin.nodes.view.allocation', $node->id);
}
/**
* Deletes a node from the system.
*
* @throws \App\Exceptions\DisplayException
*/
public function delete(int|Node $node): RedirectResponse
{
$this->deletionService->handle($node);
$this->alert->success(trans('admin/node.notices.node_deleted'))->flash();
return redirect()->route('admin.nodes');
}
}

View File

@@ -1,72 +0,0 @@
<?php
namespace App\Http\Controllers\Admin\Servers;
use App\Models\Egg;
use Illuminate\View\View;
use App\Models\Node;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\ServerFormRequest;
use App\Services\Servers\ServerCreationService;
class CreateServerController extends Controller
{
/**
* CreateServerController constructor.
*/
public function __construct(
private AlertsMessageBag $alert,
private ServerCreationService $creationService,
) {
}
/**
* Displays the create server page.
*/
public function index(): View|RedirectResponse
{
$nodes = Node::all();
if (count($nodes) < 1) {
$this->alert->warning(trans('admin/server.alerts.node_required'))->flash();
return redirect()->route('admin.nodes');
}
$eggs = Egg::with('variables')->get();
\JavaScript::put([
'nodeData' => Node::getForServerCreation(),
'eggs' => $eggs->keyBy('id'),
]);
return view('admin.servers.new', [
'eggs' => $eggs,
'nodes' => Node::all(),
]);
}
/**
* Create a new server on the remote system.
*
* @throws \Illuminate\Validation\ValidationException
* @throws \App\Exceptions\DisplayException
* @throws \App\Exceptions\Service\Deployment\NoViableAllocationException
* @throws \Throwable
*/
public function store(ServerFormRequest $request): RedirectResponse
{
$data = $request->except(['_token']);
if (!empty($data['custom_image'])) {
$data['image'] = $data['custom_image'];
unset($data['custom_image']);
}
$server = $this->creationService->handle($data);
$this->alert->success(trans('admin/server.alerts.server_created'))->flash();
return new RedirectResponse('/admin/servers/view/' . $server->id);
}
}

View File

@@ -1,29 +0,0 @@
<?php
namespace App\Http\Controllers\Admin\Servers;
use Illuminate\View\View;
use App\Models\Server;
use Spatie\QueryBuilder\QueryBuilder;
use Spatie\QueryBuilder\AllowedFilter;
use App\Http\Controllers\Controller;
use App\Models\Filters\AdminServerFilter;
class ServerController extends Controller
{
/**
* Returns all the servers that exist on the system using a paginated result set. If
* a query is passed along in the request it is also passed to the repository function.
*/
public function index(): View
{
$servers = QueryBuilder::for(Server::query()->with('node', 'user', 'allocation'))
->allowedFilters([
AllowedFilter::exact('owner_id'),
AllowedFilter::custom('*', new AdminServerFilter()),
])
->paginate(config()->get('panel.paginate.admin.servers'));
return view('admin.servers.index', ['servers' => $servers]);
}
}

View File

@@ -1,44 +0,0 @@
<?php
namespace App\Http\Controllers\Admin\Servers;
use App\Http\Controllers\Controller;
use App\Models\Server;
use App\Services\Servers\TransferServerService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Prologue\Alerts\AlertsMessageBag;
class ServerTransferController extends Controller
{
/**
* ServerTransferController constructor.
*/
public function __construct(
private AlertsMessageBag $alert,
private TransferServerService $transferServerService,
) {
}
/**
* Starts a transfer of a server to a new node.
*
* @throws \Throwable
*/
public function transfer(Request $request, Server $server): RedirectResponse
{
$validatedData = $request->validate([
'node_id' => 'required|exists:nodes,id',
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
'allocation_additional' => 'nullable',
]);
if ($this->transferServerService->handle($server, $validatedData)) {
$this->alert->success(trans('admin/server.alerts.transfer_started'))->flash();
} else {
$this->alert->danger(trans('admin/server.alerts.transfer_not_viable'))->flash();
}
return redirect()->route('admin.servers.view.manage', $server->id);
}
}

View File

@@ -1,142 +0,0 @@
<?php
namespace App\Http\Controllers\Admin\Servers;
use App\Enums\ServerState;
use App\Models\DatabaseHost;
use App\Models\Egg;
use App\Models\Mount;
use App\Models\Node;
use Illuminate\View\View;
use App\Models\Server;
use App\Exceptions\DisplayException;
use App\Http\Controllers\Controller;
use App\Services\Servers\EnvironmentService;
use App\Traits\Controllers\JavascriptInjection;
class ServerViewController extends Controller
{
use JavascriptInjection;
/**
* ServerViewController constructor.
*/
public function __construct(
private readonly EnvironmentService $environmentService,
) {
}
/**
* Returns the index view for a server.
*/
public function index(Server $server): View
{
return view('admin.servers.view.index', compact('server'));
}
/**
* Returns the server details page.
*/
public function details(Server $server): View
{
return view('admin.servers.view.details', compact('server'));
}
/**
* Returns a view of server build settings.
*/
public function build(Server $server): View
{
$allocations = $server->node->allocations->toBase();
return view('admin.servers.view.build', [
'server' => $server,
'assigned' => $allocations->where('server_id', $server->id)->sortBy('port')->sortBy('ip'),
'unassigned' => $allocations->where('server_id', null)->sortBy('port')->sortBy('ip'),
]);
}
/**
* Returns the server startup management page.
*/
public function startup(Server $server): View
{
$variables = $this->environmentService->handle($server);
$eggs = Egg::all()->keyBy('id');
$this->plainInject([
'server' => $server,
'server_variables' => $variables,
'eggs' => $eggs,
]);
return view('admin.servers.view.startup', compact('server', 'eggs'));
}
/**
* Returns all the databases that exist for the server.
*/
public function database(Server $server): View
{
return view('admin.servers.view.database', [
'hosts' => DatabaseHost::all(),
'server' => $server,
]);
}
/**
* Returns all the mounts that exist for the server.
*/
public function mounts(Server $server): View
{
$server->load('mounts');
$mounts = Mount::query()
->whereHas('eggs', fn ($q) => $q->where('id', $server->egg_id))
->whereHas('nodes', fn ($q) => $q->where('id', $server->node_id))
->get();
return view('admin.servers.view.mounts', [
'mounts' => $mounts,
'server' => $server,
]);
}
/**
* Returns the base server management page, or an exception if the server
* is in a state that cannot be recovered from.
*
* @throws \App\Exceptions\DisplayException
*/
public function manage(Server $server): View
{
if ($server->status === ServerState::InstallFailed) {
throw new DisplayException('This server is in a failed install state and cannot be recovered. Please delete and re-create the server.');
}
// Check if the panel doesn't have at least 2 nodes configured.
$nodeCount = Node::query()->count();
$canTransfer = false;
if ($nodeCount >= 2) {
$canTransfer = true;
}
\JavaScript::put([
'nodeData' => Node::getForServerCreation(),
]);
return view('admin.servers.view.manage', [
'nodes' => Node::all(),
'server' => $server,
'canTransfer' => $canTransfer,
]);
}
/**
* Returns the server deletion page.
*/
public function delete(Server $server): View
{
return view('admin.servers.view.delete', compact('server'));
}
}

View File

@@ -1,254 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Enums\ServerState;
use Filament\Notifications\Notification;
use Illuminate\Http\Request;
use App\Models\User;
use Illuminate\Http\Response;
use App\Models\Mount;
use App\Models\Server;
use App\Models\Database;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use App\Exceptions\DisplayException;
use App\Http\Controllers\Controller;
use Illuminate\Validation\ValidationException;
use App\Services\Servers\SuspensionService;
use App\Services\Servers\ServerDeletionService;
use App\Services\Servers\ReinstallServerService;
use App\Exceptions\Model\DataValidationException;
use App\Repositories\Daemon\DaemonServerRepository;
use App\Services\Servers\BuildModificationService;
use App\Services\Databases\DatabasePasswordService;
use App\Services\Servers\DetailsModificationService;
use App\Services\Servers\StartupModificationService;
use App\Services\Databases\DatabaseManagementService;
use App\Services\Servers\ServerConfigurationStructureService;
use App\Http\Requests\Admin\Servers\Databases\StoreServerDatabaseRequest;
class ServersController extends Controller
{
/**
* ServersController constructor.
*/
public function __construct(
protected AlertsMessageBag $alert,
protected BuildModificationService $buildModificationService,
protected DaemonServerRepository $daemonServerRepository,
protected DatabaseManagementService $databaseManagementService,
protected DatabasePasswordService $databasePasswordService,
protected ServerDeletionService $deletionService,
protected DetailsModificationService $detailsModificationService,
protected ReinstallServerService $reinstallService,
protected ServerConfigurationStructureService $serverConfigurationStructureService,
protected StartupModificationService $startupModificationService,
protected SuspensionService $suspensionService
) {
}
/**
* Update the details for a server.
*
* @throws \App\Exceptions\Model\DataValidationException
*/
public function setDetails(Request $request, Server $server): RedirectResponse
{
$this->detailsModificationService->handle($server, $request->only([
'owner_id', 'external_id', 'name', 'description',
]));
$this->alert->success(trans('admin/server.alerts.details_updated'))->flash();
return redirect()->route('admin.servers.view.details', $server->id);
}
/**
* Toggles the installation status for a server.
*
* @throws \App\Exceptions\DisplayException
* @throws \App\Exceptions\Model\DataValidationException
*/
public function toggleInstall(Server $server): void
{
if ($server->status === ServerState::InstallFailed) {
throw new DisplayException(trans('admin/server.exceptions.marked_as_failed'));
}
$server->status = $server->isInstalled() ? ServerState::Installing : null;
$server->save();
Notification::make()
->title('Success!')
->body(trans('admin/server.alerts.install_toggled'))
->success()
->send();
}
/**
* Reinstalls the server with the currently assigned service.
*
* @throws \App\Exceptions\DisplayException
* @throws \App\Exceptions\Model\DataValidationException
*/
public function reinstallServer(Server $server): void
{
$this->reinstallService->handle($server);
Notification::make()
->title('Success!')
->body(trans('admin/server.alerts.server_reinstalled'))
->success()
->send();
}
/**
* Manage the suspension status for a server.
*
* @throws \App\Exceptions\DisplayException
* @throws \App\Exceptions\Model\DataValidationException
*/
public function manageSuspension(Request $request, Server $server): RedirectResponse
{
$this->suspensionService->toggle($server, $request->input('action'));
$this->alert->success(trans('admin/server.alerts.suspension_toggled', [
'status' => $request->input('action') . 'ed',
]))->flash();
return redirect()->route('admin.servers.view.manage', $server->id);
}
/**
* Update the build configuration for a server.
*
* @throws \App\Exceptions\DisplayException
* @throws \Illuminate\Validation\ValidationException
*/
public function updateBuild(Request $request, Server $server): RedirectResponse
{
try {
$this->buildModificationService->handle($server, $request->only([
'allocation_id', 'add_allocations', 'remove_allocations',
'memory', 'swap', 'io', 'cpu', 'threads', 'disk',
'database_limit', 'allocation_limit', 'backup_limit', 'oom_killer',
]));
} catch (DataValidationException $exception) {
throw new ValidationException($exception->getValidator());
}
$this->alert->success(trans('admin/server.alerts.build_updated'))->flash();
return redirect()->route('admin.servers.view.build', $server->id);
}
/**
* Start the server deletion process.
*
* @throws \App\Exceptions\DisplayException
* @throws \Throwable
*/
public function delete(Request $request, Server $server): RedirectResponse
{
$this->deletionService->withForce($request->filled('force_delete'))->handle($server);
$this->alert->success(trans('admin/server.alerts.server_deleted'))->flash();
return redirect()->route('admin.servers');
}
/**
* Update the startup command as well as variables.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function saveStartup(Request $request, Server $server): RedirectResponse
{
$data = $request->except('_token');
if (!empty($data['custom_docker_image'])) {
$data['docker_image'] = $data['custom_docker_image'];
unset($data['custom_docker_image']);
}
try {
$this->startupModificationService
->setUserLevel(User::USER_LEVEL_ADMIN)
->handle($server, $data);
} catch (DataValidationException $exception) {
throw new ValidationException($exception->getValidator());
}
$this->alert->success(trans('admin/server.alerts.startup_changed'))->flash();
return redirect()->route('admin.servers.view.startup', $server->id);
}
/**
* Creates a new database assigned to a specific server.
*
* @throws \Throwable
*/
public function newDatabase(StoreServerDatabaseRequest $request, Server $server): RedirectResponse
{
$this->databaseManagementService->create($server, [
'database' => DatabaseManagementService::generateUniqueDatabaseName($request->input('database'), $server->id),
'remote' => $request->input('remote'),
'database_host_id' => $request->input('database_host_id'),
'max_connections' => $request->input('max_connections'),
]);
return redirect()->route('admin.servers.view.database', $server->id)->withInput();
}
/**
* Resets the database password for a specific database on this server.
*
* @throws \Throwable
*/
public function resetDatabasePassword(Request $request, Server $server): Response
{
/** @var \App\Models\Database $database */
$database = $server->databases()->findOrFail($request->input('database'));
$this->databasePasswordService->handle($database);
return response('', 204);
}
/**
* Deletes a database from a server.
*
* @throws \Exception
*/
public function deleteDatabase(Server $server, Database $database): Response
{
$this->databaseManagementService->delete($database);
return response('', 204);
}
/**
* Add a mount to a server.
*
* @throws \Throwable
*/
public function addMount(Request $request, Server $server): RedirectResponse
{
$server->mounts()->attach($request->input('mount_id'));
$this->alert->success('Mount was added successfully.')->flash();
return redirect()->route('admin.servers.view.mounts', $server->id);
}
/**
* Remove a mount from a server.
*/
public function deleteMount(Server $server, Mount $mount): RedirectResponse
{
$server->mounts()->detach($mount);
$this->alert->success('Mount was removed successfully.')->flash();
return redirect()->route('admin.servers.view.mounts', $server->id);
}
}

View File

@@ -1,146 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use Illuminate\Http\JsonResponse;
use Illuminate\View\View;
use Illuminate\Http\Request;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Spatie\QueryBuilder\QueryBuilder;
use Illuminate\View\Factory as ViewFactory;
use App\Http\Controllers\Controller;
use Illuminate\Contracts\Translation\Translator;
use App\Services\Users\UserUpdateService;
use App\Traits\Helpers\AvailableLanguages;
use App\Services\Users\UserCreationService;
use App\Http\Requests\Admin\UserFormRequest;
use App\Http\Requests\Admin\NewUserFormRequest;
class UserController extends Controller
{
use AvailableLanguages;
/**
* UserController constructor.
*/
public function __construct(
protected AlertsMessageBag $alert,
protected UserCreationService $creationService,
protected Translator $translator,
protected UserUpdateService $updateService,
protected ViewFactory $view
) {
}
/**
* Display user index page.
*/
public function index(): View
{
$users = QueryBuilder::for(
User::query()->select('users.*')
->selectRaw('COUNT(DISTINCT(subusers.id)) as subuser_of_count')
->selectRaw('COUNT(DISTINCT(servers.id)) as servers_count')
->leftJoin('subusers', 'subusers.user_id', '=', 'users.id')
->leftJoin('servers', 'servers.owner_id', '=', 'users.id')
->groupBy('users.id')
)
->allowedFilters(['username', 'email', 'uuid'])
->allowedSorts(['id', 'uuid'])
->paginate(50);
return view('admin.users.index', ['users' => $users]);
}
/**
* Display new user page.
*/
public function create(): View
{
return view('admin.users.new', [
'languages' => $this->getAvailableLanguages(),
]);
}
/**
* Display user view page.
*/
public function view(User $user): View
{
return view('admin.users.view', [
'user' => $user,
'languages' => $this->getAvailableLanguages(),
]);
}
/**
* Delete a user from the system.
*
* @throws \Exception
* @throws \App\Exceptions\DisplayException
*/
public function delete(User $user): RedirectResponse
{
$user->delete();
return redirect()->route('admin.users');
}
/**
* Create a user.
*
* @throws \Exception
* @throws \Throwable
*/
public function store(NewUserFormRequest $request): RedirectResponse
{
$user = $this->creationService->handle($request->normalize());
$this->alert->success($this->translator->get('admin/user.notices.account_created'))->flash();
return redirect()->route('admin.users.view', $user->id);
}
/**
* Update a user on the system.
*
* @throws \App\Exceptions\Model\DataValidationException
*/
public function update(UserFormRequest $request, User $user): RedirectResponse
{
$this->updateService
->setUserLevel(User::USER_LEVEL_ADMIN)
->handle($user, $request->normalize());
$this->alert->success(trans('admin/user.notices.account_updated'))->flash();
return redirect()->route('admin.users.view', $user->id);
}
/**
* Get a JSON response of users on the system.
*/
public function json(Request $request): JsonResponse
{
// Handle single user requests | TODO: Separate this out into its own method
if ($userId = $request->query('user_id')) {
$user = User::query()->findOrFail($userId);
$user['md5'] = md5(strtolower($user->email));
return response()->json($user);
}
// Handle all users list
$userPaginator = QueryBuilder::for(User::query())->allowedFilters(['email'])->paginate(25);
/** @var User[] $users */
$users = $userPaginator->items();
return response()->json(collect($users)->map(function (User $user) {
$user['md5'] = md5(strtolower($user->email));
return $user;
}));
}
}

View File

@@ -1,48 +0,0 @@
<?php
namespace App\Http\Controllers\Api\Remote;
use App\Models\Server;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
use App\Services\Servers\EnvironmentService;
class EggInstallController extends Controller
{
/**
* EggInstallController constructor.
*/
public function __construct(private EnvironmentService $environment)
{
}
/**
* Handle request to get script and installation information for a server
* that is being created on the node.
*/
public function index(Request $request, string $uuid): JsonResponse
{
$node = $request->attributes->get('node');
$server = Server::query()
->with('egg.scriptFrom')
->where('uuid', $uuid)
->where('node_id', $node->id)
->firstOrFail();
$egg = $server->egg;
return response()->json([
'scripts' => [
'install' => !$egg->copy_script_install ? null : str_replace(["\r\n", "\n", "\r"], "\n", $egg->copy_script_install),
'privileged' => $egg->script_is_privileged,
],
'config' => [
'container' => $egg->copy_script_container,
'entry' => $egg->copy_script_entry,
],
'env' => $this->environment->handle($server),
]);
}
}

View File

@@ -1,31 +0,0 @@
<?php
namespace App\Http\Middleware\Admin\Servers;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Models\Server;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class ServerInstalled
{
/**
* Checks that the server is installed before allowing access through the route.
*/
public function handle(Request $request, \Closure $next): mixed
{
/** @var \App\Models\Server|null $server */
$server = $request->route()->parameter('server');
if (!$server instanceof Server) {
throw new NotFoundHttpException('No server resource was located in the request parameters.');
}
if (!$server->isInstalled()) {
throw new HttpException(Response::HTTP_FORBIDDEN, 'Access to this resource is not allowed due to the current installation state.');
}
return $next($request);
}
}

View File

@@ -1,23 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class AdminAuthenticate
{
/**
* Handle an incoming request.
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/
public function handle(Request $request, \Closure $next): mixed
{
if (!$request->user() || !$request->user()->isRootAdmin()) {
throw new AccessDeniedHttpException();
}
return $next($request);
}
}

View File

@@ -4,7 +4,6 @@ namespace App\Http\Middleware;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Prologue\Alerts\AlertsMessageBag;
use App\Exceptions\Http\TwoFactorAuthRequiredException;
class RequireTwoFactorAuthentication
@@ -20,13 +19,6 @@ class RequireTwoFactorAuthentication
*/
protected string $redirectRoute = '/account';
/**
* RequireTwoFactorAuthentication constructor.
*/
public function __construct(private AlertsMessageBag $alert)
{
}
/**
* Check the user state on the incoming request to determine if they should be allowed to
* proceed or not. This checks if the Panel is configured to require 2FA on an account in
@@ -62,8 +54,6 @@ class RequireTwoFactorAuthentication
throw new TwoFactorAuthRequiredException();
}
$this->alert->danger(trans('auth.2fa_must_be_enabled'))->flash();
return redirect()->to($this->redirectRoute);
}
}

View File

@@ -2,11 +2,11 @@
namespace App\Http\Middleware;
use GuzzleHttp\Client;
use Illuminate\Foundation\Application;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Events\Auth\FailedCaptcha;
use Coderflex\LaravelTurnstile\Facades\LaravelTurnstile;
use Symfony\Component\HttpKernel\Exception\HttpException;
readonly class VerifyReCaptcha
@@ -18,7 +18,7 @@ readonly class VerifyReCaptcha
public function handle(Request $request, \Closure $next): mixed
{
if (!config('recaptcha.enabled')) {
if (!config('turnstile.turnstile_enabled')) {
return $next($request);
}
@@ -26,40 +26,30 @@ readonly class VerifyReCaptcha
return $next($request);
}
if ($request->filled('g-recaptcha-response')) {
$client = new Client();
$res = $client->post(config('recaptcha.domain'), [
'form_params' => [
'secret' => config('recaptcha.secret_key'),
'response' => $request->input('g-recaptcha-response'),
],
]);
if ($request->filled('cf-turnstile-response')) {
$response = LaravelTurnstile::validate($request->get('cf-turnstile-response'));
if ($res->getStatusCode() === 200) {
$result = json_decode($res->getBody());
if ($result->success && (!config('recaptcha.verify_domain') || $this->isResponseVerified($result, $request))) {
return $next($request);
}
if ($response['success'] && $this->isResponseVerified($response['hostname'] ?? '', $request)) {
return $next($request);
}
}
event(new FailedCaptcha($request->ip(), $result->hostname ?? null));
event(new FailedCaptcha($request->ip(), $response['message'] ?? null));
throw new HttpException(Response::HTTP_BAD_REQUEST, 'Failed to validate reCAPTCHA data.');
throw new HttpException(Response::HTTP_BAD_REQUEST, 'Failed to validate turnstile captcha data.');
}
/**
* Determine if the response from the recaptcha servers was valid.
*/
private function isResponseVerified(\stdClass $result, Request $request): bool
private function isResponseVerified(string $hostname, Request $request): bool
{
if (!config('recaptcha.verify_domain')) {
return false;
if (!config('turnstile.turnstile_verify_domain')) {
return true;
}
$url = parse_url($request->url());
return $result->hostname === array_get($url, 'host');
return $hostname === array_get($url, 'host');
}
}

View File

@@ -1,35 +0,0 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
abstract class AdminFormRequest extends FormRequest
{
/**
* The rules to apply to the incoming form request.
*/
abstract public function rules(): array;
/**
* Determine if the user is an admin and has permission to access this
* form controller in the first place.
*/
public function authorize(): bool
{
if (is_null($this->user())) {
return false;
}
return $this->user()->isRootAdmin();
}
/**
* Return only the fields that we are interested in from the request.
* This will include empty fields as a null value.
*/
public function normalize(?array $only = null): array
{
return $this->only($only ?? array_keys($this->rules()));
}
}

View File

@@ -1,37 +0,0 @@
<?php
namespace App\Http\Requests\Admin\Api;
use App\Models\ApiKey;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Admin\AdminFormRequest;
class StoreApplicationApiKeyRequest extends AdminFormRequest
{
/**
* @throws \ReflectionException
* @throws \ReflectionException
*/
public function rules(): array
{
$modelRules = ApiKey::getRules();
return collect(AdminAcl::getResourceList())->mapWithKeys(function ($resource) use ($modelRules) {
return [AdminAcl::COLUMN_IDENTIFIER . $resource => $modelRules['r_' . $resource]];
})->merge(['memo' => $modelRules['memo']])->toArray();
}
public function attributes(): array
{
return [
'memo' => 'Description',
];
}
public function getKeyPermissions(): array
{
return collect($this->validated())->filter(function ($value, $key) {
return substr($key, 0, strlen(AdminAcl::COLUMN_IDENTIFIER)) === AdminAcl::COLUMN_IDENTIFIER;
})->toArray();
}
}

View File

@@ -1,13 +0,0 @@
<?php
namespace App\Http\Requests\Admin;
class BaseFormRequest extends AdminFormRequest
{
public function rules(): array
{
return [
'company' => 'required|between:1,256',
];
}
}

View File

@@ -1,30 +0,0 @@
<?php
namespace App\Http\Requests\Admin;
use App\Models\DatabaseHost;
use Illuminate\Contracts\Validation\Validator;
class DatabaseHostFormRequest extends AdminFormRequest
{
public function rules(): array
{
if ($this->method() !== 'POST') {
return DatabaseHost::getRulesForUpdate($this->route()->parameter('host'));
}
return DatabaseHost::getRules();
}
/**
* Modify submitted data before it is passed off to the validator.
*/
protected function getValidatorInstance(): Validator
{
if (!$this->filled('node_id')) {
$this->merge(['node_id' => null]);
}
return parent::getValidatorInstance();
}
}

View File

@@ -1,44 +0,0 @@
<?php
namespace App\Http\Requests\Admin\Egg;
use App\Http\Requests\Admin\AdminFormRequest;
use Illuminate\Validation\Validator;
class EggFormRequest extends AdminFormRequest
{
public function rules(): array
{
$rules = [
'name' => 'required|string|max:255',
'description' => 'nullable|string',
'docker_images' => 'required|string',
'force_outgoing_ip' => 'sometimes|boolean',
'file_denylist' => 'array',
'startup' => 'required|string',
'config_from' => 'sometimes|bail|nullable|numeric',
'config_stop' => 'required_without:config_from|nullable|string|max:255',
'config_startup' => 'required_without:config_from|nullable|json',
'config_logs' => 'required_without:config_from|nullable|json',
'config_files' => 'required_without:config_from|nullable|json',
];
return $rules;
}
public function withValidator(Validator $validator): void
{
$validator->sometimes('config_from', 'exists:eggs,id', function () {
return (int) $this->input('config_from') !== 0;
});
}
public function validated($key = null, $default = null): array
{
$data = parent::validated();
return array_merge($data, [
'force_outgoing_ip' => array_get($data, 'force_outgoing_ip', false),
]);
}
}

View File

@@ -1,15 +0,0 @@
<?php
namespace App\Http\Requests\Admin\Egg;
use App\Http\Requests\Admin\AdminFormRequest;
class EggImportFormRequest extends AdminFormRequest
{
public function rules(): array
{
return [
'import_file' => 'bail|required|file|max:1000|mimetypes:application/json,text/plain',
];
}
}

View File

@@ -1,22 +0,0 @@
<?php
namespace App\Http\Requests\Admin\Egg;
use App\Http\Requests\Admin\AdminFormRequest;
class EggScriptFormRequest extends AdminFormRequest
{
/**
* Return the rules to be used when validating the data sent in the request.
*/
public function rules(): array
{
return [
'script_install' => 'sometimes|nullable|string',
'script_is_privileged' => 'sometimes|required|boolean',
'script_entry' => 'sometimes|required|string',
'script_container' => 'sometimes|required|string',
'copy_script_from' => 'sometimes|nullable|numeric',
];
}
}

View File

@@ -1,24 +0,0 @@
<?php
namespace App\Http\Requests\Admin\Egg;
use App\Models\EggVariable;
use App\Http\Requests\Admin\AdminFormRequest;
class EggVariableFormRequest extends AdminFormRequest
{
/**
* Define rules for validation of this request.
*/
public function rules(): array
{
return [
'name' => 'required|string|min:1|max:255',
'description' => 'sometimes|nullable|string',
'env_variable' => 'required|regex:/^[\w]{1,255}$/|notIn:' . EggVariable::RESERVED_ENV_NAMES,
'options' => 'sometimes|required|array',
'rules' => 'bail|required|string',
'default_value' => 'present',
];
}
}

View File

@@ -1,27 +0,0 @@
<?php
namespace App\Http\Requests\Admin;
use App\Models\User;
use Illuminate\Support\Collection;
class NewUserFormRequest extends AdminFormRequest
{
/**
* Rules to apply to requests for updating or creating a user
* in the Admin CP.
*/
public function rules(): array
{
return Collection::make(
User::getRules()
)->only([
'email',
'username',
'name_first',
'name_last',
'password',
'language',
])->toArray();
}
}

View File

@@ -1,16 +0,0 @@
<?php
namespace App\Http\Requests\Admin\Node;
use App\Http\Requests\Admin\AdminFormRequest;
class AllocationAliasFormRequest extends AdminFormRequest
{
public function rules(): array
{
return [
'alias' => 'present|nullable|string',
'allocation_id' => 'required|numeric|exists:allocations,id',
];
}
}

View File

@@ -1,17 +0,0 @@
<?php
namespace App\Http\Requests\Admin\Node;
use App\Http\Requests\Admin\AdminFormRequest;
class AllocationFormRequest extends AdminFormRequest
{
public function rules(): array
{
return [
'allocation_ip' => 'required|string',
'allocation_alias' => 'sometimes|nullable|string|max:255',
'allocation_ports' => 'required|array',
];
}
}

View File

@@ -1,21 +0,0 @@
<?php
namespace App\Http\Requests\Admin\Node;
use App\Models\Node;
use App\Http\Requests\Admin\AdminFormRequest;
class NodeFormRequest extends AdminFormRequest
{
/**
* Get rules to apply to data in this request.
*/
public function rules(): array
{
if ($this->method() === 'PATCH') {
return Node::getRulesForUpdate($this->route()->parameter('node'));
}
return Node::getRules();
}
}

View File

@@ -1,58 +0,0 @@
<?php
namespace App\Http\Requests\Admin;
use App\Models\Server;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Validator;
class ServerFormRequest extends AdminFormRequest
{
/**
* Rules to be applied to this request.
*/
public function rules(): array
{
$rules = Server::getRules();
$rules['description'][] = 'nullable';
$rules['custom_image'] = 'sometimes|nullable|string';
return $rules;
}
/**
* Run validation after the rules above have been applied.
*/
public function withValidator(Validator $validator): void
{
$validator->after(function ($validator) {
$validator->sometimes('node_id', 'required|numeric|bail|exists:nodes,id', function ($input) {
return !$input->auto_deploy;
});
$validator->sometimes('allocation_id', [
'required',
'numeric',
'bail',
Rule::exists('allocations', 'id')->where(function ($query) {
$query->where('node_id', $this->input('node_id'));
$query->whereNull('server_id');
}),
], function ($input) {
return !$input->auto_deploy;
});
$validator->sometimes('allocation_additional.*', [
'sometimes',
'required',
'numeric',
Rule::exists('allocations', 'id')->where(function ($query) {
$query->where('node_id', $this->input('node_id'));
$query->whereNull('server_id');
}),
], function ($input) {
return !$input->auto_deploy;
});
});
}
}

View File

@@ -1,31 +0,0 @@
<?php
namespace App\Http\Requests\Admin\Servers\Databases;
use Illuminate\Validation\Rule;
use Illuminate\Database\Query\Builder;
use App\Http\Requests\Admin\AdminFormRequest;
class StoreServerDatabaseRequest extends AdminFormRequest
{
/**
* Validation rules for database creation.
*/
public function rules(): array
{
return [
'database' => [
'required',
'string',
'min:1',
'max:24',
Rule::unique('databases')->where(function (Builder $query) {
$query->where('database_host_id', $this->input('database_host_id') ?? 0);
}),
],
'max_connections' => 'nullable',
'remote' => 'required|string|regex:/^[0-9%.]{1,15}$/',
'database_host_id' => 'required|integer|exists:database_hosts,id',
];
}
}

View File

@@ -1,50 +0,0 @@
<?php
namespace App\Http\Requests\Admin\Settings;
use App\Http\Requests\Admin\AdminFormRequest;
class AdvancedSettingsFormRequest extends AdminFormRequest
{
/**
* Return all the rules to apply to this request's data.
*/
public function rules(): array
{
return [
'recaptcha:enabled' => 'required|in:true,false',
'recaptcha:secret_key' => 'required|string|max:255',
'recaptcha:website_key' => 'required|string|max:255',
'panel:guzzle:timeout' => 'required|integer|between:1,60',
'panel:guzzle:connect_timeout' => 'required|integer|between:1,60',
'panel:client_features:allocations:enabled' => 'required|in:true,false',
'panel:client_features:allocations:range_start' => [
'nullable',
'required_if:panel:client_features:allocations:enabled,true',
'integer',
'between:1024,65535',
],
'panel:client_features:allocations:range_end' => [
'nullable',
'required_if:panel:client_features:allocations:enabled,true',
'integer',
'between:1024,65535',
'gt:panel:client_features:allocations:range_start',
],
];
}
public function attributes(): array
{
return [
'recaptcha:enabled' => 'reCAPTCHA Enabled',
'recaptcha:secret_key' => 'reCAPTCHA Secret Key',
'recaptcha:website_key' => 'reCAPTCHA Website Key',
'panel:guzzle:timeout' => 'HTTP Request Timeout',
'panel:guzzle:connect_timeout' => 'HTTP Connection Timeout',
'panel:client_features:allocations:enabled' => 'Auto Create Allocations Enabled',
'panel:client_features:allocations:range_start' => 'Starting Port',
'panel:client_features:allocations:range_end' => 'Ending Port',
];
}
}

View File

@@ -1,30 +0,0 @@
<?php
namespace App\Http\Requests\Admin\Settings;
use Illuminate\Validation\Rule;
use App\Traits\Helpers\AvailableLanguages;
use App\Http\Requests\Admin\AdminFormRequest;
class BaseSettingsFormRequest extends AdminFormRequest
{
use AvailableLanguages;
public function rules(): array
{
return [
'app:name' => 'required|string|max:255',
'panel:auth:2fa_required' => 'required|integer|in:0,1,2',
'app:locale' => ['required', 'string', Rule::in(array_keys($this->getAvailableLanguages()))],
];
}
public function attributes(): array
{
return [
'app:name' => 'Company Name',
'panel:auth:2fa_required' => 'Require 2-Factor Authentication',
'app:locale' => 'Default Language',
];
}
}

View File

@@ -1,40 +0,0 @@
<?php
namespace App\Http\Requests\Admin\Settings;
use Illuminate\Validation\Rule;
use App\Http\Requests\Admin\AdminFormRequest;
class MailSettingsFormRequest extends AdminFormRequest
{
/**
* Return rules to validate mail settings POST data against.
*/
public function rules(): array
{
return [
'mail:mailers:smtp:host' => 'required|string',
'mail:mailers:smtp:port' => 'required|integer|between:1,65535',
'mail:mailers:smtp:encryption' => ['present', Rule::in([null, 'tls', 'ssl'])],
'mail:mailers:smtp:username' => 'nullable|string|max:255',
'mail:mailers:smtp:password' => 'nullable|string|max:255',
'mail:from:address' => 'required|string|email',
'mail:from:name' => 'nullable|string|max:255',
];
}
/**
* Override the default normalization function for this type of request
* as we need to accept empty values on the keys.
*/
public function normalize(?array $only = null): array
{
$keys = array_flip(array_keys($this->rules()));
if (empty($this->input('mail:mailers:smtp:password'))) {
unset($keys['mail:mailers:smtp:password']);
}
return $this->only(array_flip($keys));
}
}

View File

@@ -1,27 +0,0 @@
<?php
namespace App\Http\Requests\Admin;
use App\Models\User;
use Illuminate\Support\Collection;
class UserFormRequest extends AdminFormRequest
{
/**
* Rules to apply to requests for updating or creating a user
* in the Admin CP.
*/
public function rules(): array
{
return Collection::make(
User::getRulesForUpdate($this->route()->parameter('user'))
)->only([
'email',
'username',
'name_first',
'name_last',
'password',
'language',
])->toArray();
}
}

View File

@@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\Allocations;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Allocation;
class DeleteAllocationRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_ALLOCATIONS;
protected ?string $resource = Allocation::RESOURCE_NAME;
protected int $permission = AdminAcl::WRITE;
}

View File

@@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\Allocations;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Allocation;
class GetAllocationsRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_ALLOCATIONS;
protected ?string $resource = Allocation::RESOURCE_NAME;
protected int $permission = AdminAcl::READ;
}

View File

@@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\Allocations;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Allocation;
class StoreAllocationRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_ALLOCATIONS;
protected ?string $resource = Allocation::RESOURCE_NAME;
protected int $permission = AdminAcl::WRITE;

View File

@@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\DatabaseHosts;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\DatabaseHost;
class DeleteDatabaseHostRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_DATABASE_HOSTS;
protected ?string $resource = DatabaseHost::RESOURCE_NAME;
protected int $permission = AdminAcl::WRITE;
}

View File

@@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\DatabaseHosts;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\DatabaseHost;
class GetDatabaseHostRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_DATABASE_HOSTS;
protected ?string $resource = DatabaseHost::RESOURCE_NAME;
protected int $permission = AdminAcl::READ;
}

View File

@@ -8,7 +8,7 @@ use App\Http\Requests\Api\Application\ApplicationApiRequest;
class StoreDatabaseHostRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_DATABASE_HOSTS;
protected ?string $resource = DatabaseHost::RESOURCE_NAME;
protected int $permission = AdminAcl::WRITE;

View File

@@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\Eggs;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Services\Acl\Api\AdminAcl;
use App\Models\Egg;
class GetEggRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_EGGS;
protected ?string $resource = Egg::RESOURCE_NAME;
protected int $permission = AdminAcl::READ;
}

View File

@@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\Eggs;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Services\Acl\Api\AdminAcl;
use App\Models\Egg;
class GetEggsRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_EGGS;
protected ?string $resource = Egg::RESOURCE_NAME;
protected int $permission = AdminAcl::READ;
}

View File

@@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\Mounts;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Mount;
class DeleteMountRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_MOUNTS;
protected ?string $resource = Mount::RESOURCE_NAME;
protected int $permission = AdminAcl::WRITE;
}

View File

@@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\Mounts;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Mount;
class GetMountRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_MOUNTS;
protected ?string $resource = Mount::RESOURCE_NAME;
protected int $permission = AdminAcl::READ;
}

View File

@@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\Mounts;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Mount;
class StoreMountRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_MOUNTS;
protected ?string $resource = Mount::RESOURCE_NAME;
protected int $permission = AdminAcl::WRITE;
}

View File

@@ -4,10 +4,11 @@ namespace App\Http\Requests\Api\Application\Nodes;
use App\Services\Acl\Api\AdminAcl;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Node;
class DeleteNodeRequest extends ApplicationApiRequest
{
protected ?string $resource = AdminAcl::RESOURCE_NODES;
protected ?string $resource = Node::RESOURCE_NAME;
protected int $permission = AdminAcl::WRITE;
}

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