mirror of
https://github.com/pelican-dev/panel.git
synced 2026-05-04 18:00:48 +03:00
Compare commits
40 Commits
release/v1
...
v1.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
953ee940aa | ||
|
|
496eaaaf83 | ||
|
|
18cf6e9338 | ||
|
|
525a106e81 | ||
|
|
d22f975684 | ||
|
|
c4864feaa5 | ||
|
|
b7b72d7336 | ||
|
|
686c4375bc | ||
|
|
3f40256f8b | ||
|
|
a58e159478 | ||
|
|
d89af243a8 | ||
|
|
bddd6af8af | ||
|
|
e1bdf95971 | ||
|
|
465a03bf0e | ||
|
|
2c2e52b18a | ||
|
|
fcef8d69ae | ||
|
|
8662806dfd | ||
|
|
acf43f2826 | ||
|
|
dfba8e3993 | ||
|
|
56484a2282 | ||
|
|
56b4938dc2 | ||
|
|
10806d6d6b | ||
|
|
a04937d698 | ||
|
|
8a3d67ada0 | ||
|
|
833ae30e59 | ||
|
|
1fdff43ae7 | ||
|
|
bb7c0e0e66 | ||
|
|
447e889a4f | ||
|
|
1c1c8c0cc6 | ||
|
|
7dad2d0e42 | ||
|
|
212c93c2ba | ||
|
|
7557dc1c8d | ||
|
|
07735464c7 | ||
|
|
8ba15538a9 | ||
|
|
c115c6ddf5 | ||
|
|
160ea1ed50 | ||
|
|
7164951085 | ||
|
|
40721a2cb8 | ||
|
|
c464b321dd | ||
|
|
0f8c27a297 |
@@ -4,7 +4,7 @@ APP_KEY=
|
||||
APP_TIMEZONE=UTC
|
||||
APP_URL=http://panel.test
|
||||
APP_LOCALE=en
|
||||
APP_ENVIRONMENT_ONLY=true
|
||||
APP_INSTALLED=false
|
||||
|
||||
LOG_CHANNEL=daily
|
||||
LOG_STACK=single
|
||||
@@ -27,11 +27,7 @@ MAIL_FROM_ADDRESS=no-reply@example.com
|
||||
MAIL_FROM_NAME="Pelican Admin"
|
||||
# Set this to your domain to prevent it defaulting to 'localhost', causing mail servers such as Gmail to reject your mail
|
||||
# MAIL_EHLO_DOMAIN=panel.example.com
|
||||
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
# Set this to true, and set start & end ports to auto create allocations.
|
||||
PANEL_CLIENT_ALLOCATIONS_ENABLED=false
|
||||
PANEL_CLIENT_ALLOCATIONS_RANGE_START=
|
||||
PANEL_CLIENT_ALLOCATIONS_RANGE_END=
|
||||
|
||||
9
.github/workflows/ci.yaml
vendored
9
.github/workflows/ci.yaml
vendored
@@ -32,7 +32,6 @@ jobs:
|
||||
APP_KEY: ThisIsARandomStringForTests12345
|
||||
APP_TIMEZONE: UTC
|
||||
APP_URL: http://localhost/
|
||||
APP_ENVIRONMENT_ONLY: "true"
|
||||
CACHE_DRIVER: array
|
||||
MAIL_MAILER: array
|
||||
SESSION_DRIVER: array
|
||||
@@ -41,6 +40,8 @@ jobs:
|
||||
DB_HOST: 127.0.0.1
|
||||
DB_DATABASE: testing
|
||||
DB_USERNAME: root
|
||||
GUZZLE_TIMEOUT: 60
|
||||
GUZZLE_CONNECT_TIMEOUT: 60
|
||||
steps:
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -104,7 +105,6 @@ jobs:
|
||||
APP_KEY: ThisIsARandomStringForTests12345
|
||||
APP_TIMEZONE: UTC
|
||||
APP_URL: http://localhost/
|
||||
APP_ENVIRONMENT_ONLY: "true"
|
||||
CACHE_DRIVER: array
|
||||
MAIL_MAILER: array
|
||||
SESSION_DRIVER: array
|
||||
@@ -113,6 +113,8 @@ jobs:
|
||||
DB_HOST: 127.0.0.1
|
||||
DB_DATABASE: testing
|
||||
DB_USERNAME: root
|
||||
GUZZLE_TIMEOUT: 60
|
||||
GUZZLE_CONNECT_TIMEOUT: 60
|
||||
steps:
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -166,13 +168,14 @@ jobs:
|
||||
APP_KEY: ThisIsARandomStringForTests12345
|
||||
APP_TIMEZONE: UTC
|
||||
APP_URL: http://localhost/
|
||||
APP_ENVIRONMENT_ONLY: "true"
|
||||
CACHE_DRIVER: array
|
||||
MAIL_MAILER: array
|
||||
SESSION_DRIVER: array
|
||||
QUEUE_CONNECTION: sync
|
||||
DB_CONNECTION: sqlite
|
||||
DB_DATABASE: testing.sqlite
|
||||
GUZZLE_TIMEOUT: 60
|
||||
GUZZLE_CONNECT_TIMEOUT: 60
|
||||
steps:
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Console\Commands\Environment;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
use App\Traits\Commands\EnvironmentWriterTrait;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
@@ -11,154 +10,41 @@ class AppSettingsCommand extends Command
|
||||
{
|
||||
use EnvironmentWriterTrait;
|
||||
|
||||
public const CACHE_DRIVERS = [
|
||||
'file' => 'Filesystem (recommended)',
|
||||
'redis' => 'Redis',
|
||||
];
|
||||
|
||||
public const SESSION_DRIVERS = [
|
||||
'file' => 'Filesystem (recommended)',
|
||||
'redis' => 'Redis',
|
||||
'database' => 'Database',
|
||||
'cookie' => 'Cookie',
|
||||
];
|
||||
|
||||
public const QUEUE_DRIVERS = [
|
||||
'database' => 'Database (recommended)',
|
||||
'redis' => 'Redis',
|
||||
'sync' => 'Synchronous',
|
||||
];
|
||||
|
||||
protected $description = 'Configure basic environment settings for the Panel.';
|
||||
|
||||
protected $signature = 'p:environment:setup
|
||||
{--url= : The URL that this Panel is running on.}
|
||||
{--cache= : The cache driver backend to use.}
|
||||
{--session= : The session driver backend to use.}
|
||||
{--queue= : The queue driver backend to use.}
|
||||
{--redis-host= : Redis host to use for connections.}
|
||||
{--redis-pass= : Password used to connect to redis.}
|
||||
{--redis-port= : Port to connect to redis over.}
|
||||
{--settings-ui= : Enable or disable the settings UI.}';
|
||||
{--url= : The URL that this Panel is running on.}';
|
||||
|
||||
protected array $variables = [];
|
||||
|
||||
/**
|
||||
* AppSettingsCommand constructor.
|
||||
*/
|
||||
public function __construct(private Kernel $console)
|
||||
public function handle(): void
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
$path = base_path('.env');
|
||||
if (!file_exists($path)) {
|
||||
$this->comment('Copying example .env file');
|
||||
copy($path . '.example', $path);
|
||||
}
|
||||
|
||||
if (!config('app.key')) {
|
||||
$this->comment('Generating app key');
|
||||
Artisan::call('key:generate');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle command execution.
|
||||
*
|
||||
* @throws \App\Exceptions\PanelException
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$this->variables['APP_TIMEZONE'] = 'UTC';
|
||||
|
||||
$this->output->comment(__('commands.appsettings.comment.url'));
|
||||
$this->variables['APP_URL'] = $this->option('url') ?? $this->ask(
|
||||
'Application URL',
|
||||
config('app.url', 'https://example.com')
|
||||
);
|
||||
|
||||
$selected = config('cache.default', 'file');
|
||||
$this->variables['CACHE_STORE'] = $this->option('cache') ?? $this->choice(
|
||||
'Cache Driver',
|
||||
self::CACHE_DRIVERS,
|
||||
array_key_exists($selected, self::CACHE_DRIVERS) ? $selected : null
|
||||
);
|
||||
|
||||
$selected = config('session.driver', 'file');
|
||||
$this->variables['SESSION_DRIVER'] = $this->option('session') ?? $this->choice(
|
||||
'Session Driver',
|
||||
self::SESSION_DRIVERS,
|
||||
array_key_exists($selected, self::SESSION_DRIVERS) ? $selected : null
|
||||
);
|
||||
|
||||
$selected = config('queue.default', 'database');
|
||||
$this->variables['QUEUE_CONNECTION'] = $this->option('queue') ?? $this->choice(
|
||||
'Queue Driver',
|
||||
self::QUEUE_DRIVERS,
|
||||
array_key_exists($selected, self::QUEUE_DRIVERS) ? $selected : null
|
||||
);
|
||||
|
||||
if (!is_null($this->option('settings-ui'))) {
|
||||
$this->variables['APP_ENVIRONMENT_ONLY'] = $this->option('settings-ui') == 'true' ? 'false' : 'true';
|
||||
} else {
|
||||
$this->variables['APP_ENVIRONMENT_ONLY'] = $this->confirm(__('commands.appsettings.comment.settings_ui'), true) ? 'false' : 'true';
|
||||
}
|
||||
|
||||
// Make sure session cookies are set as "secure" when using HTTPS
|
||||
if (str_starts_with($this->variables['APP_URL'], 'https://')) {
|
||||
$this->variables['SESSION_SECURE_COOKIE'] = 'true';
|
||||
}
|
||||
|
||||
$redisUsed = count(collect($this->variables)->filter(function ($item) {
|
||||
return $item === 'redis';
|
||||
})) !== 0;
|
||||
|
||||
if ($redisUsed) {
|
||||
$this->requestRedisSettings();
|
||||
}
|
||||
|
||||
$path = base_path('.env');
|
||||
if (!file_exists($path)) {
|
||||
copy($path . '.example', $path);
|
||||
}
|
||||
|
||||
$this->comment('Writing variables to .env file');
|
||||
$this->writeToEnvironment($this->variables);
|
||||
|
||||
if (!config('app.key')) {
|
||||
Artisan::call('key:generate');
|
||||
}
|
||||
|
||||
if ($this->variables['QUEUE_CONNECTION'] !== 'sync') {
|
||||
$this->call('p:environment:queue-service', [
|
||||
'--use-redis' => $redisUsed,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->info($this->console->output());
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request redis connection details and verify them.
|
||||
*/
|
||||
private function requestRedisSettings(): void
|
||||
{
|
||||
$this->output->note(__('commands.appsettings.redis.note'));
|
||||
$this->variables['REDIS_HOST'] = $this->option('redis-host') ?? $this->ask(
|
||||
'Redis Host',
|
||||
config('database.redis.default.host')
|
||||
);
|
||||
|
||||
$askForRedisPassword = true;
|
||||
if (!empty(config('database.redis.default.password'))) {
|
||||
$this->variables['REDIS_PASSWORD'] = config('database.redis.default.password');
|
||||
$askForRedisPassword = $this->confirm('It seems a password is already defined for Redis, would you like to change it?');
|
||||
}
|
||||
|
||||
if ($askForRedisPassword) {
|
||||
$this->output->comment(__('commands.appsettings.redis.comment'));
|
||||
$this->variables['REDIS_PASSWORD'] = $this->option('redis-pass') ?? $this->output->askHidden(
|
||||
'Redis Password'
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->variables['REDIS_PASSWORD'])) {
|
||||
$this->variables['REDIS_PASSWORD'] = 'null';
|
||||
}
|
||||
|
||||
$this->variables['REDIS_PORT'] = $this->option('redis-port') ?? $this->ask(
|
||||
'Redis Port',
|
||||
config('database.redis.default.port')
|
||||
);
|
||||
$this->info("Setup complete. Vist {$this->variables['APP_URL']}/installer to complete the installation");
|
||||
}
|
||||
}
|
||||
|
||||
68
app/Console/Commands/Environment/CacheSettingsCommand.php
Normal file
68
app/Console/Commands/Environment/CacheSettingsCommand.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Environment;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
use App\Traits\Commands\EnvironmentWriterTrait;
|
||||
use App\Traits\Commands\RequestRedisSettingsTrait;
|
||||
|
||||
class CacheSettingsCommand extends Command
|
||||
{
|
||||
use EnvironmentWriterTrait;
|
||||
use RequestRedisSettingsTrait;
|
||||
|
||||
public const CACHE_DRIVERS = [
|
||||
'file' => 'Filesystem (default)',
|
||||
'database' => 'Database',
|
||||
'redis' => 'Redis',
|
||||
];
|
||||
|
||||
protected $description = 'Configure cache settings for the Panel.';
|
||||
|
||||
protected $signature = 'p:environment:cache
|
||||
{--driver= : The cache driver backend to use.}
|
||||
{--redis-host= : Redis host to use for connections.}
|
||||
{--redis-pass= : Password used to connect to redis.}
|
||||
{--redis-port= : Port to connect to redis over.}';
|
||||
|
||||
protected array $variables = [];
|
||||
|
||||
/**
|
||||
* CacheSettingsCommand constructor.
|
||||
*/
|
||||
public function __construct(private Kernel $console)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle command execution.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$selected = config('cache.default', 'file');
|
||||
$this->variables['CACHE_STORE'] = $this->option('driver') ?? $this->choice(
|
||||
'Cache Driver',
|
||||
self::CACHE_DRIVERS,
|
||||
array_key_exists($selected, self::CACHE_DRIVERS) ? $selected : null
|
||||
);
|
||||
|
||||
if ($this->variables['CACHE_STORE'] === 'redis') {
|
||||
$this->requestRedisSettings();
|
||||
|
||||
if (config('queue.default') !== 'sync') {
|
||||
$this->call('p:environment:queue-service', [
|
||||
'--use-redis' => true,
|
||||
'--overwrite' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->writeToEnvironment($this->variables);
|
||||
|
||||
$this->info($this->console->output());
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,8 @@ class EmailSettingsCommand extends Command
|
||||
|
||||
$this->writeToEnvironment($this->variables);
|
||||
|
||||
$this->call('queue:restart');
|
||||
|
||||
$this->line('Updating stored environment configuration file.');
|
||||
$this->line('');
|
||||
}
|
||||
|
||||
66
app/Console/Commands/Environment/QueueSettingsCommand.php
Normal file
66
app/Console/Commands/Environment/QueueSettingsCommand.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Environment;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
use App\Traits\Commands\EnvironmentWriterTrait;
|
||||
use App\Traits\Commands\RequestRedisSettingsTrait;
|
||||
|
||||
class QueueSettingsCommand extends Command
|
||||
{
|
||||
use EnvironmentWriterTrait;
|
||||
use RequestRedisSettingsTrait;
|
||||
|
||||
public const QUEUE_DRIVERS = [
|
||||
'database' => 'Database (default)',
|
||||
'redis' => 'Redis',
|
||||
'sync' => 'Synchronous',
|
||||
];
|
||||
|
||||
protected $description = 'Configure queue settings for the Panel.';
|
||||
|
||||
protected $signature = 'p:environment:queue
|
||||
{--driver= : The queue driver backend to use.}
|
||||
{--redis-host= : Redis host to use for connections.}
|
||||
{--redis-pass= : Password used to connect to redis.}
|
||||
{--redis-port= : Port to connect to redis over.}';
|
||||
|
||||
protected array $variables = [];
|
||||
|
||||
/**
|
||||
* QueueSettingsCommand constructor.
|
||||
*/
|
||||
public function __construct(private Kernel $console)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle command execution.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$selected = config('queue.default', 'database');
|
||||
$this->variables['QUEUE_CONNECTION'] = $this->option('driver') ?? $this->choice(
|
||||
'Queue Driver',
|
||||
self::QUEUE_DRIVERS,
|
||||
array_key_exists($selected, self::QUEUE_DRIVERS) ? $selected : null
|
||||
);
|
||||
|
||||
if ($this->variables['QUEUE_CONNECTION'] === 'redis') {
|
||||
$this->requestRedisSettings();
|
||||
|
||||
$this->call('p:environment:queue-service', [
|
||||
'--use-redis' => true,
|
||||
'--overwrite' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->writeToEnvironment($this->variables);
|
||||
|
||||
$this->info($this->console->output());
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ class QueueWorkerServiceCommand extends Command
|
||||
{--service-name= : Name of the queue worker service.}
|
||||
{--user= : The user that PHP runs under.}
|
||||
{--group= : The group that PHP runs under.}
|
||||
{--use-redis : Whether redis is used.}
|
||||
{--overwrite : Force overwrite if the service file already exists.}';
|
||||
|
||||
public function handle(): void
|
||||
@@ -32,7 +31,8 @@ class QueueWorkerServiceCommand extends Command
|
||||
$user = $this->option('user') ?? $this->ask('Webserver User', 'www-data');
|
||||
$group = $this->option('group') ?? $this->ask('Webserver Group', 'www-data');
|
||||
|
||||
$afterRedis = $this->option('use-redis') ? '
|
||||
$redisUsed = config('queue.default') === 'redis' || config('session.driver') === 'redis' || config('cache.default') === 'redis';
|
||||
$afterRedis = $redisUsed ? '
|
||||
After=redis-server.service' : '';
|
||||
|
||||
$basePath = base_path();
|
||||
|
||||
69
app/Console/Commands/Environment/SessionSettingsCommand.php
Normal file
69
app/Console/Commands/Environment/SessionSettingsCommand.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Environment;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
use App\Traits\Commands\EnvironmentWriterTrait;
|
||||
use App\Traits\Commands\RequestRedisSettingsTrait;
|
||||
|
||||
class SessionSettingsCommand extends Command
|
||||
{
|
||||
use EnvironmentWriterTrait;
|
||||
use RequestRedisSettingsTrait;
|
||||
|
||||
public const SESSION_DRIVERS = [
|
||||
'file' => 'Filesystem (default)',
|
||||
'redis' => 'Redis',
|
||||
'database' => 'Database',
|
||||
'cookie' => 'Cookie',
|
||||
];
|
||||
|
||||
protected $description = 'Configure session settings for the Panel.';
|
||||
|
||||
protected $signature = 'p:environment:session
|
||||
{--driver= : The session driver backend to use.}
|
||||
{--redis-host= : Redis host to use for connections.}
|
||||
{--redis-pass= : Password used to connect to redis.}
|
||||
{--redis-port= : Port to connect to redis over.}';
|
||||
|
||||
protected array $variables = [];
|
||||
|
||||
/**
|
||||
* SessionSettingsCommand constructor.
|
||||
*/
|
||||
public function __construct(private Kernel $console)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle command execution.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$selected = config('session.driver', 'file');
|
||||
$this->variables['SESSION_DRIVER'] = $this->option('driver') ?? $this->choice(
|
||||
'Session Driver',
|
||||
self::SESSION_DRIVERS,
|
||||
array_key_exists($selected, self::SESSION_DRIVERS) ? $selected : null
|
||||
);
|
||||
|
||||
if ($this->variables['SESSION_DRIVER'] === 'redis') {
|
||||
$this->requestRedisSettings();
|
||||
|
||||
if (config('queue.default') !== 'sync') {
|
||||
$this->call('p:environment:queue-service', [
|
||||
'--use-redis' => true,
|
||||
'--overwrite' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->writeToEnvironment($this->variables);
|
||||
|
||||
$this->info($this->console->output());
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
60
app/Console/Commands/Maintenance/PruneImagesCommand.php
Normal file
60
app/Console/Commands/Maintenance/PruneImagesCommand.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Maintenance;
|
||||
|
||||
use App\Models\Node;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class PruneImagesCommand extends Command
|
||||
{
|
||||
protected $signature = 'p:maintenance:prune-images {node?}';
|
||||
|
||||
protected $description = 'Clean up all dangling docker images to clear up disk space.';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$node = $this->argument('node');
|
||||
|
||||
if (empty($node)) {
|
||||
$nodes = Node::all();
|
||||
/** @var Node $node */
|
||||
foreach ($nodes as $node) {
|
||||
$this->cleanupImages($node);
|
||||
}
|
||||
} else {
|
||||
$this->cleanupImages((int) $node);
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanupImages(int|Node $node): void
|
||||
{
|
||||
if (!$node instanceof Node) {
|
||||
$node = Node::query()->findOrFail($node);
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::daemon($node)
|
||||
->connectTimeout(5)
|
||||
->timeout(30)
|
||||
->delete('/api/system/docker/image/prune')
|
||||
->json() ?? [];
|
||||
|
||||
if (empty($response) || $response['ImagesDeleted'] === null) {
|
||||
$this->warn("Node {$node->id}: No images to clean up.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$count = count($response['ImagesDeleted']);
|
||||
|
||||
$useBinaryPrefix = config('panel.use_binary_prefix');
|
||||
$space = round($useBinaryPrefix ? $response['SpaceReclaimed'] / 1024 / 1024 : $response['SpaceReclaimed'] / 1000 / 1000, 2) . ($useBinaryPrefix ? ' MiB' : ' MB');
|
||||
|
||||
$this->info("Node {$node->id}: Cleaned up {$count} dangling docker images. ({$space})");
|
||||
} catch (Exception $exception) {
|
||||
$this->error($exception->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ class DeleteUserCommand extends Command
|
||||
public function handle(): int
|
||||
{
|
||||
$search = $this->option('user') ?? $this->ask(trans('command/messages.user.search_users'));
|
||||
Assert::notEmpty($search, 'Search term should be an email address, got: %s.');
|
||||
Assert::notEmpty($search, 'Search term should not be empty.');
|
||||
|
||||
$results = User::query()
|
||||
->where('id', 'LIKE', "$search%")
|
||||
@@ -42,6 +42,8 @@ class DeleteUserCommand extends Command
|
||||
if (!$deleteUser = $this->ask(trans('command/messages.user.select_search_user'))) {
|
||||
return $this->handle();
|
||||
}
|
||||
|
||||
$deleteUser = User::query()->findOrFail($deleteUser);
|
||||
} else {
|
||||
if (count($results) > 1) {
|
||||
$this->error(trans('command/messages.user.multiple_found'));
|
||||
@@ -53,8 +55,7 @@ class DeleteUserCommand extends Command
|
||||
}
|
||||
|
||||
if ($this->confirm(trans('command/messages.user.confirm_delete')) || !$this->input->isInteractive()) {
|
||||
$user = User::query()->findOrFail($deleteUser);
|
||||
$user->delete();
|
||||
$deleteUser->delete();
|
||||
|
||||
$this->info(trans('command/messages.user.deleted'));
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Console;
|
||||
|
||||
use App\Jobs\NodeStatistics;
|
||||
use App\Models\ActivityLog;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Database\Console\PruneCommand;
|
||||
@@ -9,6 +10,7 @@ use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
use App\Console\Commands\Schedule\ProcessRunnableCommand;
|
||||
use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand;
|
||||
use App\Console\Commands\Maintenance\CleanServiceBackupFilesCommand;
|
||||
use App\Console\Commands\Maintenance\PruneImagesCommand;
|
||||
|
||||
class Kernel extends ConsoleKernel
|
||||
{
|
||||
@@ -30,7 +32,11 @@ class Kernel extends ConsoleKernel
|
||||
|
||||
// Execute scheduled commands for servers every minute, as if there was a normal cron running.
|
||||
$schedule->command(ProcessRunnableCommand::class)->everyMinute()->withoutOverlapping();
|
||||
|
||||
$schedule->command(CleanServiceBackupFilesCommand::class)->daily();
|
||||
$schedule->command(PruneImagesCommand::class)->daily();
|
||||
|
||||
$schedule->job(new NodeStatistics())->everyFiveSeconds()->withoutOverlapping();
|
||||
|
||||
if (config('backups.prune_age')) {
|
||||
// Every 30 minutes, run the backup pruning command so that any abandoned backups can be deleted.
|
||||
|
||||
@@ -6,12 +6,14 @@ enum ContainerStatus: string
|
||||
{
|
||||
// Docker Based
|
||||
case Created = 'created';
|
||||
case Starting = 'starting';
|
||||
case Running = 'running';
|
||||
case Restarting = 'restarting';
|
||||
case Exited = 'exited';
|
||||
case Paused = 'paused';
|
||||
case Dead = 'dead';
|
||||
case Removing = 'removing';
|
||||
case Stopping = 'stopping';
|
||||
case Offline = 'offline';
|
||||
|
||||
// HTTP Based
|
||||
@@ -20,15 +22,17 @@ enum ContainerStatus: string
|
||||
public function icon(): string
|
||||
{
|
||||
return match ($this) {
|
||||
|
||||
self::Created => 'tabler-heart-plus',
|
||||
self::Starting => 'tabler-heart-up',
|
||||
self::Running => 'tabler-heartbeat',
|
||||
self::Restarting => 'tabler-heart-bolt',
|
||||
self::Exited => 'tabler-heart-exclamation',
|
||||
self::Paused => 'tabler-heart-pause',
|
||||
self::Dead => 'tabler-heart-x',
|
||||
self::Dead, self::Offline => 'tabler-heart-x',
|
||||
self::Removing => 'tabler-heart-down',
|
||||
self::Missing => 'tabler-heart-question',
|
||||
self::Offline => 'tabler-heart-bolt',
|
||||
self::Missing => 'tabler-heart-search',
|
||||
self::Stopping => 'tabler-heart-minus',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,6 +40,7 @@ enum ContainerStatus: string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Created => 'primary',
|
||||
self::Starting => 'warning',
|
||||
self::Running => 'success',
|
||||
self::Restarting => 'info',
|
||||
self::Exited => 'danger',
|
||||
@@ -43,6 +48,7 @@ enum ContainerStatus: string
|
||||
self::Dead => 'danger',
|
||||
self::Removing => 'warning',
|
||||
self::Missing => 'danger',
|
||||
self::Stopping => 'warning',
|
||||
self::Offline => 'gray',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Clusters;
|
||||
|
||||
use Filament\Clusters\Cluster;
|
||||
|
||||
class Settings extends Cluster
|
||||
{
|
||||
protected static ?string $navigationIcon = 'tabler-settings';
|
||||
}
|
||||
132
app/Filament/Pages/Installer/PanelInstaller.php
Normal file
132
app/Filament/Pages/Installer/PanelInstaller.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer;
|
||||
|
||||
use App\Filament\Pages\Installer\Steps\AdminUserStep;
|
||||
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\RequirementsStep;
|
||||
use App\Services\Users\UserCreationService;
|
||||
use App\Traits\Commands\EnvironmentWriterTrait;
|
||||
use Exception;
|
||||
use Filament\Facades\Filament;
|
||||
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\Concerns\HasUnsavedDataChangesAlert;
|
||||
use Filament\Pages\SimplePage;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
/**
|
||||
* @property Form $form
|
||||
*/
|
||||
class PanelInstaller extends SimplePage implements HasForms
|
||||
{
|
||||
use EnvironmentWriterTrait;
|
||||
use HasUnsavedDataChangesAlert;
|
||||
use InteractsWithForms;
|
||||
|
||||
public $data = [];
|
||||
|
||||
protected static string $view = 'filament.pages.installer';
|
||||
|
||||
public function getMaxWidth(): MaxWidth|string
|
||||
{
|
||||
return MaxWidth::SevenExtraLarge;
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
if (is_installed()) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$this->form->fill();
|
||||
}
|
||||
|
||||
public function dehydrate(): void
|
||||
{
|
||||
Artisan::call('config:clear');
|
||||
Artisan::call('cache:clear');
|
||||
}
|
||||
|
||||
protected function getFormSchema(): array
|
||||
{
|
||||
return [
|
||||
Wizard::make([
|
||||
RequirementsStep::make(),
|
||||
EnvironmentStep::make(),
|
||||
DatabaseStep::make(),
|
||||
RedisStep::make()
|
||||
->hidden(fn (Get $get) => $get('env.SESSION_DRIVER') != 'redis' && $get('env.QUEUE_CONNECTION') != 'redis' && $get('env.CACHE_STORE') != 'redis'),
|
||||
AdminUserStep::make(),
|
||||
])
|
||||
->persistStepInQueryString()
|
||||
->submitAction(new HtmlString(Blade::render(<<<'BLADE'
|
||||
<x-filament::button
|
||||
type="submit"
|
||||
size="sm"
|
||||
>
|
||||
Finish
|
||||
</x-filament::button>
|
||||
BLADE))),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFormStatePath(): ?string
|
||||
{
|
||||
return 'data';
|
||||
}
|
||||
|
||||
protected function hasUnsavedDataChangesAlert(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$inputs = $this->form->getState();
|
||||
|
||||
// Write variables to .env file
|
||||
$variables = array_get($inputs, 'env');
|
||||
$this->writeToEnvironment($variables);
|
||||
|
||||
// Run migrations
|
||||
Artisan::call('migrate', [
|
||||
'--force' => true,
|
||||
'--seed' => true,
|
||||
]);
|
||||
|
||||
// Create first admin user
|
||||
$userData = array_get($inputs, 'user');
|
||||
$userData['root_admin'] = true;
|
||||
app(UserCreationService::class)->handle($userData);
|
||||
|
||||
// Install setup complete
|
||||
$this->writeToEnvironment(['APP_INSTALLED' => true]);
|
||||
|
||||
$this->rememberData();
|
||||
|
||||
Notification::make()
|
||||
->title('Successfully Installed')
|
||||
->success()
|
||||
->send();
|
||||
|
||||
redirect()->intended(Filament::getUrl());
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Installation Failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
}
|
||||
31
app/Filament/Pages/Installer/Steps/AdminUserStep.php
Normal file
31
app/Filament/Pages/Installer/Steps/AdminUserStep.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
|
||||
class AdminUserStep
|
||||
{
|
||||
public static function make(): Step
|
||||
{
|
||||
return Step::make('user')
|
||||
->label('Admin User')
|
||||
->schema([
|
||||
TextInput::make('user.email')
|
||||
->label('Admin E-Mail')
|
||||
->required()
|
||||
->email()
|
||||
->default('admin@example.com'),
|
||||
TextInput::make('user.username')
|
||||
->label('Admin Username')
|
||||
->required()
|
||||
->default('admin'),
|
||||
TextInput::make('user.password')
|
||||
->label('Admin Password')
|
||||
->required()
|
||||
->password()
|
||||
->revealable(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
95
app/Filament/Pages/Installer/Steps/DatabaseStep.php
Normal file
95
app/Filament/Pages/Installer/Steps/DatabaseStep.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
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\Database\DatabaseManager;
|
||||
use PDOException;
|
||||
|
||||
class DatabaseStep
|
||||
{
|
||||
public static function make(): Step
|
||||
{
|
||||
return Step::make('database')
|
||||
->label('Database')
|
||||
->columns()
|
||||
->schema([
|
||||
TextInput::make('env.DB_DATABASE')
|
||||
->label(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite' ? 'Database Path' : 'Database Name')
|
||||
->columnSpanFull()
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite' ? 'The path of your .sqlite file relative to the database folder.' : 'The name of the panel database.')
|
||||
->required()
|
||||
->default(fn (Get $get) => env('DB_DATABASE', $get('env.DB_CONNECTION') === 'sqlite' ? 'database.sqlite' : 'panel')),
|
||||
TextInput::make('env.DB_HOST')
|
||||
->label('Database Host')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The host of your database. Make sure it is reachable.')
|
||||
->required()
|
||||
->default(env('DB_HOST', '127.0.0.1'))
|
||||
->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'),
|
||||
TextInput::make('env.DB_PORT')
|
||||
->label('Database Port')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The port of your database.')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(env('DB_PORT', 3306))
|
||||
->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'),
|
||||
TextInput::make('env.DB_USERNAME')
|
||||
->label('Database Username')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The name of your database user.')
|
||||
->required()
|
||||
->default(env('DB_USERNAME', 'pelican'))
|
||||
->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'),
|
||||
TextInput::make('env.DB_PASSWORD')
|
||||
->label('Database Password')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The password of your database user. Can be empty.')
|
||||
->password()
|
||||
->revealable()
|
||||
->default(env('DB_PASSWORD'))
|
||||
->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'),
|
||||
])
|
||||
->afterValidation(function (Get $get) {
|
||||
$driver = $get('env.DB_CONNECTION');
|
||||
if ($driver !== 'sqlite') {
|
||||
/** @var DatabaseManager $database */
|
||||
$database = app(DatabaseManager::class);
|
||||
|
||||
try {
|
||||
config()->set('database.connections._panel_install_test', [
|
||||
'driver' => $driver,
|
||||
'host' => $get('env.DB_HOST'),
|
||||
'port' => $get('env.DB_PORT'),
|
||||
'database' => $get('env.DB_DATABASE'),
|
||||
'username' => $get('env.DB_USERNAME'),
|
||||
'password' => $get('env.DB_PASSWORD'),
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'strict' => true,
|
||||
]);
|
||||
|
||||
$database->connection('_panel_install_test')->getPdo();
|
||||
} catch (PDOException $exception) {
|
||||
Notification::make()
|
||||
->title('Database connection failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
$database->disconnect('_panel_install_test');
|
||||
|
||||
throw new Halt('Database connection failed');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
94
app/Filament/Pages/Installer/Steps/EnvironmentStep.php
Normal file
94
app/Filament/Pages/Installer/Steps/EnvironmentStep.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Forms\Set;
|
||||
|
||||
class EnvironmentStep
|
||||
{
|
||||
public const CACHE_DRIVERS = [
|
||||
'file' => 'Filesystem',
|
||||
'redis' => 'Redis',
|
||||
];
|
||||
|
||||
public const SESSION_DRIVERS = [
|
||||
'file' => 'Filesystem',
|
||||
'redis' => 'Redis',
|
||||
'database' => 'Database',
|
||||
'cookie' => 'Cookie',
|
||||
];
|
||||
|
||||
public const QUEUE_DRIVERS = [
|
||||
'database' => 'Database',
|
||||
'redis' => 'Redis',
|
||||
'sync' => 'Synchronous',
|
||||
];
|
||||
|
||||
public const DATABASE_DRIVERS = [
|
||||
'sqlite' => 'SQLite',
|
||||
'mariadb' => 'MariaDB',
|
||||
'mysql' => 'MySQL',
|
||||
];
|
||||
|
||||
public static function make(): Step
|
||||
{
|
||||
return Step::make('environment')
|
||||
->label('Environment')
|
||||
->columns()
|
||||
->schema([
|
||||
TextInput::make('env.APP_NAME')
|
||||
->label('App Name')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('This will be the Name of your Panel.')
|
||||
->required()
|
||||
->default(config('app.name')),
|
||||
TextInput::make('env.APP_URL')
|
||||
->label('App URL')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('This will be the URL you access your Panel from.')
|
||||
->required()
|
||||
->default(config('app.url'))
|
||||
->live()
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('env.SESSION_SECURE_COOKIE', str_starts_with($state, 'https://'))),
|
||||
Toggle::make('env.SESSION_SECURE_COOKIE')
|
||||
->hidden()
|
||||
->default(env('SESSION_SECURE_COOKIE')),
|
||||
ToggleButtons::make('env.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.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.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.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')),
|
||||
]);
|
||||
}
|
||||
}
|
||||
42
app/Filament/Pages/Installer/Steps/RedisStep.php
Normal file
42
app/Filament/Pages/Installer/Steps/RedisStep.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
|
||||
class RedisStep
|
||||
{
|
||||
public static function make(): Step
|
||||
{
|
||||
return Step::make('redis')
|
||||
->label('Redis')
|
||||
->columns()
|
||||
->schema([
|
||||
TextInput::make('env.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_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_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_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')),
|
||||
]);
|
||||
}
|
||||
}
|
||||
87
app/Filament/Pages/Installer/Steps/RequirementsStep.php
Normal file
87
app/Filament/Pages/Installer/Steps/RequirementsStep.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
|
||||
class RequirementsStep
|
||||
{
|
||||
public static function make(): Step
|
||||
{
|
||||
$correctPhpVersion = version_compare(PHP_VERSION, '8.2.0') >= 0;
|
||||
|
||||
$fields = [
|
||||
Section::make('PHP Version')
|
||||
->description('8.2 or newer')
|
||||
->icon($correctPhpVersion ? 'tabler-check' : 'tabler-x')
|
||||
->iconColor($correctPhpVersion ? 'success' : 'danger')
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content('Your PHP Version ' . ($correctPhpVersion ? 'is' : 'needs to be') .' 8.2 or newer.'),
|
||||
]),
|
||||
];
|
||||
|
||||
$phpExtensions = [
|
||||
'BCMath' => extension_loaded('bcmath'),
|
||||
'cURL' => extension_loaded('curl'),
|
||||
'GD' => extension_loaded('gd'),
|
||||
'intl' => extension_loaded('intl'),
|
||||
'mbstring' => extension_loaded('mbstring'),
|
||||
'MySQL' => extension_loaded('pdo_mysql'),
|
||||
'SQLite3' => extension_loaded('pdo_sqlite'),
|
||||
'XML' => extension_loaded('xml'),
|
||||
'Zip' => extension_loaded('zip'),
|
||||
];
|
||||
$allExtensionsInstalled = !in_array(false, $phpExtensions);
|
||||
|
||||
$fields[] = Section::make('PHP Extensions')
|
||||
->description(implode(', ', array_keys($phpExtensions)))
|
||||
->icon($allExtensionsInstalled ? 'tabler-check' : 'tabler-x')
|
||||
->iconColor($allExtensionsInstalled ? 'success' : 'danger')
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content('All needed PHP Extensions are installed.')
|
||||
->visible($allExtensionsInstalled),
|
||||
Placeholder::make('')
|
||||
->content('The following PHP Extensions are missing: ' . implode(', ', array_keys($phpExtensions, false)))
|
||||
->visible(!$allExtensionsInstalled),
|
||||
]);
|
||||
|
||||
$folderPermissions = [
|
||||
'Storage' => substr(sprintf('%o', fileperms(base_path('storage/'))), -4) >= 755,
|
||||
'Cache' => substr(sprintf('%o', fileperms(base_path('bootstrap/cache/'))), -4) >= 755,
|
||||
];
|
||||
$correctFolderPermissions = !in_array(false, $folderPermissions);
|
||||
|
||||
$fields[] = Section::make('Folder Permissions')
|
||||
->description(implode(', ', array_keys($folderPermissions)))
|
||||
->icon($correctFolderPermissions ? 'tabler-check' : 'tabler-x')
|
||||
->iconColor($correctFolderPermissions ? 'success' : 'danger')
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content('All Folders have the correct permissions.')
|
||||
->visible($correctFolderPermissions),
|
||||
Placeholder::make('')
|
||||
->content('The following Folders have wrong permissions: ' . implode(', ', array_keys($folderPermissions, false)))
|
||||
->visible(!$correctFolderPermissions),
|
||||
]);
|
||||
|
||||
return Step::make('requirements')
|
||||
->label('Server Requirements')
|
||||
->schema($fields)
|
||||
->afterValidation(function () use ($correctPhpVersion, $allExtensionsInstalled, $correctFolderPermissions) {
|
||||
if (!$correctPhpVersion || !$allExtensionsInstalled || !$correctFolderPermissions) {
|
||||
Notification::make()
|
||||
->title('Some requirements are missing!')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
throw new Halt();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
573
app/Filament/Pages/Settings.php
Normal file
573
app/Filament/Pages/Settings.php
Normal file
@@ -0,0 +1,573 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use App\Models\Backup;
|
||||
use App\Notifications\MailTested;
|
||||
use App\Traits\Commands\EnvironmentWriterTrait;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Actions\Action as FormAction;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\Tabs\Tab;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Concerns\HasUnsavedDataChangesAlert;
|
||||
use Filament\Pages\Concerns\InteractsWithHeaderActions;
|
||||
use Filament\Pages\Page;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Notification as MailNotification;
|
||||
|
||||
/**
|
||||
* @property Form $form
|
||||
*/
|
||||
class Settings extends Page implements HasForms
|
||||
{
|
||||
use EnvironmentWriterTrait;
|
||||
use HasUnsavedDataChangesAlert;
|
||||
use InteractsWithForms;
|
||||
use InteractsWithHeaderActions;
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-settings';
|
||||
protected static ?string $navigationGroup = 'Advanced';
|
||||
|
||||
protected static string $view = 'filament.pages.settings';
|
||||
|
||||
public ?array $data = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->form->fill();
|
||||
}
|
||||
|
||||
protected function getFormSchema(): array
|
||||
{
|
||||
return [
|
||||
Tabs::make('Tabs')
|
||||
->columns()
|
||||
->persistTabInQueryString()
|
||||
->tabs([
|
||||
Tab::make('general')
|
||||
->label('General')
|
||||
->icon('tabler-home')
|
||||
->schema($this->generalSettings()),
|
||||
Tab::make('recaptcha')
|
||||
->label('reCAPTCHA')
|
||||
->icon('tabler-shield')
|
||||
->schema($this->recaptchaSettings()),
|
||||
Tab::make('mail')
|
||||
->label('Mail')
|
||||
->icon('tabler-mail')
|
||||
->schema($this->mailSettings()),
|
||||
Tab::make('backup')
|
||||
->label('Backup')
|
||||
->icon('tabler-box')
|
||||
->schema($this->backupSettings()),
|
||||
Tab::make('misc')
|
||||
->label('Misc')
|
||||
->icon('tabler-tool')
|
||||
->schema($this->miscSettings()),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
private function generalSettings(): array
|
||||
{
|
||||
return [
|
||||
TextInput::make('APP_NAME')
|
||||
->label('App Name')
|
||||
->required()
|
||||
->default(env('APP_NAME', 'Pelican')),
|
||||
TextInput::make('APP_FAVICON')
|
||||
->label('App Favicon')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('Favicons should be placed in the public folder, located in the root panel directory.')
|
||||
->required()
|
||||
->default(env('APP_FAVICON', './pelican.ico')),
|
||||
Toggle::make('APP_DEBUG')
|
||||
->label('Enable Debug Mode?')
|
||||
->inline(false)
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('APP_DEBUG', (bool) $state))
|
||||
->default(env('APP_DEBUG', config('app.debug'))),
|
||||
ToggleButtons::make('FILAMENT_TOP_NAVIGATION')
|
||||
->label('Navigation')
|
||||
->inline()
|
||||
->options([
|
||||
false => 'Sidebar',
|
||||
true => 'Topbar',
|
||||
])
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('FILAMENT_TOP_NAVIGATION', (bool) $state))
|
||||
->default(env('FILAMENT_TOP_NAVIGATION', config('panel.filament.top-navigation'))),
|
||||
ToggleButtons::make('PANEL_USE_BINARY_PREFIX')
|
||||
->label('Unit prefix')
|
||||
->inline()
|
||||
->options([
|
||||
false => 'Decimal Prefix (MB/ GB)',
|
||||
true => 'Binary Prefix (MiB/ GiB)',
|
||||
])
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_USE_BINARY_PREFIX', (bool) $state))
|
||||
->default(env('PANEL_USE_BINARY_PREFIX', config('panel.use_binary_prefix'))),
|
||||
ToggleButtons::make('APP_2FA_REQUIRED')
|
||||
->label('2FA Requirement')
|
||||
->inline()
|
||||
->options([
|
||||
0 => 'Not required',
|
||||
1 => 'Required for only Admins',
|
||||
2 => 'Required for all Users',
|
||||
])
|
||||
->formatStateUsing(fn ($state): int => (int) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('APP_2FA_REQUIRED', (int) $state))
|
||||
->default(env('APP_2FA_REQUIRED', config('panel.auth.2fa_required'))),
|
||||
TagsInput::make('TRUSTED_PROXIES')
|
||||
->label('Trusted Proxies')
|
||||
->separator()
|
||||
->splitKeys(['Tab', ' '])
|
||||
->placeholder('New IP or IP Range')
|
||||
->default(env('TRUSTED_PROXIES', config('trustedproxy.proxies')))
|
||||
->hintActions([
|
||||
FormAction::make('clear')
|
||||
->label('Clear')
|
||||
->color('danger')
|
||||
->icon('tabler-trash')
|
||||
->requiresConfirmation()
|
||||
->action(fn (Set $set) => $set('TRUSTED_PROXIES', [])),
|
||||
FormAction::make('cloudflare')
|
||||
->label('Set to Cloudflare IPs')
|
||||
->icon('tabler-brand-cloudflare')
|
||||
->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',
|
||||
])),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
private function recaptchaSettings(): array
|
||||
{
|
||||
return [
|
||||
Toggle::make('RECAPTCHA_ENABLED')
|
||||
->label('Enable reCAPTCHA?')
|
||||
->inline(false)
|
||||
->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')
|
||||
->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')
|
||||
->label('Secret Key')
|
||||
->required()
|
||||
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
|
||||
->default(env('RECAPTCHA_SECRET_KEY', config('recaptcha.secret_key'))),
|
||||
];
|
||||
}
|
||||
|
||||
private function mailSettings(): array
|
||||
{
|
||||
return [
|
||||
ToggleButtons::make('MAIL_MAILER')
|
||||
->label('Mail Driver')
|
||||
->columnSpanFull()
|
||||
->inline()
|
||||
->options([
|
||||
'log' => 'Print mails to Log',
|
||||
'smtp' => 'SMTP Server',
|
||||
'sendmail' => 'sendmail Binary',
|
||||
'mailgun' => 'Mailgun',
|
||||
'mandrill' => 'Mandrill',
|
||||
'postmark' => 'Postmark',
|
||||
])
|
||||
->live()
|
||||
->default(env('MAIL_MAILER', config('mail.default')))
|
||||
->hintAction(
|
||||
FormAction::make('test')
|
||||
->label('Send Test Mail')
|
||||
->icon('tabler-send')
|
||||
->hidden(fn (Get $get) => $get('MAIL_MAILER') === 'log')
|
||||
->action(function () {
|
||||
try {
|
||||
MailNotification::route('mail', auth()->user()->email)
|
||||
->notify(new MailTested(auth()->user()));
|
||||
|
||||
Notification::make()
|
||||
->title('Test Mail sent')
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Test Mail failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
})
|
||||
),
|
||||
Section::make('"From" Settings')
|
||||
->description('Set the Address and Name used as "From" in mails.')
|
||||
->columns()
|
||||
->schema([
|
||||
TextInput::make('MAIL_FROM_ADDRESS')
|
||||
->label('From Address')
|
||||
->required()
|
||||
->email()
|
||||
->default(env('MAIL_FROM_ADDRESS', config('mail.from.address'))),
|
||||
TextInput::make('MAIL_FROM_NAME')
|
||||
->label('From Name')
|
||||
->required()
|
||||
->default(env('MAIL_FROM_NAME', config('mail.from.name'))),
|
||||
]),
|
||||
Section::make('SMTP Configuration')
|
||||
->columns()
|
||||
->visible(fn (Get $get) => $get('MAIL_MAILER') === 'smtp')
|
||||
->schema([
|
||||
TextInput::make('MAIL_HOST')
|
||||
->label('SMTP Host')
|
||||
->required()
|
||||
->default(env('MAIL_HOST', config('mail.mailers.smtp.host'))),
|
||||
TextInput::make('MAIL_PORT')
|
||||
->label('SMTP Port')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(env('MAIL_PORT', config('mail.mailers.smtp.port'))),
|
||||
TextInput::make('MAIL_USERNAME')
|
||||
->label('SMTP Username')
|
||||
->required()
|
||||
->default(env('MAIL_USERNAME', config('mail.mailers.smtp.username'))),
|
||||
TextInput::make('MAIL_PASSWORD')
|
||||
->label('SMTP Password')
|
||||
->password()
|
||||
->revealable()
|
||||
->default(env('MAIL_PASSWORD')),
|
||||
ToggleButtons::make('MAIL_ENCRYPTION')
|
||||
->label('SMTP encryption')
|
||||
->required()
|
||||
->inline()
|
||||
->options(['tls' => 'TLS', 'ssl' => 'SSL', '' => 'None'])
|
||||
->default(env('MAIL_ENCRYPTION', config('mail.mailers.smtp.encryption', 'tls'))),
|
||||
]),
|
||||
Section::make('Mailgun Configuration')
|
||||
->columns()
|
||||
->visible(fn (Get $get) => $get('MAIL_MAILER') === 'mailgun')
|
||||
->schema([
|
||||
TextInput::make('MAILGUN_DOMAIN')
|
||||
->label('Mailgun Domain')
|
||||
->required()
|
||||
->default(env('MAILGUN_DOMAIN', config('services.mailgun.domain'))),
|
||||
TextInput::make('MAILGUN_SECRET')
|
||||
->label('Mailgun Secret')
|
||||
->required()
|
||||
->default(env('MAIL_USERNAME', config('services.mailgun.secret'))),
|
||||
TextInput::make('MAILGUN_ENDPOINT')
|
||||
->label('Mailgun Endpoint')
|
||||
->required()
|
||||
->default(env('MAILGUN_ENDPOINT', config('services.mailgun.endpoint'))),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
private function backupSettings(): array
|
||||
{
|
||||
return [
|
||||
ToggleButtons::make('APP_BACKUP_DRIVER')
|
||||
->label('Backup Driver')
|
||||
->columnSpanFull()
|
||||
->inline()
|
||||
->options([
|
||||
Backup::ADAPTER_DAEMON => 'Wings',
|
||||
Backup::ADAPTER_AWS_S3 => 'S3',
|
||||
])
|
||||
->live()
|
||||
->default(env('APP_BACKUP_DRIVER', config('backups.default'))),
|
||||
Section::make('Throttles')
|
||||
->description('Configure how many backups can be created in a period. Set period to 0 to disable this throttle.')
|
||||
->columns()
|
||||
->schema([
|
||||
TextInput::make('BACKUP_THROTTLE_LIMIT')
|
||||
->label('Limit')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->default(config('backups.throttles.limit')),
|
||||
TextInput::make('BACKUP_THROTTLE_PERIOD')
|
||||
->label('Period')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->suffix('Seconds')
|
||||
->default(config('backups.throttles.period')),
|
||||
]),
|
||||
Section::make('S3 Configuration')
|
||||
->columns()
|
||||
->visible(fn (Get $get) => $get('APP_BACKUP_DRIVER') === Backup::ADAPTER_AWS_S3)
|
||||
->schema([
|
||||
TextInput::make('AWS_DEFAULT_REGION')
|
||||
->label('Default Region')
|
||||
->required()
|
||||
->default(config('backups.disks.s3.region')),
|
||||
TextInput::make('AWS_ACCESS_KEY_ID')
|
||||
->label('Access Key ID')
|
||||
->required()
|
||||
->default(config('backups.disks.s3.key')),
|
||||
TextInput::make('AWS_SECRET_ACCESS_KEY')
|
||||
->label('Secret Access Key')
|
||||
->required()
|
||||
->default(config('backups.disks.s3.secret')),
|
||||
TextInput::make('AWS_BACKUPS_BUCKET')
|
||||
->label('Bucket')
|
||||
->required()
|
||||
->default(config('backups.disks.s3.bucket')),
|
||||
TextInput::make('AWS_ENDPOINT')
|
||||
->label('Endpoint')
|
||||
->required()
|
||||
->default(config('backups.disks.s3.endpoint')),
|
||||
Toggle::make('AWS_USE_PATH_STYLE_ENDPOINT')
|
||||
->label('Use path style endpoint?')
|
||||
->inline(false)
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('AWS_USE_PATH_STYLE_ENDPOINT', (bool) $state))
|
||||
->default(env('AWS_USE_PATH_STYLE_ENDPOINT', config('backups.disks.s3.use_path_style_endpoint'))),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
private function miscSettings(): array
|
||||
{
|
||||
return [
|
||||
Section::make('Automatic Allocation Creation')
|
||||
->description('Toggle if Users can create allocations via the client area.')
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
Toggle::make('PANEL_CLIENT_ALLOCATIONS_ENABLED')
|
||||
->label('Allow Users to create allocations?')
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_CLIENT_ALLOCATIONS_ENABLED', (bool) $state))
|
||||
->default(env('PANEL_CLIENT_ALLOCATIONS_ENABLED', config('panel.client_features.allocations.enabled'))),
|
||||
TextInput::make('PANEL_CLIENT_ALLOCATIONS_RANGE_START')
|
||||
->label('Starting Port')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1024)
|
||||
->maxValue(65535)
|
||||
->visible(fn (Get $get) => $get('PANEL_CLIENT_ALLOCATIONS_ENABLED'))
|
||||
->default(env('PANEL_CLIENT_ALLOCATIONS_RANGE_START')),
|
||||
TextInput::make('PANEL_CLIENT_ALLOCATIONS_RANGE_END')
|
||||
->label('Ending Port')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1024)
|
||||
->maxValue(65535)
|
||||
->visible(fn (Get $get) => $get('PANEL_CLIENT_ALLOCATIONS_ENABLED'))
|
||||
->default(env('PANEL_CLIENT_ALLOCATIONS_RANGE_END')),
|
||||
]),
|
||||
Section::make('Mail Notifications')
|
||||
->description('Toggle which mail notifications should be sent to Users.')
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
Toggle::make('PANEL_SEND_INSTALL_NOTIFICATION')
|
||||
->label('Server Installed')
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_INSTALL_NOTIFICATION', (bool) $state))
|
||||
->default(env('PANEL_SEND_INSTALL_NOTIFICATION', config('panel.email.send_install_notification'))),
|
||||
Toggle::make('PANEL_SEND_REINSTALL_NOTIFICATION')
|
||||
->label('Server Reinstalled')
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_REINSTALL_NOTIFICATION', (bool) $state))
|
||||
->default(env('PANEL_SEND_REINSTALL_NOTIFICATION', config('panel.email.send_reinstall_notification'))),
|
||||
]),
|
||||
Section::make('Connections')
|
||||
->description('Timeouts used when making requests.')
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
TextInput::make('GUZZLE_TIMEOUT')
|
||||
->label('Request Timeout')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(15)
|
||||
->maxValue(60)
|
||||
->suffix('Seconds')
|
||||
->default(env('GUZZLE_TIMEOUT', config('panel.guzzle.timeout'))),
|
||||
TextInput::make('GUZZLE_CONNECT_TIMEOUT')
|
||||
->label('Connect Timeout')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(5)
|
||||
->maxValue(60)
|
||||
->suffix('Seconds')
|
||||
->default(env('GUZZLE_CONNECT_TIMEOUT', config('panel.guzzle.connect_timeout'))),
|
||||
]),
|
||||
Section::make('Activity Logs')
|
||||
->description('Configure how often old activity logs should be pruned and whether admin activities should be logged.')
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
TextInput::make('APP_ACTIVITY_PRUNE_DAYS')
|
||||
->label('Prune age')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(365)
|
||||
->suffix('Days')
|
||||
->default(env('APP_ACTIVITY_PRUNE_DAYS', config('activity.prune_days'))),
|
||||
Toggle::make('APP_ACTIVITY_HIDE_ADMIN')
|
||||
->label('Hide admin activities?')
|
||||
->inline(false)
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('APP_ACTIVITY_HIDE_ADMIN', (bool) $state))
|
||||
->default(env('APP_ACTIVITY_HIDE_ADMIN', config('activity.hide_admin_activity'))),
|
||||
]),
|
||||
Section::make('API')
|
||||
->description('Defines the rate limit for the number of requests per minute that can be executed.')
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
TextInput::make('APP_API_CLIENT_RATELIMIT')
|
||||
->label('Client API Rate Limit')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->suffix('Requests Per Minute')
|
||||
->default(env('APP_API_CLIENT_RATELIMIT', config('http.rate_limit.client'))),
|
||||
TextInput::make('APP_API_APPLICATION_RATELIMIT')
|
||||
->label('Application API Rate Limit')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->suffix('Requests Per Minute')
|
||||
->default(env('APP_API_APPLICATION_RATELIMIT', config('http.rate_limit.application'))),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFormStatePath(): ?string
|
||||
{
|
||||
return 'data';
|
||||
}
|
||||
|
||||
protected function hasUnsavedDataChangesAlert(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
try {
|
||||
$data = $this->form->getState();
|
||||
|
||||
// Convert bools to a string, so they are correctly written to the .env file
|
||||
$data = array_map(fn ($value) => is_bool($value) ? ($value ? 'true' : 'false') : $value, $data);
|
||||
|
||||
$this->writeToEnvironment($data);
|
||||
|
||||
Artisan::call('config:clear');
|
||||
Artisan::call('queue:restart');
|
||||
|
||||
$this->rememberData();
|
||||
|
||||
$this->redirect($this->getUrl());
|
||||
|
||||
Notification::make()
|
||||
->title('Settings saved')
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Save failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('save')
|
||||
->action('save')
|
||||
->keyBindings(['mod+s']),
|
||||
];
|
||||
|
||||
}
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -64,8 +64,7 @@ class EditDatabaseHost extends EditRecord
|
||||
->helperText('The password for the database user.')
|
||||
->password()
|
||||
->revealable()
|
||||
->maxLength(255)
|
||||
->required(),
|
||||
->maxLength(255),
|
||||
Select::make('node_id')
|
||||
->searchable()
|
||||
->preload()
|
||||
|
||||
@@ -74,9 +74,10 @@ class CreateEgg extends CreateRecord
|
||||
->helperText('')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
TextInput::make('update_url')
|
||||
->disabled()
|
||||
->helperText('Not implemented.')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('URLs must point directly to the raw .json file.')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->url(),
|
||||
KeyValue::make('docker_images')
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
|
||||
@@ -91,8 +91,10 @@ class EditEgg extends EditRecord
|
||||
->helperText('')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
TextInput::make('update_url')
|
||||
->disabled()
|
||||
->helperText('Not implemented.')
|
||||
->label('Update URL')
|
||||
->url()
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('URLs must point directly to the raw .json file.')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
KeyValue::make('docker_images')
|
||||
->live()
|
||||
@@ -249,6 +251,7 @@ class EditEgg extends EditRecord
|
||||
->schema([
|
||||
TextInput::make('url')
|
||||
->label('URL')
|
||||
->default(fn (Egg $egg): ?string => $egg->update_url)
|
||||
->hint('Link to the egg file (eg. minecraft.json)')
|
||||
->url(),
|
||||
]),
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
namespace App\Filament\Resources\NodeResource\Pages;
|
||||
|
||||
use App\Filament\Resources\NodeResource;
|
||||
use App\Filament\Resources\NodeResource\Widgets\NodeMemoryChart;
|
||||
use App\Filament\Resources\NodeResource\Widgets\NodeStorageChart;
|
||||
use App\Models\Node;
|
||||
use App\Services\Nodes\NodeUpdateService;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
@@ -17,6 +16,7 @@ use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Components\View;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
@@ -41,6 +41,32 @@ class EditNode extends EditRecord
|
||||
->persistTabInQueryString()
|
||||
->columnSpanFull()
|
||||
->tabs([
|
||||
Tab::make('')
|
||||
->label('Overview')
|
||||
->icon('tabler-chart-area-line-filled')
|
||||
->columns(6)
|
||||
->schema([
|
||||
Fieldset::make()
|
||||
->label('Node Information')
|
||||
->columns(4)
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->label('Wings Version')
|
||||
->content(fn (Node $node) => $node->systemInformation()['version'] ?? 'Unknown'),
|
||||
Placeholder::make('')
|
||||
->label('CPU Threads')
|
||||
->content(fn (Node $node) => $node->systemInformation()['cpu_count'] ?? 0),
|
||||
Placeholder::make('')
|
||||
->label('Architecture')
|
||||
->content(fn (Node $node) => $node->systemInformation()['architecture'] ?? 'Unknown'),
|
||||
Placeholder::make('')
|
||||
->label('Kernel')
|
||||
->content(fn (Node $node) => $node->systemInformation()['kernel_version'] ?? 'Unknown'),
|
||||
]),
|
||||
View::make('filament.components.node-cpu-chart')->columnSpan(3),
|
||||
View::make('filament.components.node-memory-chart')->columnSpan(3),
|
||||
// TODO: Make purdy View::make('filament.components.node-storage-chart')->columnSpan(3),
|
||||
]),
|
||||
Tab::make('Basic Settings')
|
||||
->icon('tabler-server')
|
||||
->schema([
|
||||
@@ -437,16 +463,17 @@ class EditNode extends EditRecord
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFooterWidgets(): array
|
||||
{
|
||||
return [
|
||||
NodeStorageChart::class,
|
||||
NodeMemoryChart::class,
|
||||
];
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
$this->fillForm();
|
||||
}
|
||||
|
||||
protected function getColumnSpan()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
protected function getColumnStart()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
81
app/Filament/Resources/NodeResource/Widgets/NodeCpuChart.php
Normal file
81
app/Filament/Resources/NodeResource/Widgets/NodeCpuChart.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\NodeResource\Widgets;
|
||||
|
||||
use App\Models\Node;
|
||||
use Carbon\Carbon;
|
||||
use Filament\Support\RawJs;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class NodeCpuChart extends ChartWidget
|
||||
{
|
||||
protected static ?string $pollingInterval = '5s';
|
||||
protected static ?string $maxHeight = '300px';
|
||||
|
||||
public ?Model $record = null;
|
||||
|
||||
protected function getData(): array
|
||||
{
|
||||
/** @var Node $node */
|
||||
$node = $this->record;
|
||||
$threads = $node->systemInformation()['cpu_count'] ?? 0;
|
||||
|
||||
$cpu = collect(cache()->get("nodes.$node->id.cpu_percent"))
|
||||
->slice(-10)
|
||||
->map(fn ($value, $key) => [
|
||||
'cpu' => number_format($value * $threads, 2),
|
||||
'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'),
|
||||
])
|
||||
->all();
|
||||
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'data' => array_column($cpu, 'cpu'),
|
||||
'backgroundColor' => [
|
||||
'rgba(96, 165, 250, 0.3)',
|
||||
],
|
||||
'tension' => '0.3',
|
||||
'fill' => true,
|
||||
],
|
||||
],
|
||||
'labels' => array_column($cpu, 'timestamp'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getType(): string
|
||||
{
|
||||
return 'line';
|
||||
}
|
||||
|
||||
protected function getOptions(): RawJs
|
||||
{
|
||||
return RawJs::make(<<<'JS'
|
||||
{
|
||||
scales: {
|
||||
y: {
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
JS);
|
||||
}
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
/** @var Node $node */
|
||||
$node = $this->record;
|
||||
$threads = $node->systemInformation()['cpu_count'] ?? 0;
|
||||
|
||||
$cpu = number_format(collect(cache()->get("nodes.$node->id.cpu_percent"))->last() * $threads, 2);
|
||||
$max = number_format($threads * 100) . '%';
|
||||
|
||||
return 'CPU - ' . $cpu . '% Of ' . $max;
|
||||
}
|
||||
}
|
||||
@@ -3,66 +3,83 @@
|
||||
namespace App\Filament\Resources\NodeResource\Widgets;
|
||||
|
||||
use App\Models\Node;
|
||||
use Carbon\Carbon;
|
||||
use Filament\Support\RawJs;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class NodeMemoryChart extends ChartWidget
|
||||
{
|
||||
protected static ?string $heading = 'Memory';
|
||||
|
||||
protected static ?string $pollingInterval = '60s';
|
||||
protected static ?string $pollingInterval = '5s';
|
||||
protected static ?string $maxHeight = '300px';
|
||||
|
||||
public ?Model $record = null;
|
||||
|
||||
protected static ?array $options = [
|
||||
'scales' => [
|
||||
'x' => [
|
||||
'grid' => [
|
||||
'display' => false,
|
||||
],
|
||||
'ticks' => [
|
||||
'display' => false,
|
||||
],
|
||||
],
|
||||
'y' => [
|
||||
'grid' => [
|
||||
'display' => false,
|
||||
],
|
||||
'ticks' => [
|
||||
'display' => false,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
protected function getData(): array
|
||||
{
|
||||
/** @var Node $node */
|
||||
$node = $this->record;
|
||||
|
||||
$total = ($node->statistics()['memory_total'] ?? 0) / 1024 / 1024 / 1024;
|
||||
$used = ($node->statistics()['memory_used'] ?? 0) / 1024 / 1024 / 1024;
|
||||
$unused = $total - $used;
|
||||
$memUsed = collect(cache()->get("nodes.$node->id.memory_used"))->slice(-10)
|
||||
->map(fn ($value, $key) => [
|
||||
'memory' => config('panel.use_binary_prefix') ? $value / 1024 / 1024 / 1024 : $value / 1000 / 1000 / 1000,
|
||||
'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'),
|
||||
])
|
||||
->all();
|
||||
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => 'Data Cool',
|
||||
'data' => [$used, $unused],
|
||||
'data' => array_column($memUsed, 'memory'),
|
||||
'backgroundColor' => [
|
||||
'rgb(255, 99, 132)',
|
||||
'rgb(54, 162, 235)',
|
||||
'rgb(255, 205, 86)',
|
||||
'rgba(96, 165, 250, 0.3)',
|
||||
],
|
||||
'tension' => '0.3',
|
||||
'fill' => true,
|
||||
],
|
||||
// 'backgroundColor' => [],
|
||||
],
|
||||
'labels' => ['Used', 'Unused'],
|
||||
'labels' => array_column($memUsed, 'timestamp'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getType(): string
|
||||
{
|
||||
return 'pie';
|
||||
return 'line';
|
||||
}
|
||||
|
||||
protected function getOptions(): RawJs
|
||||
{
|
||||
return RawJs::make(<<<'JS'
|
||||
{
|
||||
scales: {
|
||||
y: {
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
JS);
|
||||
}
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
/** @var Node $node */
|
||||
$node = $this->record;
|
||||
$latestMemoryUsed = collect(cache()->get("nodes.$node->id.memory_used"))->last();
|
||||
$totalMemory = collect(cache()->get("nodes.$node->id.memory_total"))->last();
|
||||
|
||||
$used = config('panel.use_binary_prefix')
|
||||
? number_format($latestMemoryUsed / 1024 / 1024 / 1024, 2) .' GiB'
|
||||
: number_format($latestMemoryUsed / 1000 / 1000 / 1000, 2) . ' GB';
|
||||
|
||||
$total = config('panel.use_binary_prefix')
|
||||
? number_format($totalMemory / 1024 / 1024 / 1024, 2) .' GiB'
|
||||
: number_format($totalMemory / 1000 / 1000 / 1000, 2) . ' GB';
|
||||
|
||||
return 'Memory - ' . $used . ' Of ' . $total;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ use Illuminate\Database\Eloquent\Model;
|
||||
class NodeStorageChart extends ChartWidget
|
||||
{
|
||||
protected static ?string $heading = 'Storage';
|
||||
|
||||
protected static ?string $pollingInterval = '60s';
|
||||
protected static ?string $maxHeight = '300px';
|
||||
|
||||
public ?Model $record = null;
|
||||
|
||||
@@ -47,7 +47,6 @@ class NodeStorageChart extends ChartWidget
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => 'Data Cool',
|
||||
'data' => [$used, $unused],
|
||||
'backgroundColor' => [
|
||||
'rgb(255, 99, 132)',
|
||||
@@ -55,7 +54,6 @@ class NodeStorageChart extends ChartWidget
|
||||
'rgb(255, 205, 86)',
|
||||
],
|
||||
],
|
||||
// 'backgroundColor' => [],
|
||||
],
|
||||
'labels' => ['Used', 'Unused'],
|
||||
];
|
||||
|
||||
@@ -40,8 +40,8 @@ class CreateServer extends CreateRecord
|
||||
->icon('tabler-info-circle')
|
||||
->completedIcon('tabler-check')
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'sm' => 2,
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
@@ -61,7 +61,7 @@ class CreateServer extends CreateRecord
|
||||
}))
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 4,
|
||||
'sm' => 3,
|
||||
'md' => 2,
|
||||
'lg' => 3,
|
||||
])
|
||||
@@ -75,12 +75,12 @@ class CreateServer extends CreateRecord
|
||||
->label('Owner')
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 4,
|
||||
'md' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'lg' => 3,
|
||||
])
|
||||
->relationship('user', 'username')
|
||||
->searchable(['user', 'username', 'email'])
|
||||
->searchable(['username', 'email'])
|
||||
->getOptionLabelFromRecordUsing(fn (User $user) => "$user->email | $user->username " . ($user->root_admin ? '(admin)' : ''))
|
||||
->createOptionForm([
|
||||
Forms\Components\TextInput::make('username')
|
||||
@@ -125,10 +125,10 @@ class CreateServer extends CreateRecord
|
||||
->prefixIcon('tabler-server-2')
|
||||
->default(fn () => ($this->node = Node::query()->latest()->first())?->id)
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 2,
|
||||
'lg' => 2,
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 6,
|
||||
'lg' => 6,
|
||||
])
|
||||
->live()
|
||||
->relationship('node', 'name')
|
||||
@@ -146,10 +146,10 @@ class CreateServer extends CreateRecord
|
||||
->prefixIcon('tabler-network')
|
||||
->label('Primary Allocation')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 1,
|
||||
'lg' => 2,
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 2,
|
||||
'lg' => 3,
|
||||
])
|
||||
->disabled(fn (Forms\Get $get) => $get('node_id') === null)
|
||||
->searchable(['ip', 'port', 'ip_alias'])
|
||||
@@ -233,7 +233,9 @@ class CreateServer extends CreateRecord
|
||||
$end = min((int) $end, 2 ** 16 - 1);
|
||||
$range = $start <= $end ? range($start, $end) : range($end, $start);
|
||||
foreach ($range as $i) {
|
||||
$ports->push($i);
|
||||
if ($i > 1024 && $i <= 65535) {
|
||||
$ports->push($i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,8 +251,6 @@ class CreateServer extends CreateRecord
|
||||
$ports = $sortedPorts;
|
||||
}
|
||||
|
||||
$ports = $ports->filter(fn ($port) => $port > 1024 && $port < 65535)->values();
|
||||
|
||||
if ($update) {
|
||||
$set('allocation_ports', $ports->all());
|
||||
}
|
||||
@@ -268,10 +268,10 @@ class CreateServer extends CreateRecord
|
||||
Forms\Components\Repeater::make('allocation_additional')
|
||||
->label('Additional Allocations')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 1,
|
||||
'lg' => 2,
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'lg' => 3,
|
||||
])
|
||||
->addActionLabel('Add Allocation')
|
||||
->disabled(fn (Forms\Get $get) => $get('allocation_id') === null)
|
||||
@@ -303,12 +303,13 @@ class CreateServer extends CreateRecord
|
||||
),
|
||||
),
|
||||
|
||||
Forms\Components\TextInput::make('description')
|
||||
Forms\Components\Textarea::make('description')
|
||||
->placeholder('Description')
|
||||
->rows(3)
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 2,
|
||||
'default' => 2,
|
||||
'sm' => 6,
|
||||
'md' => 6,
|
||||
'lg' => 6,
|
||||
])
|
||||
->label('Notes'),
|
||||
@@ -491,12 +492,7 @@ class CreateServer extends CreateRecord
|
||||
->completedIcon('tabler-check')
|
||||
->schema([
|
||||
Forms\Components\Fieldset::make('Resource Limits')
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 4,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->columnSpan(6)
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
@@ -676,12 +672,7 @@ class CreateServer extends CreateRecord
|
||||
|
||||
Forms\Components\Fieldset::make('Feature Limits')
|
||||
->inlineLabel()
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 4,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->columnSpan(6)
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
@@ -712,18 +703,13 @@ class CreateServer extends CreateRecord
|
||||
->default(0),
|
||||
]),
|
||||
Forms\Components\Fieldset::make('Docker Settings')
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 4,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 3,
|
||||
'lg' => 3,
|
||||
'lg' => 4,
|
||||
])
|
||||
->columnSpan(6)
|
||||
->schema([
|
||||
Forms\Components\Select::make('select_image')
|
||||
->label('Image Name')
|
||||
@@ -742,7 +728,12 @@ class CreateServer extends CreateRecord
|
||||
return array_flip($images) + ['ghcr.io/custom-image' => 'Custom Image'];
|
||||
})
|
||||
->selectablePlaceholder(false)
|
||||
->columnSpan(1),
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 3,
|
||||
'lg' => 2,
|
||||
]),
|
||||
|
||||
Forms\Components\TextInput::make('image')
|
||||
->label('Image')
|
||||
@@ -758,13 +749,18 @@ class CreateServer extends CreateRecord
|
||||
}
|
||||
})
|
||||
->placeholder('Enter a custom Image')
|
||||
->columnSpan(2),
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 3,
|
||||
'lg' => 2,
|
||||
]),
|
||||
|
||||
Forms\Components\KeyValue::make('docker_labels')
|
||||
->label('Container Labels')
|
||||
->keyLabel('Title')
|
||||
->valueLabel('Description')
|
||||
->columnSpan(3),
|
||||
->columnSpanFull(),
|
||||
|
||||
Forms\Components\CheckboxList::make('mounts')
|
||||
->live()
|
||||
|
||||
@@ -7,6 +7,8 @@ use App\Services\Databases\DatabaseManagementService;
|
||||
use App\Services\Databases\DatabasePasswordService;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use LogicException;
|
||||
use App\Filament\Resources\ServerResource;
|
||||
use App\Http\Controllers\Admin\ServersController;
|
||||
@@ -36,22 +38,16 @@ class EditServer extends EditRecord
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 2,
|
||||
'lg' => 4,
|
||||
])
|
||||
->schema([
|
||||
Tabs::make('Tabs')
|
||||
->persistTabInQueryString()
|
||||
->columnSpan(6)
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'sm' => 2,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->columnSpanFull()
|
||||
->tabs([
|
||||
Tabs\Tab::make('Information')
|
||||
->icon('tabler-info-circle')
|
||||
@@ -124,7 +120,8 @@ class EditServer extends EditRecord
|
||||
'md' => 2,
|
||||
'lg' => 3,
|
||||
])
|
||||
->readOnly(),
|
||||
->readOnly()
|
||||
->dehydrated(false),
|
||||
Forms\Components\TextInput::make('uuid_short')
|
||||
->label('Short UUID')
|
||||
->hintAction(CopyAction::make())
|
||||
@@ -134,7 +131,8 @@ class EditServer extends EditRecord
|
||||
'md' => 2,
|
||||
'lg' => 3,
|
||||
])
|
||||
->readOnly(),
|
||||
->readOnly()
|
||||
->dehydrated(false),
|
||||
Forms\Components\TextInput::make('external_id')
|
||||
->label('External ID')
|
||||
->columnSpan([
|
||||
@@ -159,12 +157,6 @@ class EditServer extends EditRecord
|
||||
->icon('tabler-brand-docker')
|
||||
->schema([
|
||||
Forms\Components\Fieldset::make('Resource Limits')
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 4,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
@@ -340,12 +332,6 @@ class EditServer extends EditRecord
|
||||
|
||||
Forms\Components\Fieldset::make('Feature Limits')
|
||||
->inlineLabel()
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 4,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
@@ -370,12 +356,6 @@ class EditServer extends EditRecord
|
||||
->numeric(),
|
||||
]),
|
||||
Forms\Components\Fieldset::make('Docker Settings')
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 4,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
@@ -438,10 +418,10 @@ class EditServer extends EditRecord
|
||||
->disabledOn('edit')
|
||||
->prefixIcon('tabler-egg')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'default' => 6,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'lg' => 5,
|
||||
'lg' => 4,
|
||||
])
|
||||
->relationship('egg', 'name')
|
||||
->searchable()
|
||||
@@ -450,6 +430,12 @@ class EditServer extends EditRecord
|
||||
|
||||
Forms\Components\ToggleButtons::make('skip_scripts')
|
||||
->label('Run Egg Install Script?')->inline()
|
||||
->columnSpan([
|
||||
'default' => 6,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 2,
|
||||
])
|
||||
->options([
|
||||
false => 'Yes',
|
||||
true => 'Skip',
|
||||
@@ -467,12 +453,7 @@ class EditServer extends EditRecord
|
||||
Forms\Components\Textarea::make('startup')
|
||||
->label('Startup Command')
|
||||
->required()
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 4,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->columnSpan(6)
|
||||
->rows(function ($state) {
|
||||
return str($state)->explode("\n")->reduce(
|
||||
fn (int $carry, $line) => $carry + floor(strlen($line) / 125),
|
||||
@@ -484,17 +465,12 @@ class EditServer extends EditRecord
|
||||
->hintAction(CopyAction::make())
|
||||
->label('Default Startup Command')
|
||||
->disabled()
|
||||
->formatStateUsing(function ($state, Forms\Get $get, Forms\Set $set) {
|
||||
->formatStateUsing(function ($state, Get $get, Set $set) {
|
||||
$egg = Egg::query()->find($get('egg_id'));
|
||||
|
||||
return $egg->startup;
|
||||
})
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 4,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
]),
|
||||
->columnSpan(6),
|
||||
|
||||
Forms\Components\Repeater::make('server_variables')
|
||||
->relationship('serverVariables')
|
||||
@@ -747,8 +723,7 @@ class EditServer extends EditRecord
|
||||
Actions\DeleteAction::make('Delete')
|
||||
->successRedirectUrl(route('filament.admin.resources.servers.index'))
|
||||
->color('danger')
|
||||
->disabled(fn (Server $server) => $server->databases()->count() > 0)
|
||||
->label(fn (Server $server) => $server->databases()->count() > 0 ? 'Server has a Database' : 'Delete')
|
||||
->label('Delete')
|
||||
->after(fn (Server $server) => resolve(ServerDeletionService::class)->handle($server))
|
||||
->requiresConfirmation(),
|
||||
Actions\Action::make('console')
|
||||
|
||||
@@ -4,11 +4,15 @@ namespace App\Filament\Resources\ServerResource\RelationManagers;
|
||||
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Server;
|
||||
use Filament\Forms;
|
||||
use App\Services\Allocations\AssignmentService;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
/**
|
||||
* @method Server getOwnerRecord()
|
||||
@@ -21,7 +25,7 @@ class AllocationsRelationManager extends RelationManager
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('ip')
|
||||
TextInput::make('ip')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
]);
|
||||
@@ -62,9 +66,87 @@ class AllocationsRelationManager extends RelationManager
|
||||
->label(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id ? '' : 'Make Primary'),
|
||||
])
|
||||
->headerActions([
|
||||
//TODO Tables\Actions\CreateAction::make()->label('Create Allocation'),
|
||||
Tables\Actions\CreateAction::make()->label('Create Allocation')
|
||||
->createAnother(false)
|
||||
->form(fn () => [
|
||||
TextInput::make('allocation_ip')
|
||||
->datalist($this->getOwnerRecord()->node->ipAddresses())
|
||||
->label('IP Address')
|
||||
->inlineLabel()
|
||||
->ipv4()
|
||||
->helperText("Usually your machine's public IP unless you are port forwarding.")
|
||||
->required(),
|
||||
TextInput::make('allocation_alias')
|
||||
->label('Alias')
|
||||
->inlineLabel()
|
||||
->default(null)
|
||||
->helperText('Optional display name to help you remember what these are.')
|
||||
->required(false),
|
||||
TagsInput::make('allocation_ports')
|
||||
->placeholder('Examples: 27015, 27017-27019')
|
||||
->helperText(new HtmlString('
|
||||
These are the ports that users can connect to this Server through.
|
||||
<br />
|
||||
You would have to port forward these on your home network.
|
||||
'))
|
||||
->label('Ports')
|
||||
->inlineLabel()
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
$ports = collect();
|
||||
$update = false;
|
||||
foreach ($state as $portEntry) {
|
||||
if (!str_contains($portEntry, '-')) {
|
||||
if (is_numeric($portEntry)) {
|
||||
$ports->push((int) $portEntry);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Do not add non numerical ports
|
||||
$update = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$update = true;
|
||||
[$start, $end] = explode('-', $portEntry);
|
||||
if (!is_numeric($start) || !is_numeric($end)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$start = max((int) $start, 0);
|
||||
$end = min((int) $end, 2 ** 16 - 1);
|
||||
foreach (range($start, $end) as $i) {
|
||||
$ports->push($i);
|
||||
}
|
||||
}
|
||||
|
||||
$uniquePorts = $ports->unique()->values();
|
||||
if ($ports->count() > $uniquePorts->count()) {
|
||||
$update = true;
|
||||
$ports = $uniquePorts;
|
||||
}
|
||||
|
||||
$sortedPorts = $ports->sort()->values();
|
||||
if ($sortedPorts->all() !== $ports->all()) {
|
||||
$update = true;
|
||||
$ports = $sortedPorts;
|
||||
}
|
||||
|
||||
$ports = $ports->filter(fn ($port) => $port > 1024 && $port < 65535)->values();
|
||||
|
||||
if ($update) {
|
||||
$set('allocation_ports', $ports->all());
|
||||
}
|
||||
})
|
||||
->splitKeys(['Tab', ' ', ','])
|
||||
->required(),
|
||||
])
|
||||
->action(fn (array $data) => resolve(AssignmentService::class)->handle($this->getOwnerRecord()->node, $data, $this->getOwnerRecord())),
|
||||
Tables\Actions\AssociateAction::make()
|
||||
->multiple()
|
||||
->associateAnother(false)
|
||||
->preloadRecordSelect()
|
||||
->recordSelectOptionsQuery(fn ($query) => $query->whereBelongsTo($this->getOwnerRecord()->node))
|
||||
->label('Add Allocation'),
|
||||
|
||||
@@ -53,6 +53,7 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
->label(trans('strings.username'))
|
||||
->disabled()
|
||||
->readOnly()
|
||||
->dehydrated(false)
|
||||
->maxLength(255)
|
||||
->unique(ignoreRecord: true)
|
||||
->autofocus(),
|
||||
@@ -119,6 +120,7 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
->hidden(fn () => !cache()->get("users.{$this->getUser()->id}.2fa.tokens"))
|
||||
->rows(10)
|
||||
->readOnly()
|
||||
->dehydrated(false)
|
||||
->formatStateUsing(fn () => cache()->get("users.{$this->getUser()->id}.2fa.tokens"))
|
||||
->helperText('These will not be shown again!')
|
||||
->label('Backup Tokens:'),
|
||||
@@ -215,7 +217,7 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
Action::make('Create')
|
||||
->disabled(fn (Get $get) => $get('description') === null)
|
||||
->successRedirectUrl(route('filament.admin.auth.profile', ['tab' => '-api-keys-tab']))
|
||||
->action(function (Get $get, Action $action, $user) {
|
||||
->action(function (Get $get, Action $action, User $user) {
|
||||
$token = $user->createToken(
|
||||
$get('description'),
|
||||
$get('allowed_ips'),
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Settings;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Settings\AdvancedSettingsFormRequest;
|
||||
|
||||
class AdvancedController extends Controller
|
||||
{
|
||||
/**
|
||||
* AdvancedController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private AlertsMessageBag $alert,
|
||||
private Kernel $kernel,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render advanced Panel settings UI.
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
$showRecaptchaWarning = false;
|
||||
if (
|
||||
config('recaptcha._shipped_secret_key') === config('recaptcha.secret_key')
|
||||
|| config('recaptcha._shipped_website_key') === config('recaptcha.website_key')
|
||||
) {
|
||||
$showRecaptchaWarning = true;
|
||||
}
|
||||
|
||||
return view('admin.settings.advanced', [
|
||||
'showRecaptchaWarning' => $showRecaptchaWarning,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \App\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function update(AdvancedSettingsFormRequest $request): RedirectResponse
|
||||
{
|
||||
foreach ($request->normalize() as $key => $value) {
|
||||
Setting::set('settings::' . $key, $value);
|
||||
}
|
||||
|
||||
$this->kernel->call('queue:restart');
|
||||
$this->alert->success('Advanced settings have been updated successfully and the queue worker was restarted to apply these changes.')->flash();
|
||||
|
||||
return redirect()->route('admin.settings.advanced');
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Settings;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Traits\Helpers\AvailableLanguages;
|
||||
use App\Services\Helpers\SoftwareVersionService;
|
||||
use App\Http\Requests\Admin\Settings\BaseSettingsFormRequest;
|
||||
|
||||
class IndexController extends Controller
|
||||
{
|
||||
use AvailableLanguages;
|
||||
|
||||
/**
|
||||
* IndexController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private AlertsMessageBag $alert,
|
||||
private Kernel $kernel,
|
||||
private SoftwareVersionService $versionService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the UI for basic Panel settings.
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
return view('admin.settings.index', [
|
||||
'version' => $this->versionService,
|
||||
'languages' => $this->getAvailableLanguages(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle settings update.
|
||||
*
|
||||
* @throws \App\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function update(BaseSettingsFormRequest $request): RedirectResponse
|
||||
{
|
||||
foreach ($request->normalize() as $key => $value) {
|
||||
Setting::set('settings::' . $key, $value);
|
||||
}
|
||||
|
||||
$this->kernel->call('queue:restart');
|
||||
$this->alert->success('Panel settings have been updated successfully and the queue worker was restarted to apply these changes.')->flash();
|
||||
|
||||
return redirect()->route('admin.settings');
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Settings;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
use App\Notifications\MailTested;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use App\Exceptions\DisplayException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Providers\SettingsServiceProvider;
|
||||
use App\Http\Requests\Admin\Settings\MailSettingsFormRequest;
|
||||
|
||||
class MailController extends Controller
|
||||
{
|
||||
/**
|
||||
* MailController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private Kernel $kernel,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render UI for editing mail settings. This UI should only display if
|
||||
* the server is configured to send mail using SMTP.
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
return view('admin.settings.mail', [
|
||||
'disabled' => config('mail.default') !== 'smtp',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle request to update SMTP mail settings.
|
||||
*
|
||||
* @throws DisplayException
|
||||
* @throws \App\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function update(MailSettingsFormRequest $request): Response
|
||||
{
|
||||
if (config('mail.default') !== 'smtp') {
|
||||
throw new DisplayException('This feature is only available if SMTP is the selected email driver for the Panel.');
|
||||
}
|
||||
|
||||
$values = $request->normalize();
|
||||
if (array_get($values, 'mail:mailers:smtp:password') === '!e') {
|
||||
$values['mail:mailers:smtp:password'] = '';
|
||||
}
|
||||
|
||||
foreach ($values as $key => $value) {
|
||||
if (in_array($key, SettingsServiceProvider::getEncryptedKeys()) && !empty($value)) {
|
||||
$value = encrypt($value);
|
||||
}
|
||||
|
||||
Setting::set('settings::' . $key, $value);
|
||||
}
|
||||
|
||||
$this->kernel->call('queue:restart');
|
||||
|
||||
return response('', 204);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a request to send a test mail message.
|
||||
*/
|
||||
public function test(Request $request): Response
|
||||
{
|
||||
try {
|
||||
Notification::route('mail', $request->user()->email)
|
||||
->notify(new MailTested($request->user()));
|
||||
} catch (\Exception $exception) {
|
||||
return response($exception->getMessage(), 500);
|
||||
}
|
||||
|
||||
return response('', 204);
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ class StoreTaskRequest extends ViewScheduleRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'action' => 'required|in:command,power,backup',
|
||||
'action' => 'required|in:command,power,backup,delete_files',
|
||||
'payload' => 'required_unless:action,backup|string|nullable',
|
||||
'time_offset' => 'required|numeric|min:0|max:900',
|
||||
'sequence_id' => 'sometimes|required|numeric|min:1',
|
||||
|
||||
46
app/Jobs/NodeStatistics.php
Normal file
46
app/Jobs/NodeStatistics.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Node;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class NodeStatistics implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
foreach (Node::all() as $node) {
|
||||
$stats = $node->statistics();
|
||||
$timestamp = now()->getTimestamp();
|
||||
|
||||
foreach ($stats as $key => $value) {
|
||||
$cacheKey = "nodes.{$node->id}.$key";
|
||||
$data = cache()->get($cacheKey, []);
|
||||
|
||||
// Add current timestamp and value to the data array
|
||||
$data[$timestamp] = $value;
|
||||
|
||||
// Update the cache with the new data, expires in 1 minute
|
||||
cache()->put($cacheKey, $data, now()->addMinute());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,6 +12,7 @@ use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use App\Services\Backups\InitiateBackupService;
|
||||
use App\Repositories\Daemon\DaemonPowerRepository;
|
||||
use App\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
use App\Services\Files\DeleteFilesService;
|
||||
|
||||
class RunTaskJob extends Job implements ShouldQueue
|
||||
{
|
||||
@@ -34,7 +35,8 @@ class RunTaskJob extends Job implements ShouldQueue
|
||||
*/
|
||||
public function handle(
|
||||
InitiateBackupService $backupService,
|
||||
DaemonPowerRepository $powerRepository
|
||||
DaemonPowerRepository $powerRepository,
|
||||
DeleteFilesService $deleteFilesService
|
||||
): void {
|
||||
// Do not process a task that is not set to active, unless it's been manually triggered.
|
||||
if (!$this->task->schedule->is_active && !$this->manualRun) {
|
||||
@@ -67,6 +69,9 @@ class RunTaskJob extends Job implements ShouldQueue
|
||||
case Task::ACTION_BACKUP:
|
||||
$backupService->setIgnoredFiles(explode(PHP_EOL, $this->task->payload))->handle($server, null, true);
|
||||
break;
|
||||
case Task::ACTION_DELETE_FILES:
|
||||
$deleteFilesService->handle($server, explode(PHP_EOL, $this->task->payload));
|
||||
break;
|
||||
default:
|
||||
throw new \InvalidArgumentException('Invalid task action provided: ' . $this->task->action);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace App\Listeners\Auth;
|
||||
use App\Facades\Activity;
|
||||
use Illuminate\Auth\Events\Failed;
|
||||
use App\Events\Auth\DirectLogin;
|
||||
use Illuminate\Events\Dispatcher;
|
||||
|
||||
class AuthenticationListener
|
||||
{
|
||||
@@ -28,10 +27,4 @@ class AuthenticationListener
|
||||
|
||||
$activity->event($event instanceof Failed ? 'auth:fail' : 'auth:success')->log();
|
||||
}
|
||||
|
||||
public function subscribe(Dispatcher $events): void
|
||||
{
|
||||
$events->listen(Failed::class, self::class);
|
||||
$events->listen(DirectLogin::class, self::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +140,10 @@ class ActivityLog extends Model
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function (self $model) {
|
||||
$model->timestamp = Carbon::now();
|
||||
});
|
||||
|
||||
static::created(function (self $model) {
|
||||
Event::dispatch(new ActivityLogged($model));
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ use Illuminate\Support\Str;
|
||||
* @property array|null $features
|
||||
* @property string $docker_image -- deprecated, use $docker_images
|
||||
* @property array<string, string> $docker_images
|
||||
* @property string $update_url
|
||||
* @property string|null $update_url
|
||||
* @property bool $force_outgoing_ip
|
||||
* @property array|null $file_denylist
|
||||
* @property string|null $config_files
|
||||
@@ -95,6 +95,7 @@ class Egg extends Model
|
||||
'config_stop',
|
||||
'config_from',
|
||||
'startup',
|
||||
'update_url',
|
||||
'script_is_privileged',
|
||||
'script_install',
|
||||
'script_entry',
|
||||
|
||||
@@ -24,62 +24,4 @@ class Setting extends Model
|
||||
'key' => 'required|string|between:1,255',
|
||||
'value' => 'string',
|
||||
];
|
||||
|
||||
private static array $cache = [];
|
||||
|
||||
private static array $databaseMiss = [];
|
||||
|
||||
/**
|
||||
* Store a new persistent setting in the database.
|
||||
*/
|
||||
public static function set(string $key, string $value = null): void
|
||||
{
|
||||
// Clear item from the cache.
|
||||
self::clearCache($key);
|
||||
|
||||
self::query()->updateOrCreate(['key' => $key], ['value' => $value ?? '']);
|
||||
|
||||
self::$cache[$key] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a persistent setting from the database.
|
||||
*/
|
||||
public static function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
// If item has already been requested return it from the cache. If
|
||||
// we already know it is missing, immediately return the default value.
|
||||
if (array_key_exists($key, self::$cache)) {
|
||||
return self::$cache[$key];
|
||||
} elseif (array_key_exists($key, self::$databaseMiss)) {
|
||||
return value($default);
|
||||
}
|
||||
|
||||
$instance = self::query()->where('key', $key)->first();
|
||||
if (is_null($instance)) {
|
||||
self::$databaseMiss[$key] = true;
|
||||
|
||||
return value($default);
|
||||
}
|
||||
|
||||
return self::$cache[$key] = $instance->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a key from the database cache.
|
||||
*/
|
||||
public static function forget(string $key)
|
||||
{
|
||||
self::clearCache($key);
|
||||
|
||||
return self::query()->where('key', $key)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a key from the cache.
|
||||
*/
|
||||
private static function clearCache(string $key): void
|
||||
{
|
||||
unset(self::$cache[$key], self::$databaseMiss[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ class Task extends Model
|
||||
public const ACTION_POWER = 'power';
|
||||
public const ACTION_COMMAND = 'command';
|
||||
public const ACTION_BACKUP = 'backup';
|
||||
public const ACTION_DELETE_FILES = 'delete_files';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
|
||||
@@ -10,6 +10,8 @@ use App\Services\Helpers\SoftwareVersionService;
|
||||
use Dedoc\Scramble\Scramble;
|
||||
use Dedoc\Scramble\Support\Generator\OpenApi;
|
||||
use Dedoc\Scramble\Support\Generator\SecurityScheme;
|
||||
use Filament\Support\Colors\Color;
|
||||
use Filament\Support\Facades\FilamentColor;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Pagination\Paginator;
|
||||
use Illuminate\Support\Facades\Broadcast;
|
||||
@@ -80,6 +82,15 @@ class AppServiceProvider extends ServiceProvider
|
||||
Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) {
|
||||
$event->extendSocialite('discord', \SocialiteProviders\Discord\Provider::class);
|
||||
});
|
||||
|
||||
FilamentColor::register([
|
||||
'danger' => Color::Red,
|
||||
'gray' => Color::Zinc,
|
||||
'info' => Color::Sky,
|
||||
'primary' => Color::Blue,
|
||||
'success' => Color::Green,
|
||||
'warning' => Color::Amber,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,11 +98,6 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
// Only load the settings service provider if the environment is configured to allow it.
|
||||
if (!config('panel.load_environment_only', false) && $this->app->environment() !== 'testing') {
|
||||
$this->app->register(SettingsServiceProvider::class);
|
||||
}
|
||||
|
||||
$this->app->singleton('extensions.themes', function () {
|
||||
return new Theme();
|
||||
});
|
||||
|
||||
@@ -10,7 +10,6 @@ use App\Observers\UserObserver;
|
||||
use App\Observers\ServerObserver;
|
||||
use App\Observers\SubuserObserver;
|
||||
use App\Observers\EggVariableObserver;
|
||||
use App\Listeners\Auth\AuthenticationListener;
|
||||
use App\Events\Server\Installed as ServerInstalledEvent;
|
||||
use App\Notifications\ServerInstalled as ServerInstalledNotification;
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
@@ -24,10 +23,6 @@ class EventServiceProvider extends ServiceProvider
|
||||
ServerInstalledEvent::class => [ServerInstalledNotification::class],
|
||||
];
|
||||
|
||||
protected $subscribe = [
|
||||
AuthenticationListener::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* Register any events for your application.
|
||||
*/
|
||||
|
||||
@@ -9,7 +9,6 @@ use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||
use Filament\Http\Middleware\DispatchServingFilamentEvent;
|
||||
use Filament\Panel;
|
||||
use Filament\PanelProvider;
|
||||
use Filament\Support\Colors\Color;
|
||||
use Filament\Support\Facades\FilamentAsset;
|
||||
use Filament\Widgets;
|
||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||
@@ -37,21 +36,13 @@ class AdminPanelProvider extends PanelProvider
|
||||
->path('admin')
|
||||
->topNavigation(config('panel.filament.top-navigation', true))
|
||||
->login()
|
||||
->breadcrumbs(false)
|
||||
->homeUrl('/')
|
||||
->favicon(config('app.favicon', '/pelican.ico'))
|
||||
->brandName(config('app.name', 'Pelican'))
|
||||
->brandLogo(config('app.logo'))
|
||||
->brandLogoHeight('2rem')
|
||||
->profile(EditProfile::class, false)
|
||||
->colors([
|
||||
'danger' => Color::Red,
|
||||
'gray' => Color::Zinc,
|
||||
'info' => Color::Sky,
|
||||
'primary' => Color::Blue,
|
||||
'success' => Color::Green,
|
||||
'warning' => Color::Amber,
|
||||
'blurple' => Color::hex('#5865F2'),
|
||||
])
|
||||
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
|
||||
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
|
||||
->discoverClusters(in: app_path('Filament/Clusters'), for: 'App\\Filament\\Clusters')
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Exception;
|
||||
use Psr\Log\LoggerInterface as Log;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class SettingsServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* An array of configuration keys to override with database values
|
||||
* if they exist.
|
||||
*/
|
||||
protected array $keys = [
|
||||
'app:name',
|
||||
'app:locale',
|
||||
'recaptcha:enabled',
|
||||
'recaptcha:secret_key',
|
||||
'recaptcha:website_key',
|
||||
'panel:guzzle:timeout',
|
||||
'panel:guzzle:connect_timeout',
|
||||
'panel:console:count',
|
||||
'panel:console:frequency',
|
||||
'panel:auth:2fa_required',
|
||||
'panel:client_features:allocations:enabled',
|
||||
'panel:client_features:allocations:range_start',
|
||||
'panel:client_features:allocations:range_end',
|
||||
];
|
||||
|
||||
/**
|
||||
* Keys specific to the mail driver that are only grabbed from the database
|
||||
* when using the SMTP driver.
|
||||
*/
|
||||
protected array $emailKeys = [
|
||||
'mail:mailers:smtp:host',
|
||||
'mail:mailers:smtp:port',
|
||||
'mail:mailers:smtp:encryption',
|
||||
'mail:mailers:smtp:username',
|
||||
'mail:mailers:smtp:password',
|
||||
'mail:from:address',
|
||||
'mail:from:name',
|
||||
];
|
||||
|
||||
/**
|
||||
* Keys that are encrypted and should be decrypted when set in the
|
||||
* configuration array.
|
||||
*/
|
||||
protected static array $encrypted = [
|
||||
'mail:mailers:smtp:password',
|
||||
];
|
||||
|
||||
/**
|
||||
* Boot the service provider.
|
||||
*/
|
||||
public function boot(Log $log): void
|
||||
{
|
||||
// Only set the email driver settings from the database if we
|
||||
// are configured using SMTP as the driver.
|
||||
if (config('mail.default') === 'smtp') {
|
||||
$this->keys = array_merge($this->keys, $this->emailKeys);
|
||||
}
|
||||
|
||||
try {
|
||||
$values = Setting::all()->mapWithKeys(function ($setting) {
|
||||
return [$setting->key => $setting->value];
|
||||
})->toArray();
|
||||
} catch (QueryException $exception) {
|
||||
$log->notice('A query exception was encountered while trying to load settings from the database: ' . $exception->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->keys as $key) {
|
||||
$value = array_get($values, 'settings::' . $key, config(str_replace(':', '.', $key)));
|
||||
if (in_array($key, self::$encrypted)) {
|
||||
try {
|
||||
$value = decrypt($value);
|
||||
} catch (Exception) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
switch (strtolower($value)) {
|
||||
case 'true':
|
||||
case '(true)':
|
||||
$value = true;
|
||||
break;
|
||||
case 'false':
|
||||
case '(false)':
|
||||
$value = false;
|
||||
break;
|
||||
case 'empty':
|
||||
case '(empty)':
|
||||
$value = '';
|
||||
break;
|
||||
case 'null':
|
||||
case '(null)':
|
||||
$value = null;
|
||||
}
|
||||
|
||||
config()->set(str_replace(':', '.', $key), $value);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getEncryptedKeys(): array
|
||||
{
|
||||
return self::$encrypted;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Services\Allocations;
|
||||
use App\Models\Allocation;
|
||||
use IPTools\Network;
|
||||
use App\Models\Node;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use App\Exceptions\DisplayException;
|
||||
use App\Exceptions\Service\Allocation\CidrOutOfRangeException;
|
||||
@@ -14,7 +15,7 @@ use App\Exceptions\Service\Allocation\TooManyPortsInRangeException;
|
||||
|
||||
class AssignmentService
|
||||
{
|
||||
public const CIDR_MAX_BITS = 27;
|
||||
public const CIDR_MAX_BITS = 25;
|
||||
public const CIDR_MIN_BITS = 32;
|
||||
public const PORT_FLOOR = 1024;
|
||||
public const PORT_CEIL = 65535;
|
||||
@@ -37,7 +38,7 @@ class AssignmentService
|
||||
* @throws \App\Exceptions\Service\Allocation\PortOutOfRangeException
|
||||
* @throws \App\Exceptions\Service\Allocation\TooManyPortsInRangeException
|
||||
*/
|
||||
public function handle(Node $node, array $data): array
|
||||
public function handle(Node $node, array $data, Server $server = null): array
|
||||
{
|
||||
$explode = explode('/', $data['allocation_ip']);
|
||||
if (count($explode) !== 1) {
|
||||
@@ -74,7 +75,7 @@ class AssignmentService
|
||||
throw new TooManyPortsInRangeException();
|
||||
}
|
||||
|
||||
if ((int) $matches[1] <= self::PORT_FLOOR || (int) $matches[2] > self::PORT_CEIL) {
|
||||
if ((int) $matches[1] < self::PORT_FLOOR || (int) $matches[2] > self::PORT_CEIL) {
|
||||
throw new PortOutOfRangeException();
|
||||
}
|
||||
|
||||
@@ -84,11 +85,11 @@ class AssignmentService
|
||||
'ip' => $ip->__toString(),
|
||||
'port' => (int) $unit,
|
||||
'ip_alias' => array_get($data, 'allocation_alias'),
|
||||
'server_id' => null,
|
||||
'server_id' => $server->id ?? null,
|
||||
];
|
||||
}
|
||||
} else {
|
||||
if ((int) $port <= self::PORT_FLOOR || (int) $port > self::PORT_CEIL) {
|
||||
if ((int) $port < self::PORT_FLOOR || (int) $port > self::PORT_CEIL) {
|
||||
throw new PortOutOfRangeException();
|
||||
}
|
||||
|
||||
@@ -97,7 +98,7 @@ class AssignmentService
|
||||
'ip' => $ip->__toString(),
|
||||
'port' => (int) $port,
|
||||
'ip_alias' => array_get($data, 'allocation_alias'),
|
||||
'server_id' => null,
|
||||
'server_id' => $server->id ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
41
app/Services/Files/DeleteFilesService.php
Normal file
41
app/Services/Files/DeleteFilesService.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Files;
|
||||
|
||||
use App\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonFileRepository;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class DeleteFilesService
|
||||
{
|
||||
/**
|
||||
* DeleteFilesService constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private DaemonFileRepository $daemonFileRepository
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given files.
|
||||
* @throws DaemonConnectionException
|
||||
*/
|
||||
public function handle(Server $server, array $files): void
|
||||
{
|
||||
$filesToDelete = collect();
|
||||
foreach ($files as $line) {
|
||||
$path = dirname($line);
|
||||
$pattern = basename($line);
|
||||
collect($this->daemonFileRepository->setServer($server)->getDirectory($path))->each(function ($item) use ($path, $pattern, $filesToDelete) {
|
||||
if (Str::is($pattern, $item['name'])) {
|
||||
$filesToDelete->push($path . '/' . $item['name']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ($filesToDelete->isNotEmpty()) {
|
||||
$this->daemonFileRepository->setServer($server)->deleteFiles('/', $filesToDelete->toArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ namespace App\Services\Helpers;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use Carbon\CarbonImmutable;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Contracts\Cache\Repository as CacheRepository;
|
||||
|
||||
@@ -89,18 +89,28 @@ class SoftwareVersionService
|
||||
$versionData = [];
|
||||
|
||||
try {
|
||||
$response = $this->client->request('GET', 'https://api.github.com/repos/pelican-dev/panel/releases/latest');
|
||||
$response = $this->client->request('GET', 'https://api.github.com/repos/pelican-dev/panel/releases/latest',
|
||||
[
|
||||
'timeout' => config('panel.guzzle.timeout'),
|
||||
'connect_timeout' => config('panel.guzzle.connect_timeout'),
|
||||
]
|
||||
);
|
||||
if ($response->getStatusCode() === 200) {
|
||||
$panelData = json_decode($response->getBody(), true);
|
||||
$versionData['panel'] = trim($panelData['tag_name'], 'v');
|
||||
}
|
||||
|
||||
$response = $this->client->request('GET', 'https://api.github.com/repos/pelican-dev/wings/releases/latest');
|
||||
$response = $this->client->request('GET', 'https://api.github.com/repos/pelican-dev/wings/releases/latest',
|
||||
[
|
||||
'timeout' => config('panel.guzzle.timeout'),
|
||||
'connect_timeout' => config('panel.guzzle.connect_timeout'),
|
||||
]
|
||||
);
|
||||
if ($response->getStatusCode() === 200) {
|
||||
$wingsData = json_decode($response->getBody(), true);
|
||||
$versionData['daemon'] = trim($wingsData['tag_name'], 'v');
|
||||
}
|
||||
} catch (ClientException $e) {
|
||||
} catch (GuzzleException $e) {
|
||||
}
|
||||
|
||||
$versionData['discord'] = 'https://pelican.dev/discord';
|
||||
|
||||
37
app/Traits/Commands/RequestRedisSettingsTrait.php
Normal file
37
app/Traits/Commands/RequestRedisSettingsTrait.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits\Commands;
|
||||
|
||||
trait RequestRedisSettingsTrait
|
||||
{
|
||||
protected function requestRedisSettings(): void
|
||||
{
|
||||
$this->output->note(__('commands.appsettings.redis.note'));
|
||||
$this->variables['REDIS_HOST'] = $this->option('redis-host') ?? $this->ask(
|
||||
'Redis Host',
|
||||
config('database.redis.default.host')
|
||||
);
|
||||
|
||||
$askForRedisPassword = true;
|
||||
if (!empty(config('database.redis.default.password'))) {
|
||||
$this->variables['REDIS_PASSWORD'] = config('database.redis.default.password');
|
||||
$askForRedisPassword = $this->confirm('It seems a password is already defined for Redis, would you like to change it?');
|
||||
}
|
||||
|
||||
if ($askForRedisPassword) {
|
||||
$this->output->comment(__('commands.appsettings.redis.comment'));
|
||||
$this->variables['REDIS_PASSWORD'] = $this->option('redis-pass') ?? $this->output->askHidden(
|
||||
'Redis Password'
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->variables['REDIS_PASSWORD'])) {
|
||||
$this->variables['REDIS_PASSWORD'] = 'null';
|
||||
}
|
||||
|
||||
$this->variables['REDIS_PORT'] = $this->option('redis-port') ?? $this->ask(
|
||||
'Redis Port',
|
||||
config('database.redis.default.port')
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ class ActivityLogTransformer extends BaseClientTransformer
|
||||
|
||||
$properties = $model->properties
|
||||
->mapWithKeys(function ($value, $key) use ($model) {
|
||||
if ($key === 'ip' && !$model->actor->is($this->request->user())) {
|
||||
if ($key === 'ip' && $model->actor instanceof User && !$model->actor->is($this->request->user())) {
|
||||
return [$key => '[hidden]'];
|
||||
}
|
||||
|
||||
|
||||
@@ -40,3 +40,11 @@ if (!function_exists('object_get_strict')) {
|
||||
return $object;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('is_installed')) {
|
||||
function is_installed(): bool
|
||||
{
|
||||
// This defaults to true so existing panels count as "installed"
|
||||
return env('APP_INSTALLED', true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,5 @@ return [
|
||||
App\Providers\Filament\AdminPanelProvider::class,
|
||||
App\Providers\RouteServiceProvider::class,
|
||||
App\Providers\ViewComposerServiceProvider::class,
|
||||
|
||||
SocialiteProviders\Manager\ServiceProvider::class,
|
||||
];
|
||||
|
||||
@@ -5,8 +5,9 @@ use Illuminate\Support\Facades\Facade;
|
||||
return [
|
||||
|
||||
'name' => env('APP_NAME', 'Pelican'),
|
||||
'favicon' => env('APP_FAVICON', './pelican.ico'),
|
||||
|
||||
'version' => '1.0.0-beta6',
|
||||
'version' => 'canary',
|
||||
|
||||
'exceptions' => [
|
||||
'report_all' => env('APP_REPORT_ALL_EXCEPTIONS', false),
|
||||
|
||||
@@ -1,18 +1,6 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Restricted Environment
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Set this environment variable to true to enable a restricted configuration
|
||||
| setup on the panel. When set to true, configurations stored in the
|
||||
| database will not be applied.
|
||||
*/
|
||||
|
||||
'load_environment_only' => (bool) env('APP_ENVIRONMENT_ONLY', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication
|
||||
|
||||
54
contributing.md
Normal file
54
contributing.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Contributing
|
||||
|
||||
Welcome to the Pelican project! We are excited to have you contribute to our open-source project. This guide will help you get started with setting up your development environment, understanding our coding standards, and making your first or next contribution.
|
||||
|
||||
## Getting started
|
||||
|
||||
To start contributing to Pelican Panel, you need to have a basic understanding of the following:
|
||||
|
||||
* [PHP](https://php.net) & [Laravel](https://laravel.com)
|
||||
* [Livewire](https://laravel-livewire.com) & [Filament](https://filamentphp.com)
|
||||
* [Git](https://git-scm.com) & [Github](https://github.com)
|
||||
|
||||
## Dev Environment Setup
|
||||
|
||||
1. Fork the Repository
|
||||
2. Clone your Fork
|
||||
3. Install Dependencies (PHP modules & composer, and run `composer install`)
|
||||
4. Configure your Environment (via `php artisan p:environment:setup`)
|
||||
5. Set up your Database (via `php artisan p:environment:database`) and run Migrations (via `php artisan migrate --seed --force`)
|
||||
6. Create your first Admin User (via `php artisan p:user:make`)
|
||||
7. Start your Webserver (e.g. Nginx or Apache)
|
||||
|
||||
As IDE we recommend [Visual Studio](https://visualstudio.microsoft.com)/ [Visual Studio Code](https://code.visualstudio.com) (free) or [PhpStorm](https://www.jetbrains.com/phpstorm) (paid).
|
||||
|
||||
To easily install PHP and the Webserver we recommend Laravel Herd. ([Windows](https://herd.laravel.com/windows) & [macOS](https://herd.laravel.com))
|
||||
The (paid) Pro version of Laravel Herd also offers easy MySQL and Redis hosting, but it is not needed.
|
||||
|
||||
## Coding Standards
|
||||
|
||||
We use PHPStan/ [Larastan](https://github.com/larastan/larastan) and PHP-CS-Fixer/ [Pint](https://laravel.com/docs/11.x/pint) to enforce certain code styles and standards.
|
||||
You can run PHPStan via `\vendor\bin\phpstan analyse` and Pint via `\vendor\bin\pint`.
|
||||
|
||||
## Making Contributions
|
||||
|
||||
From your forked repository, make your own changes on your own branch. (do not make changes directly to `main`!)
|
||||
When you are ready, you can submit a pull request to the Pelican repository. If you still work on your pull request or need help with something make sure to mark it as Draft.
|
||||
|
||||
Also, please make sure that your pull requests are as targeted and simple as possible and don't do a hundred things at a time. If you want to add/ change/ fix 5 different things you should make 5 different pull requests.
|
||||
|
||||
*Note: For now we only accept pull requests that handle existing issues!*
|
||||
|
||||
## Code Review Process
|
||||
|
||||
Your pull request will then be reviewed by the maintainers.
|
||||
Once you have an approval from a maintainer, another will merge it once it’s confirmed.
|
||||
|
||||
Depending on the pull request size this process can take multiple days.
|
||||
|
||||
## Community and Support
|
||||
|
||||
* Help: [Discord](https://discord.gg/pelican-panel)
|
||||
* Bugs: [GitHub Issues](https://github.com/pelican-dev/panel/issues)
|
||||
* Features: [GitHub Discussions](https://github.com/pelican-dev/panel/discussions)
|
||||
* Security vulnerabilities: See our [security policy](./security.md).
|
||||
1
database/.gitignore
vendored
1
database/.gitignore
vendored
@@ -1 +1,2 @@
|
||||
*.sqlite
|
||||
*.sqlite.backup
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
"_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL",
|
||||
"meta": {
|
||||
"version": "PTDL_v2",
|
||||
"update_url": null
|
||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-bungeecord.json"
|
||||
},
|
||||
"exported_at": "2024-06-04T22:51:49+00:00",
|
||||
"exported_at": "2024-07-03T14:33:51+00:00",
|
||||
"name": "Bungeecord",
|
||||
"author": "panel@example.com",
|
||||
"uuid": "9e6b409e-4028-4947-aea8-50a2c404c271",
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
"_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL",
|
||||
"meta": {
|
||||
"version": "PTDL_v2",
|
||||
"update_url": null
|
||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-forge-minecraft.json"
|
||||
},
|
||||
"exported_at": "2024-06-04T22:51:58+00:00",
|
||||
"exported_at": "2024-07-03T14:33:51+00:00",
|
||||
"name": "Forge Minecraft",
|
||||
"author": "panel@example.com",
|
||||
"uuid": "ed072427-f209-4603-875c-f540c6dd5a65",
|
||||
@@ -45,7 +45,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/",
|
||||
"sort": null,
|
||||
"sort": 1,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -56,7 +56,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|max:9",
|
||||
"sort": null,
|
||||
"sort": 2,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -67,7 +67,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|in:recommended,latest",
|
||||
"sort": null,
|
||||
"sort": 3,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -78,7 +78,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|regex:\/^[0-9\\.\\-]+$\/",
|
||||
"sort": null,
|
||||
"sort": 4,
|
||||
"field_type": "text"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
"_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL",
|
||||
"meta": {
|
||||
"version": "PTDL_v2",
|
||||
"update_url": null
|
||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-paper.json"
|
||||
},
|
||||
"exported_at": "2024-06-04T22:51:57+00:00",
|
||||
"exported_at": "2024-07-03T14:33:52+00:00",
|
||||
"name": "Paper",
|
||||
"author": "parker@example.com",
|
||||
"uuid": "5da37ef6-58da-4169-90a6-e683e1721247",
|
||||
@@ -45,7 +45,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|string|max:20",
|
||||
"sort": null,
|
||||
"sort": 1,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -56,7 +56,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/",
|
||||
"sort": null,
|
||||
"sort": 2,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -67,7 +67,7 @@
|
||||
"user_viewable": false,
|
||||
"user_editable": false,
|
||||
"rules": "nullable|string",
|
||||
"sort": null,
|
||||
"sort": 3,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -78,7 +78,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|max:20",
|
||||
"sort": null,
|
||||
"sort": 4,
|
||||
"field_type": "text"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
"_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL",
|
||||
"meta": {
|
||||
"version": "PTDL_v2",
|
||||
"update_url": null
|
||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-sponge--sponge-vanilla.json"
|
||||
},
|
||||
"exported_at": "2024-06-04T22:50:55+00:00",
|
||||
"exported_at": "2024-07-03T14:34:02+00:00",
|
||||
"name": "Sponge (SpongeVanilla)",
|
||||
"author": "panel@example.com",
|
||||
"uuid": "f0d2f88f-1ff3-42a0-b03f-ac44c5571e6d",
|
||||
@@ -45,7 +45,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|regex:\/^([a-zA-Z0-9.\\-_]+)$\/",
|
||||
"sort": null,
|
||||
"sort": 1,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -56,8 +56,8 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/",
|
||||
"sort": null,
|
||||
"sort": 2,
|
||||
"field_type": "text"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
"_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL",
|
||||
"meta": {
|
||||
"version": "PTDL_v2",
|
||||
"update_url": null
|
||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-vanilla-minecraft.json"
|
||||
},
|
||||
"exported_at": "2024-06-04T22:51:16+00:00",
|
||||
"exported_at": "2024-07-03T14:34:02+00:00",
|
||||
"name": "Vanilla Minecraft",
|
||||
"author": "panel@example.com",
|
||||
"uuid": "9ac39f3d-0c34-4d93-8174-c52ab9e6c57b",
|
||||
@@ -45,7 +45,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/",
|
||||
"sort": null,
|
||||
"sort": 1,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -56,7 +56,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|between:3,15",
|
||||
"sort": null,
|
||||
"sort": 2,
|
||||
"field_type": "text"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
"_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL",
|
||||
"meta": {
|
||||
"version": "PTDL_v2",
|
||||
"update_url": null
|
||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/rust\/egg-rust.json"
|
||||
},
|
||||
"exported_at": "2024-06-02T20:42:09+00:00",
|
||||
"exported_at": "2024-07-03T14:34:09+00:00",
|
||||
"name": "Rust",
|
||||
"author": "panel@example.com",
|
||||
"uuid": "bace2dfb-209c-452a-9459-7d6f340b07ae",
|
||||
@@ -39,7 +39,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|max:60",
|
||||
"sort": null,
|
||||
"sort": 1,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -50,7 +50,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|in:vanilla,oxide,carbon",
|
||||
"sort": null,
|
||||
"sort": 2,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -61,7 +61,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|max:20",
|
||||
"sort": null,
|
||||
"sort": 3,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -72,7 +72,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string",
|
||||
"sort": null,
|
||||
"sort": 4,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -83,7 +83,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|url",
|
||||
"sort": null,
|
||||
"sort": 5,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -94,7 +94,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|integer",
|
||||
"sort": null,
|
||||
"sort": 6,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -105,7 +105,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|string",
|
||||
"sort": null,
|
||||
"sort": 7,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -116,7 +116,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|integer",
|
||||
"sort": null,
|
||||
"sort": 8,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -127,7 +127,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|url",
|
||||
"sort": null,
|
||||
"sort": 9,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -138,7 +138,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|integer",
|
||||
"sort": null,
|
||||
"sort": 10,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -149,7 +149,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|integer",
|
||||
"sort": null,
|
||||
"sort": 11,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -160,7 +160,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|regex:\/^[\\w.-]*$\/|max:64",
|
||||
"sort": null,
|
||||
"sort": 12,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -171,7 +171,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|integer",
|
||||
"sort": null,
|
||||
"sort": 13,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -182,7 +182,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|string",
|
||||
"sort": null,
|
||||
"sort": 14,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -193,7 +193,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|integer",
|
||||
"sort": null,
|
||||
"sort": 15,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -204,7 +204,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|url",
|
||||
"sort": null,
|
||||
"sort": 16,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -215,7 +215,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|url",
|
||||
"sort": null,
|
||||
"sort": 17,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -226,7 +226,7 @@
|
||||
"user_viewable": false,
|
||||
"user_editable": false,
|
||||
"rules": "required|string|in:258550",
|
||||
"sort": null,
|
||||
"sort": 18,
|
||||
"field_type": "text"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
"_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL",
|
||||
"meta": {
|
||||
"version": "PTDL_v2",
|
||||
"update_url": null
|
||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/source-engine\/egg-counter--strike--global-offensive.json"
|
||||
},
|
||||
"exported_at": "2024-06-02T20:42:04+00:00",
|
||||
"exported_at": "2024-07-03T14:34:03+00:00",
|
||||
"name": "Counter-Strike: Global Offensive",
|
||||
"author": "panel@example.com",
|
||||
"uuid": "437c367d-06be-498f-a604-fdad135504d7",
|
||||
@@ -40,7 +40,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|alpha_dash",
|
||||
"sort": null,
|
||||
"sort": 1,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -51,7 +51,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|alpha_num|size:32",
|
||||
"sort": null,
|
||||
"sort": 2,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -62,7 +62,7 @@
|
||||
"user_viewable": false,
|
||||
"user_editable": false,
|
||||
"rules": "required|string|max:20",
|
||||
"sort": null,
|
||||
"sort": 3,
|
||||
"field_type": "text"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
"_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL",
|
||||
"meta": {
|
||||
"version": "PTDL_v2",
|
||||
"update_url": null
|
||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/source-engine\/egg-custom-source-engine-game.json"
|
||||
},
|
||||
"exported_at": "2024-06-02T20:42:04+00:00",
|
||||
"exported_at": "2024-07-03T14:34:04+00:00",
|
||||
"name": "Custom Source Engine Game",
|
||||
"author": "panel@example.com",
|
||||
"uuid": "2a42d0c2-c0ba-4067-9a0a-9b95d77a3490",
|
||||
@@ -39,7 +39,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|numeric|digits_between:1,6",
|
||||
"sort": null,
|
||||
"sort": 1,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -50,7 +50,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|alpha_dash|between:1,100",
|
||||
"sort": null,
|
||||
"sort": 2,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -61,7 +61,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|alpha_dash",
|
||||
"sort": null,
|
||||
"sort": 3,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -72,7 +72,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|string",
|
||||
"sort": null,
|
||||
"sort": 4,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -83,7 +83,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|string",
|
||||
"sort": null,
|
||||
"sort": 5,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -94,7 +94,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|string",
|
||||
"sort": null,
|
||||
"sort": 6,
|
||||
"field_type": "text"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
"_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL",
|
||||
"meta": {
|
||||
"version": "PTDL_v2",
|
||||
"update_url": null
|
||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/source-engine\/egg-garrys-mod.json"
|
||||
},
|
||||
"exported_at": "2024-06-02T20:42:05+00:00",
|
||||
"exported_at": "2024-07-03T14:34:04+00:00",
|
||||
"name": "Garrys Mod",
|
||||
"author": "panel@example.com",
|
||||
"uuid": "60ef81d4-30a2-4d98-ab64-f59c69e2f915",
|
||||
@@ -40,7 +40,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|alpha_dash",
|
||||
"sort": null,
|
||||
"sort": 1,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -51,7 +51,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|string|alpha_num|size:32",
|
||||
"sort": null,
|
||||
"sort": 2,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -62,7 +62,7 @@
|
||||
"user_viewable": false,
|
||||
"user_editable": false,
|
||||
"rules": "required|string|max:20",
|
||||
"sort": null,
|
||||
"sort": 3,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -73,7 +73,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|integer",
|
||||
"sort": null,
|
||||
"sort": 4,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -84,7 +84,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string",
|
||||
"sort": null,
|
||||
"sort": 5,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -95,7 +95,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|integer|max:128",
|
||||
"sort": null,
|
||||
"sort": 6,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -106,7 +106,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|integer|max:100",
|
||||
"sort": null,
|
||||
"sort": 7,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -117,7 +117,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|boolean",
|
||||
"sort": null,
|
||||
"sort": 8,
|
||||
"field_type": "text"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
"_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL",
|
||||
"meta": {
|
||||
"version": "PTDL_v2",
|
||||
"update_url": null
|
||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/source-engine\/egg-insurgency.json"
|
||||
},
|
||||
"exported_at": "2024-06-02T20:42:06+00:00",
|
||||
"exported_at": "2024-07-03T14:34:05+00:00",
|
||||
"name": "Insurgency",
|
||||
"author": "panel@example.com",
|
||||
"uuid": "a5702286-655b-4069-bf1e-925c7300b61a",
|
||||
@@ -39,7 +39,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|regex:\/^(237410)$\/",
|
||||
"sort": null,
|
||||
"sort": 1,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -50,7 +50,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|regex:\/^(\\w{1,20})$\/",
|
||||
"sort": null,
|
||||
"sort": 2,
|
||||
"field_type": "text"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
"_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL",
|
||||
"meta": {
|
||||
"version": "PTDL_v2",
|
||||
"update_url": null
|
||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/source-engine\/egg-team-fortress2.json"
|
||||
},
|
||||
"exported_at": "2024-06-02T20:42:07+00:00",
|
||||
"exported_at": "2024-07-03T14:34:06+00:00",
|
||||
"name": "Team Fortress 2",
|
||||
"author": "panel@example.com",
|
||||
"uuid": "7f8eb681-b2c8-4bf8-b9f4-d79ff70b6e5d",
|
||||
@@ -40,7 +40,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|in:232250",
|
||||
"sort": null,
|
||||
"sort": 1,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -51,7 +51,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|regex:\/^(\\w{1,20})$\/",
|
||||
"sort": null,
|
||||
"sort": 2,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -62,7 +62,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|alpha_num|size:32",
|
||||
"sort": null,
|
||||
"sort": 3,
|
||||
"field_type": "text"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
"_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL",
|
||||
"meta": {
|
||||
"version": "PTDL_v2",
|
||||
"update_url": null
|
||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/voice-servers\/egg-mumble-server.json"
|
||||
},
|
||||
"exported_at": "2024-06-04T22:53:03+00:00",
|
||||
"exported_at": "2024-07-03T14:34:07+00:00",
|
||||
"name": "Mumble Server",
|
||||
"author": "panel@example.com",
|
||||
"uuid": "727ee758-7fb2-4979-972b-d3eba4e1e9f0",
|
||||
"description": "Mumble is an open source, low-latency, high quality voice chat software primarily intended for use while gaming.",
|
||||
"features": null,
|
||||
"features": [],
|
||||
"docker_images": {
|
||||
"Mumble": "ghcr.io\/parkervcp\/yolks:voice_mumble"
|
||||
},
|
||||
@@ -37,7 +37,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|numeric|digits_between:1,5",
|
||||
"sort": null,
|
||||
"sort": 1,
|
||||
"field_type": "text"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
"_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL",
|
||||
"meta": {
|
||||
"version": "PTDL_v2",
|
||||
"update_url": null
|
||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/voice-servers\/egg-teamspeak3-server.json"
|
||||
},
|
||||
"exported_at": "2024-06-02T20:42:08+00:00",
|
||||
"exported_at": "2024-07-03T14:34:08+00:00",
|
||||
"name": "Teamspeak3 Server",
|
||||
"author": "panel@example.com",
|
||||
"uuid": "983b1fac-d322-4d5f-a636-436127326b37",
|
||||
"description": "VoIP software designed with security in mind, featuring crystal clear voice quality, endless customization options, and scalabilty up to thousands of simultaneous users.",
|
||||
"features": null,
|
||||
"features": [],
|
||||
"docker_images": {
|
||||
"Debian": "ghcr.io\/parkervcp\/yolks:debian"
|
||||
},
|
||||
@@ -37,7 +37,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|max:6",
|
||||
"sort": null,
|
||||
"sort": 1,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -48,7 +48,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|integer|between:1025,65535",
|
||||
"sort": null,
|
||||
"sort": 2,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -59,7 +59,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|integer|between:1025,65535",
|
||||
"sort": null,
|
||||
"sort": 3,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -70,7 +70,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|max:12",
|
||||
"sort": null,
|
||||
"sort": 4,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -81,7 +81,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|integer|between:1025,65535",
|
||||
"sort": null,
|
||||
"sort": 5,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -92,7 +92,7 @@
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|integer|between:1025,65535",
|
||||
"sort": null,
|
||||
"sort": 6,
|
||||
"field_type": "text"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('activity_logs', function (Blueprint $table) {
|
||||
$table->timestamp('timestamp')->default(null)->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('activity_logs', function (Blueprint $table) {
|
||||
$table->timestamp('timestamp')->useCurrent()->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,284 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// Only needed for sqlite
|
||||
if (Schema::getConnection()->getDriverName() !== 'sqlite') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable foreign checks
|
||||
// legacy_alter_table needs to be 'ON' so existing foreign key table references aren't renamed when renaming the table, see https://www.sqlite.org/lang_altertable.html
|
||||
DB::statement('PRAGMA foreign_keys = OFF');
|
||||
DB::statement('PRAGMA legacy_alter_table = ON');
|
||||
|
||||
DB::transaction(function () {
|
||||
// api_keys_user_id_foreign
|
||||
DB::statement('ALTER TABLE api_keys RENAME TO _api_keys_old');
|
||||
DB::statement('CREATE TABLE api_keys
|
||||
("id" integer primary key autoincrement not null,
|
||||
"token" text not null,
|
||||
"allowed_ips" text not null,
|
||||
"created_at" datetime,
|
||||
"updated_at" datetime,
|
||||
"user_id" integer not null,
|
||||
"memo" text,
|
||||
"r_servers" integer not null default \'0\',
|
||||
"r_nodes" integer not null default \'0\',
|
||||
"r_allocations" integer not null default \'0\',
|
||||
"r_users" integer not null default \'0\',
|
||||
"r_eggs" integer not null default \'0\',
|
||||
"r_database_hosts" integer not null default \'0\',
|
||||
"r_server_databases" integer not null default \'0\',
|
||||
"identifier" varchar,
|
||||
"key_type" integer not null default \'0\',
|
||||
"last_used_at" datetime,
|
||||
"expires_at" datetime,
|
||||
"r_mounts" integer not null default \'0\',
|
||||
foreign key("user_id") references "users"("id") on delete cascade)');
|
||||
DB::statement('INSERT INTO api_keys SELECT * FROM _api_keys_old');
|
||||
DB::statement('DROP TABLE _api_keys_old');
|
||||
DB::statement('CREATE UNIQUE INDEX "api_keys_identifier_unique" on "api_keys" ("identifier")');
|
||||
|
||||
// database_hosts_node_id_foreign
|
||||
DB::statement('ALTER TABLE database_hosts RENAME TO _database_hosts_old');
|
||||
DB::statement('CREATE TABLE database_hosts
|
||||
("id" integer primary key autoincrement not null,
|
||||
"name" varchar not null,
|
||||
"host" varchar not null,
|
||||
"port" integer not null,
|
||||
"username" varchar not null,
|
||||
"password" text not null,
|
||||
"max_databases" integer,
|
||||
"node_id" integer,
|
||||
"created_at" datetime,
|
||||
"updated_at" datetime,
|
||||
foreign key("node_id") references "nodes"("id") on delete set null)');
|
||||
DB::statement('INSERT INTO database_hosts SELECT * FROM _database_hosts_old');
|
||||
DB::statement('DROP TABLE _database_hosts_old');
|
||||
|
||||
// mount_node_node_id_foreign
|
||||
// mount_node_mount_id_foreign
|
||||
DB::statement('ALTER TABLE mount_node RENAME TO _mount_node_old');
|
||||
DB::statement('CREATE TABLE mount_node
|
||||
("node_id" integer not null,
|
||||
"mount_id" integer not null,
|
||||
foreign key("node_id") references "nodes"("id") on delete cascade on update cascade,
|
||||
foreign key("mount_id") references "mounts"("id") on delete cascade on update cascade)');
|
||||
DB::statement('INSERT INTO mount_node SELECT * FROM _mount_node_old');
|
||||
DB::statement('DROP TABLE _mount_node_old');
|
||||
DB::statement('CREATE UNIQUE INDEX "mount_node_node_id_mount_id_unique" on "mount_node" ("node_id", "mount_id")');
|
||||
|
||||
// servers_node_id_foreign
|
||||
// servers_owner_id_foreign
|
||||
// servers_egg_id_foreign
|
||||
// servers_allocation_id_foreign
|
||||
DB::statement('ALTER TABLE servers RENAME TO _servers_old');
|
||||
DB::statement('CREATE TABLE servers
|
||||
("id" integer primary key autoincrement not null,
|
||||
"uuid" varchar not null,
|
||||
"uuid_short" varchar not null,
|
||||
"node_id" integer not null,
|
||||
"name" varchar not null,
|
||||
"owner_id" integer not null,
|
||||
"memory" integer not null,
|
||||
"swap" integer not null,
|
||||
"disk" integer not null,
|
||||
"io" integer not null,
|
||||
"cpu" integer not null,
|
||||
"egg_id" integer not null,
|
||||
"startup" text not null,
|
||||
"created_at" datetime,
|
||||
"updated_at" datetime,
|
||||
"allocation_id" integer not null,
|
||||
"image" varchar not null,
|
||||
"description" text not null,
|
||||
"skip_scripts" tinyint(1) not null default \'0\',
|
||||
"external_id" varchar,
|
||||
"database_limit" integer default \'0\',
|
||||
"allocation_limit" integer,
|
||||
"threads" varchar,
|
||||
"backup_limit" integer not null default \'0\',
|
||||
"status" varchar,
|
||||
"installed_at" datetime,
|
||||
"oom_killer" integer not null default \'0\',
|
||||
"docker_labels" text,
|
||||
foreign key("node_id") references "nodes"("id"),
|
||||
foreign key("owner_id") references "users"("id"),
|
||||
foreign key("egg_id") references "eggs"("id"),
|
||||
foreign key("allocation_id") references "allocations"("id"))');
|
||||
DB::statement('INSERT INTO servers SELECT * FROM _servers_old');
|
||||
DB::statement('DROP TABLE _servers_old');
|
||||
DB::statement('CREATE UNIQUE INDEX "servers_allocation_id_unique" on "servers" ("allocation_id")');
|
||||
DB::statement('CREATE UNIQUE INDEX "servers_external_id_unique" on "servers" ("external_id")');
|
||||
DB::statement('CREATE UNIQUE INDEX "servers_uuid_unique" on "servers" ("uuid")');
|
||||
DB::statement('CREATE UNIQUE INDEX "servers_uuidshort_unique" on "servers" ("uuid_short")');
|
||||
|
||||
// databases_server_id_foreign
|
||||
// databases_database_host_id_foreign
|
||||
DB::statement('ALTER TABLE databases RENAME TO _databases_old');
|
||||
DB::statement('CREATE TABLE databases
|
||||
("id" integer primary key autoincrement not null,
|
||||
"server_id" integer not null,
|
||||
"database_host_id" integer not null,
|
||||
"database" varchar not null,
|
||||
"username" varchar not null,
|
||||
"remote" varchar not null default \'%\',
|
||||
"password" text not null,
|
||||
"created_at" datetime,
|
||||
"updated_at" datetime,
|
||||
"max_connections" integer default \'0\',
|
||||
foreign key("server_id") references "servers"("id"),
|
||||
foreign key("database_host_id") references "database_hosts"("id"))');
|
||||
DB::statement('INSERT INTO databases SELECT * FROM _databases_old');
|
||||
DB::statement('DROP TABLE _databases_old');
|
||||
DB::statement('CREATE UNIQUE INDEX "databases_database_host_id_server_id_database_unique" on "databases" ("database_host_id", "server_id", "database")');
|
||||
DB::statement('CREATE UNIQUE INDEX "databases_database_host_id_username_unique" on "databases" ("database_host_id", "username")');
|
||||
|
||||
// allocations_node_id_foreign
|
||||
// allocations_server_id_foreign
|
||||
DB::statement('ALTER TABLE allocations RENAME TO _allocations_old');
|
||||
DB::statement('CREATE TABLE allocations
|
||||
("id" integer primary key autoincrement not null,
|
||||
"node_id" integer not null,
|
||||
"ip" varchar not null,
|
||||
"port" integer not null,
|
||||
"server_id" integer,
|
||||
"created_at" datetime,
|
||||
"updated_at" datetime,
|
||||
"ip_alias" text,
|
||||
"notes" varchar,
|
||||
foreign key("node_id") references "nodes"("id") on delete cascade,
|
||||
foreign key("server_id") references "servers"("id") on delete cascade on update set null)');
|
||||
DB::statement('INSERT INTO allocations SELECT * FROM _allocations_old');
|
||||
DB::statement('DROP TABLE _allocations_old');
|
||||
DB::statement('CREATE UNIQUE INDEX "allocations_node_id_ip_port_unique" on "allocations" ("node_id", "ip", "port")');
|
||||
|
||||
// eggs_config_from_foreign
|
||||
// eggs_copy_script_from_foreign
|
||||
DB::statement('ALTER TABLE eggs RENAME TO _eggs_old');
|
||||
DB::statement('CREATE TABLE eggs
|
||||
("id" integer primary key autoincrement not null,
|
||||
"name" varchar not null,
|
||||
"description" text,
|
||||
"created_at" datetime,
|
||||
"updated_at" datetime,
|
||||
"startup" text,
|
||||
"config_from" integer,
|
||||
"config_stop" varchar,
|
||||
"config_logs" text,
|
||||
"config_startup" text,
|
||||
"config_files" text,
|
||||
"script_install" text,
|
||||
"script_is_privileged" tinyint(1) not null default \'1\',
|
||||
"script_entry" varchar not null default \'ash\',
|
||||
"script_container" varchar not null default \'alpine:3.4\',
|
||||
"copy_script_from" integer,
|
||||
"uuid" varchar not null,
|
||||
"author" varchar not null,
|
||||
"features" text,
|
||||
"docker_images" text,
|
||||
"update_url" text,
|
||||
"file_denylist" text,
|
||||
"force_outgoing_ip" tinyint(1) not null default \'0\',
|
||||
"tags" text not null,
|
||||
foreign key("config_from") references "eggs"("id") on delete set null,
|
||||
foreign key("copy_script_from") references "eggs"("id") on delete set null)');
|
||||
DB::statement('INSERT INTO eggs SELECT * FROM _eggs_old');
|
||||
DB::statement('DROP TABLE _eggs_old');
|
||||
DB::statement('CREATE UNIQUE INDEX "service_options_uuid_unique" on "eggs" ("uuid")');
|
||||
|
||||
// egg_mount_mount_id_foreign
|
||||
// egg_mount_egg_id_foreign
|
||||
DB::statement('ALTER TABLE egg_mount RENAME TO _egg_mount_old');
|
||||
DB::statement('CREATE TABLE egg_mount
|
||||
("egg_id" integer not null,
|
||||
"mount_id" integer not null,
|
||||
foreign key("egg_id") references "eggs"("id") on delete cascade on update cascade,
|
||||
foreign key("mount_id") references "mounts"("id") on delete cascade on update cascade)');
|
||||
DB::statement('INSERT INTO egg_mount SELECT * FROM _egg_mount_old');
|
||||
DB::statement('DROP TABLE _egg_mount_old');
|
||||
DB::statement('CREATE UNIQUE INDEX "egg_mount_egg_id_mount_id_unique" on "egg_mount" ("egg_id", "mount_id")');
|
||||
|
||||
// service_variables_egg_id_foreign
|
||||
DB::statement('ALTER TABLE egg_variables RENAME TO _egg_variables_old');
|
||||
DB::statement('CREATE TABLE egg_variables
|
||||
("id" integer primary key autoincrement not null,
|
||||
"egg_id" integer not null,
|
||||
"name" varchar not null,
|
||||
"description" text not null,
|
||||
"env_variable" varchar not null,
|
||||
"default_value" text not null,
|
||||
"user_viewable" integer not null,
|
||||
"user_editable" integer not null,
|
||||
"rules" text not null,
|
||||
"created_at" datetime,
|
||||
"updated_at" datetime,
|
||||
"sort" integer,
|
||||
foreign key("egg_id") references "eggs"("id") on delete cascade)');
|
||||
DB::statement('INSERT INTO egg_variables SELECT * FROM _egg_variables_old');
|
||||
DB::statement('DROP TABLE _egg_variables_old');
|
||||
|
||||
// mount_server_server_id_foreign
|
||||
// mount_server_mount_id_foreign
|
||||
DB::statement('ALTER TABLE mount_server RENAME TO _mount_server_old');
|
||||
DB::statement('CREATE TABLE mount_server
|
||||
("server_id" integer not null,
|
||||
"mount_id" integer not null,
|
||||
foreign key("server_id") references "servers"("id") on delete cascade on update cascade,
|
||||
foreign key("mount_id") references "mounts"("id") on delete cascade on update cascade)');
|
||||
DB::statement('INSERT INTO mount_server SELECT * FROM _mount_server_old');
|
||||
DB::statement('DROP TABLE _mount_server_old');
|
||||
DB::statement('CREATE UNIQUE INDEX "mount_server_server_id_mount_id_unique" on "mount_server" ("server_id", "mount_id")');
|
||||
|
||||
// server_variables_variable_id_foreign
|
||||
DB::statement('ALTER TABLE server_variables RENAME TO _server_variables_old');
|
||||
DB::statement('CREATE TABLE server_variables
|
||||
("id" integer primary key autoincrement not null,
|
||||
"server_id" integer not null,
|
||||
"variable_id" integer not null,
|
||||
"variable_value" text not null,
|
||||
"created_at" datetime,
|
||||
"updated_at" datetime,
|
||||
foreign key("server_id") references "servers"("id") on delete cascade,
|
||||
foreign key("variable_id") references "egg_variables"("id") on delete cascade)');
|
||||
DB::statement('INSERT INTO server_variables SELECT * FROM _server_variables_old');
|
||||
DB::statement('DROP TABLE _server_variables_old');
|
||||
|
||||
// subusers_user_id_foreign
|
||||
// subusers_server_id_foreign
|
||||
DB::statement('ALTER TABLE subusers RENAME TO _subusers_old');
|
||||
DB::statement('CREATE TABLE subusers
|
||||
("id" integer primary key autoincrement not null,
|
||||
"user_id" integer not null,
|
||||
"server_id" integer not null,
|
||||
"created_at" datetime,
|
||||
"updated_at" datetime,
|
||||
"permissions" text,
|
||||
foreign key("user_id") references "users"("id") on delete cascade,
|
||||
foreign key("server_id") references "servers"("id") on delete cascade)');
|
||||
DB::statement('INSERT INTO subusers SELECT * FROM _subusers_old');
|
||||
DB::statement('DROP TABLE _subusers_old');
|
||||
});
|
||||
|
||||
DB::statement('PRAGMA foreign_keys = ON');
|
||||
DB::statement('PRAGMA legacy_alter_table = OFF');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// Reverse not needed
|
||||
}
|
||||
};
|
||||
@@ -6,7 +6,6 @@ return [
|
||||
'author' => 'Provide the email address that eggs exported by this Panel should be from. This should be a valid email address.',
|
||||
'url' => 'The application URL MUST begin with https:// or http:// depending on if you are using SSL or not. If you do not include the scheme your emails and other content will link to the wrong location.',
|
||||
'timezone' => "The timezone should match one of PHP\'s supported timezones. If you are unsure, please reference https://php.net/manual/en/timezones.php.",
|
||||
'settings_ui' => 'Enable UI based settings editor?',
|
||||
],
|
||||
'redis' => [
|
||||
'note' => 'You\'ve selected the Redis driver for one or more options, please provide valid connection information below. In most cases you can use the defaults provided unless you have modified your setup.',
|
||||
|
||||
@@ -11,7 +11,7 @@ return [
|
||||
'too_many_ports' => 'Adding more than 1000 ports in a single range at once is not supported.',
|
||||
'invalid_mapping' => 'The mapping provided for :port was invalid and could not be processed.',
|
||||
'cidr_out_of_range' => 'CIDR notation only allows masks between /25 and /32.',
|
||||
'port_out_of_range' => 'Ports in an allocation must be greater than 1024 and less than or equal to 65535.',
|
||||
'port_out_of_range' => 'Ports in an allocation must be greater than or equal to 1024 and less than or equal to 65535.',
|
||||
],
|
||||
'egg' => [
|
||||
'delete_has_servers' => 'An Egg with active servers attached to it cannot be deleted from the Panel.',
|
||||
|
||||
@@ -5,9 +5,6 @@
|
||||
&color=rgba(255%2C%20255%2C%20255%2C%201))
|
||||
&color=rgba(255%2C%20255%2C%20255%2C%201))
|
||||
|
||||
|
||||
<a href="https://polar.sh/pelican-dev"><picture><source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/embed/subscribe.svg?org=pelican-dev&label=Subscribe&darkmode"><img alt="Subscribe on Polar" src="https://polar.sh/embed/subscribe.svg?org=pelican-dev&label=Subscribe"></picture></a>
|
||||
|
||||
Pelican Panel is an open-source, web-based application designed for easy management of game servers.
|
||||
It offers a user-friendly interface for deploying, configuring, and managing servers, with features like real-time resource monitoring, Docker container isolation, and extensive customization options.
|
||||
Ideal for both individual gamers and hosting companies, it simplifies server administration without requiring deep technical knowledge.
|
||||
@@ -21,7 +18,7 @@ Fly High, Game On: Pelican's pledge for unrivaled game servers.
|
||||
* [Discord](https://discord.gg/pelican-panel)
|
||||
* [Wings](https://github.com/pelican-dev/wings)
|
||||
|
||||
### Supported Games and Servers
|
||||
## Supported Games and Servers
|
||||
|
||||
Pelican supports a wide variety of games by utilizing Docker containers to isolate each instance.
|
||||
This gives you the power to run game servers without bloating machines with a host of additional dependencies.
|
||||
@@ -44,4 +41,7 @@ Some of our popular eggs include:
|
||||
| [Storage](https://github.com/pelican-eggs/storage) | S3 | SFTP Share | | |
|
||||
| [Monitoring](https://github.com/pelican-eggs/monitoring) | Prometheus | Loki | | |
|
||||
|
||||
## Repository Activity
|
||||

|
||||
|
||||
*Copyright Pelican® 2024*
|
||||
|
||||
@@ -4,7 +4,6 @@ import getTwoFactorTokenData, { TwoFactorTokenData } from '@/api/account/getTwoF
|
||||
import { useFlashKey } from '@/plugins/useFlash';
|
||||
import tw from 'twin.macro';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '@/i18n';
|
||||
import QRCode from 'qrcode.react';
|
||||
import { Button } from '@/components/elements/button/index';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
@@ -127,7 +126,7 @@ const ConfigureTwoFactorForm = ({ onTokens }: Props) => {
|
||||
};
|
||||
|
||||
export default asDialog({
|
||||
title: i18n.t('dashboard/account:two_factor.setup.title') ?? 'Enable Two-Step Verification',
|
||||
title: 'Enable Two-Step Verification',
|
||||
description:
|
||||
"Help protect your account from unauthorized access. You'll be prompted for a verification code each time you sign in.",
|
||||
})(ConfigureTwoFactorForm);
|
||||
|
||||
@@ -70,7 +70,7 @@ export default () => {
|
||||
id={'confirm_password'}
|
||||
type={'password'}
|
||||
name={'password'}
|
||||
label={t('confirm_password', { ns: 'strings' })}
|
||||
label={t('current_password', { ns: 'strings' })}
|
||||
/>
|
||||
</div>
|
||||
<div css={tw`mt-6`}>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
faPencilAlt,
|
||||
faToggleOn,
|
||||
faTrashAlt,
|
||||
faTrash,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import deleteScheduleTask from '@/api/server/schedules/deleteScheduleTask';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
@@ -35,6 +36,8 @@ const getActionDetails = (action: string): [string, any] => {
|
||||
return ['Send Power Action', faToggleOn];
|
||||
case 'backup':
|
||||
return ['Create Backup', faFileArchive];
|
||||
case 'delete_files':
|
||||
return ['Delete Files', faTrash];
|
||||
default:
|
||||
return ['Unknown Action', faCode];
|
||||
}
|
||||
@@ -94,6 +97,9 @@ export default ({ schedule, task }: Props) => {
|
||||
{task.action === 'backup' && (
|
||||
<p css={tw`text-xs uppercase text-neutral-400 mb-1`}>Ignoring files & folders:</p>
|
||||
)}
|
||||
{task.action === 'delete_files' && (
|
||||
<p css={tw`text-xs uppercase text-neutral-400 mb-1`}>Files to delete:</p>
|
||||
)}
|
||||
<div
|
||||
css={tw`font-mono bg-neutral-800 rounded py-1 px-2 text-sm w-auto inline-block whitespace-pre-wrap break-all`}
|
||||
>
|
||||
|
||||
@@ -34,7 +34,7 @@ interface Values {
|
||||
}
|
||||
|
||||
const schema = object().shape({
|
||||
action: string().required().oneOf(['command', 'power', 'backup']),
|
||||
action: string().required().oneOf(['command', 'power', 'backup', 'delete_files']),
|
||||
payload: string().when('action', {
|
||||
is: (v) => v !== 'backup',
|
||||
then: string().required('A task payload must be provided.'),
|
||||
@@ -131,6 +131,7 @@ const TaskDetailsModal = ({ schedule, task }: Props) => {
|
||||
<option value={'command'}>Send command</option>
|
||||
<option value={'power'}>Send power action</option>
|
||||
<option value={'backup'}>Create backup</option>
|
||||
<option value={'delete_files'}>Delete files</option>
|
||||
</FormikField>
|
||||
</FormikFieldWrapper>
|
||||
</div>
|
||||
@@ -166,7 +167,7 @@ const TaskDetailsModal = ({ schedule, task }: Props) => {
|
||||
</FormikField>
|
||||
</FormikFieldWrapper>
|
||||
</div>
|
||||
) : (
|
||||
) : values.action === 'backup' ? (
|
||||
<div>
|
||||
<Label>Ignored Files</Label>
|
||||
<FormikFieldWrapper
|
||||
@@ -178,6 +179,16 @@ const TaskDetailsModal = ({ schedule, task }: Props) => {
|
||||
<FormikField as={Textarea} name={'payload'} rows={6} />
|
||||
</FormikFieldWrapper>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Label>Files to Delete</Label>
|
||||
<FormikFieldWrapper
|
||||
name={'payload'}
|
||||
description={'Specify the files that will be deleted. (Whitelist)'}
|
||||
>
|
||||
<FormikField as={Textarea} name={'payload'} rows={6} />
|
||||
</FormikFieldWrapper>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div css={tw`mt-6 bg-neutral-700 border border-neutral-800 shadow-inner p-4 rounded`}>
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
@extends('layouts.admin')
|
||||
@include('partials/admin.settings.nav', ['activeTab' => 'advanced'])
|
||||
|
||||
@section('title')
|
||||
Advanced Settings
|
||||
@endsection
|
||||
|
||||
@section('content-header')
|
||||
<h1>Advanced Settings<small>Configure advanced settings for Panel.</small></h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{{ route('admin.index') }}">Admin</a></li>
|
||||
<li class="active">Settings</li>
|
||||
</ol>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
@yield('settings::nav')
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<form action="" method="POST">
|
||||
<div class="box">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">reCAPTCHA</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="form-group col-md-4">
|
||||
<label class="control-label">Status</label>
|
||||
<div>
|
||||
<select class="form-control" name="recaptcha:enabled">
|
||||
<option value="true">Enabled</option>
|
||||
<option value="false" @if(old('recaptcha:enabled', config('recaptcha.enabled')) == '0') selected @endif>Disabled</option>
|
||||
</select>
|
||||
<p class="text-muted small">If enabled, login forms and password reset forms will do a silent captcha check and display a visible captcha if needed.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label class="control-label">Site Key</label>
|
||||
<div>
|
||||
<input type="text" required class="form-control" name="recaptcha:website_key" value="{{ old('recaptcha:website_key', config('recaptcha.website_key')) }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label class="control-label">Secret Key</label>
|
||||
<div>
|
||||
<input type="text" required class="form-control" name="recaptcha:secret_key" value="{{ old('recaptcha:secret_key', config('recaptcha.secret_key')) }}">
|
||||
<p class="text-muted small">Used for communication between your site and Google. Be sure to keep it a secret.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if($showRecaptchaWarning)
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="alert alert-warning no-margin">
|
||||
You are currently using reCAPTCHA keys that were shipped with this Panel. For improved security it is recommended to <a href="https://www.google.com/recaptcha/admin">generate new invisible reCAPTCHA keys</a> that tied specifically to your website.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">HTTP Connections</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label class="control-label">Connection Timeout</label>
|
||||
<div>
|
||||
<input type="number" required class="form-control" name="panel:guzzle:connect_timeout" value="{{ old('panel:guzzle:connect_timeout', config('panel.guzzle.connect_timeout')) }}">
|
||||
<p class="text-muted small">The amount of time in seconds to wait for a connection to be opened before throwing an error.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="control-label">Request Timeout</label>
|
||||
<div>
|
||||
<input type="number" required class="form-control" name="panel:guzzle:timeout" value="{{ old('panel:guzzle:timeout', config('panel.guzzle.timeout')) }}">
|
||||
<p class="text-muted small">The amount of time in seconds to wait for a request to be completed before throwing an error.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Automatic Allocation Creation</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="form-group col-md-4">
|
||||
<label class="control-label">Status</label>
|
||||
<div>
|
||||
<select class="form-control" name="panel:client_features:allocations:enabled">
|
||||
<option value="false">Disabled</option>
|
||||
<option value="true" @if(old('panel:client_features:allocations:enabled', config('panel.client_features.allocations.enabled'))) selected @endif>Enabled</option>
|
||||
</select>
|
||||
<p class="text-muted small">If enabled users will have the option to automatically create new allocations for their server via the frontend.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label class="control-label">Starting Port</label>
|
||||
<div>
|
||||
<input type="number" class="form-control" name="panel:client_features:allocations:range_start" value="{{ old('panel:client_features:allocations:range_start', config('panel.client_features.allocations.range_start')) }}">
|
||||
<p class="text-muted small">The starting port in the range that can be automatically allocated.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label class="control-label">Ending Port</label>
|
||||
<div>
|
||||
<input type="number" class="form-control" name="panel:client_features:allocations:range_end" value="{{ old('panel:client_features:allocations:range_end', config('panel.client_features.allocations.range_end')) }}">
|
||||
<p class="text-muted small">The ending port in the range that can be automatically allocated.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box box-primary">
|
||||
<div class="box-footer">
|
||||
{{ csrf_field() }}
|
||||
<button type="submit" name="_method" value="PATCH" class="btn btn-sm btn-primary pull-right">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -1,75 +0,0 @@
|
||||
@extends('layouts.admin')
|
||||
@include('partials/admin.settings.nav', ['activeTab' => 'basic'])
|
||||
|
||||
@section('title')
|
||||
Settings
|
||||
@endsection
|
||||
|
||||
@section('content-header')
|
||||
<h1>Panel Settings<small>Configure Panel to your liking.</small></h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{{ route('admin.index') }}">Admin</a></li>
|
||||
<li class="active">Settings</li>
|
||||
</ol>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
@yield('settings::nav')
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="box">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Panel Settings</h3>
|
||||
</div>
|
||||
<form action="{{ route('admin.settings') }}" method="POST">
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="form-group col-md-4">
|
||||
<label class="control-label">Company Name</label>
|
||||
<div>
|
||||
<input type="text" class="form-control" name="app:name" value="{{ old('app:name', config('app.name')) }}" />
|
||||
<p class="text-muted"><small>This is the name that is used throughout the panel and in emails sent to clients.</small></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label class="control-label">Require 2-Factor Authentication</label>
|
||||
<div>
|
||||
<div class="btn-group" data-toggle="buttons">
|
||||
@php
|
||||
$level = old('panel:auth:2fa_required', config('panel.auth.2fa_required'));
|
||||
@endphp
|
||||
<label class="btn btn-primary @if ($level == 0) active @endif">
|
||||
<input type="radio" name="panel:auth:2fa_required" autocomplete="off" value="0" @if ($level == 0) checked @endif> Not Required
|
||||
</label>
|
||||
<label class="btn btn-primary @if ($level == 1) active @endif">
|
||||
<input type="radio" name="panel:auth:2fa_required" autocomplete="off" value="1" @if ($level == 1) checked @endif> Admin Only
|
||||
</label>
|
||||
<label class="btn btn-primary @if ($level == 2) active @endif">
|
||||
<input type="radio" name="panel:auth:2fa_required" autocomplete="off" value="2" @if ($level == 2) checked @endif> All Users
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-muted"><small>If enabled, any account falling into the selected grouping will be required to have 2-Factor authentication enabled to use the Panel.</small></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label class="control-label">Default Language</label>
|
||||
<div>
|
||||
<select name="app:locale" class="form-control">
|
||||
@foreach($languages as $key => $value)
|
||||
<option value="{{ $key }}" @if(config('app.locale') === $key) selected @endif>{{ $value }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<p class="text-muted"><small>The default language to use when rendering UI components.</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
{!! csrf_field() !!}
|
||||
<button type="submit" name="_method" value="PATCH" class="btn btn-sm btn-primary pull-right">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -1,202 +0,0 @@
|
||||
@extends('layouts.admin')
|
||||
@include('partials/admin.settings.nav', ['activeTab' => 'mail'])
|
||||
|
||||
@section('title')
|
||||
Mail Settings
|
||||
@endsection
|
||||
|
||||
@section('content-header')
|
||||
<h1>Mail Settings<small>Configure how email sending should be handled.</small></h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{{ route('admin.index') }}">Admin</a></li>
|
||||
<li class="active">Settings</li>
|
||||
</ol>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
@yield('settings::nav')
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="box">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Email Settings</h3>
|
||||
</div>
|
||||
@if($disabled)
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="alert alert-info no-margin-bottom">
|
||||
This interface is limited to instances using SMTP as the mail driver. Please either use <code>php artisan p:environment:mail</code> command to update your email settings, or set <code>MAIL_DRIVER=smtp</code> in your environment file.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<form>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label class="control-label">SMTP Host</label>
|
||||
<div>
|
||||
<input required type="text" class="form-control" name="mail:mailers:smtp:host" value="{{ old('mail:mailers:smtp:host', config('mail.mailers.smtp.host')) }}" />
|
||||
<p class="text-muted small">Enter the SMTP server address that mail should be sent through.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<label class="control-label">SMTP Port</label>
|
||||
<div>
|
||||
<input required type="number" class="form-control" name="mail:mailers:smtp:port" value="{{ old('mail:mailers:smtp:port', config('mail.mailers.smtp.port')) }}" />
|
||||
<p class="text-muted small">Enter the SMTP server port that mail should be sent through.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label class="control-label">Encryption</label>
|
||||
<div>
|
||||
@php
|
||||
$encryption = old('mail:mailers:smtp:encryption', config('mail.mailers.smtp.encryption'));
|
||||
@endphp
|
||||
<select name="mail:mailers:smtp:encryption" class="form-control">
|
||||
<option value="" @if($encryption === '') selected @endif>None</option>
|
||||
<option value="tls" @if($encryption === 'tls') selected @endif>Transport Layer Security (TLS)</option>
|
||||
<option value="ssl" @if($encryption === 'ssl') selected @endif>Secure Sockets Layer (SSL)</option>
|
||||
</select>
|
||||
<p class="text-muted small">Select the type of encryption to use when sending mail.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="control-label">Username <span class="field-optional"></span></label>
|
||||
<div>
|
||||
<input type="text" class="form-control" name="mail:mailers:smtp:username" value="{{ old('mail:mailers:smtp:username', config('mail.mailers.smtp.username')) }}" />
|
||||
<p class="text-muted small">The username to use when connecting to the SMTP server.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="control-label">Password <span class="field-optional"></span></label>
|
||||
<div>
|
||||
<input type="password" class="form-control" name="mail:mailers:smtp:password"/>
|
||||
<p class="text-muted small">The password to use in conjunction with the SMTP username. Leave blank to continue using the existing password. To set the password to an empty value enter <code>!e</code> into the field.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<hr />
|
||||
<div class="form-group col-md-6">
|
||||
<label class="control-label">Mail From</label>
|
||||
<div>
|
||||
<input required type="email" class="form-control" name="mail:from:address" value="{{ old('mail:from:address', config('mail.from.address')) }}" />
|
||||
<p class="text-muted small">Enter an email address that all outgoing emails will originate from.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="control-label">Mail From Name <span class="field-optional"></span></label>
|
||||
<div>
|
||||
<input type="text" class="form-control" name="mail:from:name" value="{{ old('mail:from:name', config('mail.from.name')) }}" />
|
||||
<p class="text-muted small">The name that emails should appear to come from.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
{{ csrf_field() }}
|
||||
<div class="pull-right">
|
||||
<button type="button" id="testButton" class="btn btn-sm btn-success">Test</button>
|
||||
<button type="button" id="saveButton" class="btn btn-sm btn-primary">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('footer-scripts')
|
||||
@parent
|
||||
|
||||
<script>
|
||||
function saveSettings() {
|
||||
return $.ajax({
|
||||
method: 'PATCH',
|
||||
url: '/admin/settings/mail',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
'mail:mailers:smtp:host': $('input[name="mail:mailers:smtp:host"]').val(),
|
||||
'mail:mailers:smtp:port': $('input[name="mail:mailers:smtp:port"]').val(),
|
||||
'mail:mailers:smtp:encryption': $('select[name="mail:mailers:smtp:encryption"]').val(),
|
||||
'mail:mailers:smtp:username': $('input[name="mail:mailers:smtp:username"]').val(),
|
||||
'mail:mailers:smtp:password': $('input[name="mail:mailers:smtp:password"]').val(),
|
||||
'mail:from:address': $('input[name="mail:from:address"]').val(),
|
||||
'mail:from:name': $('input[name="mail:from:name"]').val()
|
||||
}),
|
||||
headers: { 'X-CSRF-Token': $('input[name="_token"]').val() }
|
||||
}).fail(function (jqXHR) {
|
||||
showErrorDialog(jqXHR, 'save');
|
||||
});
|
||||
}
|
||||
|
||||
function testSettings() {
|
||||
swal({
|
||||
type: 'info',
|
||||
title: 'Test Mail Settings',
|
||||
text: 'Click "Test" to begin the test.',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Test',
|
||||
closeOnConfirm: false,
|
||||
showLoaderOnConfirm: true
|
||||
}, function () {
|
||||
$.ajax({
|
||||
method: 'POST',
|
||||
url: '/admin/settings/mail/test',
|
||||
headers: { 'X-CSRF-TOKEN': $('input[name="_token"]').val() }
|
||||
}).fail(function (jqXHR) {
|
||||
showErrorDialog(jqXHR, 'test');
|
||||
}).done(function () {
|
||||
swal({
|
||||
title: 'Success',
|
||||
text: 'The test message was sent successfully.',
|
||||
type: 'success'
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function saveAndTestSettings() {
|
||||
saveSettings().done(testSettings);
|
||||
}
|
||||
|
||||
function showErrorDialog(jqXHR, verb) {
|
||||
console.error(jqXHR);
|
||||
var errorText = '';
|
||||
if (!jqXHR.responseJSON) {
|
||||
errorText = jqXHR.responseText;
|
||||
} else if (jqXHR.responseJSON.error) {
|
||||
errorText = jqXHR.responseJSON.error;
|
||||
} else if (jqXHR.responseJSON.errors) {
|
||||
$.each(jqXHR.responseJSON.errors, function (i, v) {
|
||||
if (v.detail) {
|
||||
errorText += v.detail + ' ';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
swal({
|
||||
title: 'Whoops!',
|
||||
text: 'An error occurred while attempting to ' + verb + ' mail settings: ' + errorText,
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
$('#testButton').on('click', saveAndTestSettings);
|
||||
$('#saveButton').on('click', function () {
|
||||
saveSettings().done(function () {
|
||||
swal({
|
||||
title: 'Success',
|
||||
text: 'Mail settings have been updated successfully and the queue worker was restarted to apply these changes.',
|
||||
type: 'success'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
@@ -0,0 +1,3 @@
|
||||
<x-filament::widget>
|
||||
@livewire(\App\Filament\Resources\NodeResource\Widgets\NodeCpuChart::class, ['record'=> $getRecord()])
|
||||
</x-filament::widget>
|
||||
@@ -0,0 +1,3 @@
|
||||
<x-filament::widget>
|
||||
@livewire(\App\Filament\Resources\NodeResource\Widgets\NodeMemoryChart::class, ['record'=> $getRecord()])
|
||||
</x-filament::widget>
|
||||
@@ -0,0 +1,3 @@
|
||||
<x-filament::widget>
|
||||
@livewire(\App\Filament\Resources\NodeResource\Widgets\NodeStorageChart::class, ['record'=> $getRecord()])
|
||||
</x-filament::widget>
|
||||
7
resources/views/filament/pages/installer.blade.php
Normal file
7
resources/views/filament/pages/installer.blade.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<x-filament-panels::page.simple>
|
||||
<x-filament-panels::form wire:submit="submit">
|
||||
{{ $this->form }}
|
||||
</x-filament-panels::form>
|
||||
|
||||
<x-filament-panels::page.unsaved-data-changes-alert />
|
||||
</x-filament-panels::page.simple>
|
||||
15
resources/views/filament/pages/settings.blade.php
Normal file
15
resources/views/filament/pages/settings.blade.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<x-filament-panels::page
|
||||
@class([
|
||||
'fi-page-settings'
|
||||
])
|
||||
>
|
||||
<x-filament-panels::form
|
||||
id="form"
|
||||
:wire:key="$this->getId() . '.forms.' . $this->getFormStatePath()"
|
||||
wire:submit="save"
|
||||
>
|
||||
{{ $this->form }}
|
||||
</x-filament-panels::form>
|
||||
|
||||
<x-filament-panels::page.unsaved-data-changes-alert />
|
||||
</x-filament-panels::page>
|
||||
@@ -96,11 +96,6 @@
|
||||
</a>
|
||||
</li>
|
||||
<li class="header">OTHER</li>
|
||||
<li class="{{ ! starts_with(Route::currentRouteName(), 'admin.settings') ?: 'active' }}">
|
||||
<a href="{{ route('admin.settings')}}">
|
||||
<i class="fa fa-wrench"></i> <span>Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="{{ ! starts_with(Route::currentRouteName(), 'admin.api') ?: 'active' }}">
|
||||
<a href="{{ route('admin.api.index')}}">
|
||||
<i class="fa fa-gamepad"></i> <span>Application API</span>
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
@include('partials/admin.settings.notice')
|
||||
|
||||
@section('settings::nav')
|
||||
@yield('settings::notice')
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="nav-tabs-custom nav-tabs-floating">
|
||||
<ul class="nav nav-tabs">
|
||||
<li @if($activeTab === 'basic')class="active"@endif><a href="{{ route('admin.settings') }}">General</a></li>
|
||||
<li @if($activeTab === 'mail')class="active"@endif><a href="{{ route('admin.settings.mail') }}">Mail</a></li>
|
||||
<li @if($activeTab === 'advanced')class="active"@endif><a href="{{ route('admin.settings.advanced') }}">Advanced</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -1,11 +0,0 @@
|
||||
@section('settings::notice')
|
||||
@if(config('panel.load_environment_only', false))
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="alert alert-danger">
|
||||
Your Panel is currently configured to read settings from the environment only. You will need to set <code>APP_ENVIRONMENT_ONLY=false</code> in your environment file in order to load settings dynamically.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endsection
|
||||
@@ -40,26 +40,6 @@ Route::prefix('databases')->group(function () {
|
||||
Route::delete('/view/{host:id}', [Admin\DatabaseController::class, 'delete']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Settings Controller Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Endpoint: /admin/settings
|
||||
|
|
||||
*/
|
||||
Route::prefix('settings')->group(function () {
|
||||
Route::get('/', [Admin\Settings\IndexController::class, 'index'])->name('admin.settings');
|
||||
Route::get('/mail', [Admin\Settings\MailController::class, 'index'])->name('admin.settings.mail');
|
||||
Route::get('/advanced', [Admin\Settings\AdvancedController::class, 'index'])->name('admin.settings.advanced');
|
||||
|
||||
Route::post('/mail/test', [Admin\Settings\MailController::class, 'test'])->name('admin.settings.mail.test');
|
||||
|
||||
Route::patch('/', [Admin\Settings\IndexController::class, 'update']);
|
||||
Route::patch('/mail', [Admin\Settings\MailController::class, 'update']);
|
||||
Route::patch('/advanced', [Admin\Settings\AdvancedController::class, 'update']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| User Controller Routes
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Filament\Pages\Installer\PanelInstaller;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\Base;
|
||||
use App\Http\Middleware\RequireTwoFactorAuthentication;
|
||||
@@ -16,5 +17,8 @@ Route::get('/locales/locale.json', Base\LocaleController::class)
|
||||
->withoutMiddleware(['auth', RequireTwoFactorAuthentication::class])
|
||||
->where('namespace', '.*');
|
||||
|
||||
Route::get('installer', PanelInstaller::class)->name('installer')
|
||||
->withoutMiddleware(['auth', RequireTwoFactorAuthentication::class]);
|
||||
|
||||
Route::get('/{react}', [Base\IndexController::class, 'index'])
|
||||
->where('react', '^(?!(\/)?(api|auth|admin|daemon|legacy)).+');
|
||||
|
||||
Reference in New Issue
Block a user