Compare commits

..

51 Commits

Author SHA1 Message Date
github-actions[bot]
0efd777258 ci(release): bump version 2024-08-16 21:04:28 +00:00
MartinOscar
68e24896ae Patch for node 18 (#539) 2024-08-16 16:50:09 -04:00
Boy132
1864fff04f Update default image for new eggs (#540) 2024-08-16 22:44:12 +02:00
Boy132
155f2d6476 Add migration to fix allocations server_id foreign key (#542)
* add migration to fix allocations server_id foreign key

* fix the fix...
2024-08-13 19:43:16 +02:00
notCharles
bad5409d9c Fix saving SMTP without encryption 2024-08-10 19:39:41 -04:00
notCharles
3158bdfef8 Fix Single Egg Import 2024-08-10 18:20:21 -04:00
Boy132
1fba700096 Improve error handling for Installer (#532)
* make sure migrations ran

* add loading indicator to finish button

* make error notification persistent

* fix migration checker

* cleanup traits
2024-08-09 08:23:03 +02:00
MartinOscar
7f8fb3f650 Patch Env CLI (#528)
* Remove unused option

* Add redis user

* Adapt lang

* Change default redis username

* Cleanup

* Update app/Traits/Commands/RequestRedisSettingsTrait.php

Co-authored-by: Boy132 <Boy132@users.noreply.github.com>

---------

Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2024-08-08 17:59:28 -04:00
MartinOscar
d6e0421aaf Update StoreNodeRequest.php (#531) 2024-08-08 17:59:16 -04:00
MartinOscar
e8e1958969 Make default favicon path absolute to avoid 404 on admin (#529) 2024-08-06 06:31:52 -04:00
Boy132
2e094605e9 Round memory, swap and disk limits for wings (#523) 2024-08-04 22:21:23 +02:00
Boy132
953ee940aa Installer followup (#519)
* remove queue worker service creation from installer

* auto check redis
2024-08-04 18:53:54 +02:00
Boy132
496eaaaf83 Web Installer (#504)
* simplify setup command

* add installer page

* add route for installer

* adjust gitignore

* set colors globally

* add "unsaved data changes" alert

* add helper method to check if panel is installed

* make nicer

* redis username isn't required

* bring back db settings command

* store current date in "installed" file

* only redirect if install was successfull

* remove fpm requirement

* change "installed" marker to env variable

* improve requirements step

* add commands to change cache, queue or session drivers respectively

* removed `grouped` for better mobile view
2024-08-03 21:13:17 +02:00
MartinOscar
18cf6e9338 Update SetupTOTPDialog.tsx (#518) 2024-07-31 15:10:58 -04:00
Charles
525a106e81 Change TextArea -> Textarea...
Makes no sense as we have TextInput, TagsInput and KeyValue... But TextArea is an issue...
2024-07-30 14:12:29 -04:00
Charles
d22f975684 More Mobile UI
Closes https://github.com/pelican-dev/panel/issues/512
2024-07-30 12:58:16 -04:00
Charles
c4864feaa5 Whoops 2024-07-30 10:45:12 -04:00
Charles
b7b72d7336 Merge branch 'main' of https://github.com/pelican-dev/panel 2024-07-30 10:43:30 -04:00
Charles
686c4375bc Layout fix for mobile 2024-07-30 10:43:24 -04:00
Boy132
3f40256f8b Settings page followup (#514)
* remove group for toggle buttons

* fix default for APP_DEBUG

* correctly handle bool values

* fix pint

* small cleanup for example .env
2024-07-30 16:07:20 +02:00
Boy132
a58e159478 Settings page (#486)
* remove old settings stuff

* add basic settings page

* add some settings

* add "test mail" button

* fix mail fields not updating

* fix phpstan

* fix default for "top navigation"

* force toggle buttons to be bool

* force toggle to be bool

* add class to view to allow customization

* add mailgun settings

* add notification settings

* add timeout settings

* organize tabs into sub-functions

* add more settings

* add backup settings

* add sections to mail settings

* add setting for trusted_proxies

* fix unsaved data alert not showing

* fix clear action

* Fix clear action v2

TagsInput expects an array, not a string, fails on saving when using `''`

* Add App favicon

* Remove defaults, collapse misc sections

* Move Save btn, Add API rate limit

* small cleanup

---------

Co-authored-by: notCharles <charles@pelican.dev>
2024-07-29 12:14:24 +02:00
Boy132
d89af243a8 Fix user search on "create server" (#508) 2024-07-29 12:13:29 +02:00
Boy132
bddd6af8af Fix user deletion in no interactive mode (#506) 2024-07-29 12:13:08 +02:00
MartinOscar
e1bdf95971 Update SetupTOTPDialog.tsx (#476) 2024-07-29 05:58:20 -04:00
Lance Pioch
465a03bf0e Update readme.md 2024-07-24 20:10:45 -04:00
Boy132
2c2e52b18a fix phpstan (#503) 2024-07-23 11:32:32 +02:00
notCharles
fcef8d69ae Remove breadcrumbs 2024-07-20 19:15:01 -04:00
notCharles
8662806dfd Fix 500 if update url is blank 2024-07-20 18:51:38 -04:00
MartinOscar
acf43f2826 Ability to create allocations on EditServer page (#494)
* Ability to create allocation on edit page + Ability to assign allocation to server on creation

* Disable dehydrate for readonly

* set these to false

---------

Co-authored-by: notCharles <charles@pelican.dev>
2024-07-20 11:38:34 -04:00
Boy132
dfba8e3993 Command to cleanup docker images (#495)
* add command to cleanup docker images

* automatically cleanup images daily

* fix request

* fix empty check

* run pint
2024-07-20 17:23:03 +02:00
Boy132
56484a2282 Increase guzzle timeout when running tests (#485)
* increase guzzle timeout when running tests

* catch correct exception
2024-07-20 17:18:45 +02:00
MartinOscar
56b4938dc2 Fix #489 (#490)
* Fix #489

* Update app/Filament/Resources/NodeResource/Pages/EditNode.php

Co-authored-by: Boy132 <Boy132@users.noreply.github.com>

* Update app/Filament/Resources/NodeResource/Pages/EditNode.php

Co-authored-by: Boy132 <Boy132@users.noreply.github.com>

* Update app/Filament/Resources/NodeResource/Pages/EditNode.php

Co-authored-by: Boy132 <Boy132@users.noreply.github.com>

---------

Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2024-07-17 16:22:12 -04:00
Boy132
10806d6d6b Fix SQLite foreign keys (#478)
* start migration to fix sqlite foreign keys

* add remaining foreign keys

* add ".sqlite.backup" files to gitignore
2024-07-17 14:43:04 +02:00
Boy132
a04937d698 Fix PORT_FLOOR check and CIDR_MAX_BITS in AssignmentService (#491)
* fix max cidr

* fix port floor
2024-07-17 13:01:13 +02:00
Boy132
8a3d67ada0 Fix update egg from url (#492) 2024-07-17 13:00:54 +02:00
Charles
833ae30e59 Add timeouts (#483)
* Add timeouts

Add Timeouts to github call.

* use config value
2024-07-15 19:09:52 -04:00
Charles
1fdff43ae7 Add Node CPU/Memory Graphs (#459)
* Update Node Stats

Soon TM

* Update

* Make these smaller

* Change graphs

* Remove this.

Didn't work anyways.

* Update Graphs

* Use User TZ and config var

* Fix math

* Change to per thread.
2024-07-14 16:48:14 -04:00
Boy132
bb7c0e0e66 Add "Delete files" task (#470)
* started "delete files" task

* add logic to DeleteFilesService

* add frontend

* make nicer

* move description to right place
2024-07-10 09:25:15 +02:00
Boy132
447e889a4f Fix default timestamp for activity logs (#468)
* fix default timestamp for activity logs

* fix phpstan
2024-07-10 08:36:24 +02:00
Exotical
1c1c8c0cc6 Fix client Activity tab issues; fixes #465 (#466)
* Remove deploy.locations from validator

* Change location data to optional for backwards compat

* Better styling

* Add back comma to follow coding style

* Remove EventServiceProvider from providers file

Fixes duplicated auth messages in the client Activity tab.

* Add null check on $model->actor

Prevents the client Activity tab page from breaking when an authentication attempt has failed.

* Proper type checking on $model->actor

Chose instanceof as it seems to be the best in terms of type safety.

Co-authored-by: MartinOscar <40749467+RMartinOscar@users.noreply.github.com>

* Revert removal of EventServiceProvider

* Remove subscription of AuthenticationListener

* Remove subscriptions for auth events

* Remove unused import Dispatcher

* Remove unused import AuthenticationListener

---------

Co-authored-by: MartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2024-07-09 21:30:12 -07:00
notCharles
7dad2d0e42 Fix #464 2024-07-07 19:33:25 -04:00
notCharles
212c93c2ba Fix #462 2024-07-05 18:24:07 -04:00
Boy132
7557dc1c8d Restart queue worker when changing email settings (#457) 2024-07-05 16:17:35 +02:00
Boy132
07735464c7 Add contributing guide (#460) 2024-07-05 01:15:45 +02:00
notCharles
8ba15538a9 Fix ToolTip 2024-07-03 16:33:32 -04:00
Charles
c115c6ddf5 Add Update URL to stock eggs 2024-07-03 10:36:34 -04:00
Charles
160ea1ed50 Enable Update URL
Since importing an egg via url was added, we can enable this.
2024-07-03 10:27:57 -04:00
MartinOscar
7164951085 Update EditServer.php (#455) 2024-07-02 13:31:35 -04:00
Charles
40721a2cb8 Fix #452
Prob not the best solution, but it works

Closes: https://github.com/pelican-dev/panel/issues/452
2024-07-02 08:01:17 -04:00
MartinOscar
c464b321dd Update EditProfile.php (#454) 2024-07-02 07:05:00 -04:00
MartinOscar
0f8c27a297 Update ContainerStatus add Starting|Stopping|Default (#449)
* Update ContainerStatus add Starting

* Update ContainerStatus add Stopping

* Update ContainerStatus add Default

* Update Icons, PHPStan

---------

Co-authored-by: notCharles <charles@pelican.dev>
2024-06-30 10:13:08 -04:00
104 changed files with 2580 additions and 1320 deletions

View File

@@ -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=

View File

@@ -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

View File

@@ -2,163 +2,49 @@
namespace App\Console\Commands\Environment;
use App\Traits\EnvironmentWriterTrait;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Kernel;
use App\Traits\Commands\EnvironmentWriterTrait;
use Illuminate\Support\Facades\Artisan;
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");
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace App\Console\Commands\Environment;
use App\Traits\Commands\RequestRedisSettingsTrait;
use App\Traits\EnvironmentWriterTrait;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Kernel;
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-user= : User used to connect to redis.}
{--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', [
'--overwrite' => true,
]);
}
}
$this->writeToEnvironment($this->variables);
$this->info($this->console->output());
return 0;
}
}

View File

@@ -2,10 +2,10 @@
namespace App\Console\Commands\Environment;
use App\Traits\EnvironmentWriterTrait;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Database\DatabaseManager;
use App\Traits\Commands\EnvironmentWriterTrait;
class DatabaseSettingsCommand extends Command
{

View File

@@ -2,8 +2,8 @@
namespace App\Console\Commands\Environment;
use App\Traits\EnvironmentWriterTrait;
use Illuminate\Console\Command;
use App\Traits\Commands\EnvironmentWriterTrait;
class EmailSettingsCommand extends Command
{
@@ -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('');
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Console\Commands\Environment;
use App\Traits\Commands\RequestRedisSettingsTrait;
use App\Traits\EnvironmentWriterTrait;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Kernel;
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-user= : User used to connect to redis.}
{--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', [
'--overwrite' => true,
]);
}
$this->writeToEnvironment($this->variables);
$this->info($this->console->output());
return 0;
}
}

View File

@@ -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();

View File

@@ -0,0 +1,69 @@
<?php
namespace App\Console\Commands\Environment;
use App\Traits\Commands\RequestRedisSettingsTrait;
use App\Traits\EnvironmentWriterTrait;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Kernel;
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-user= : User used to connect to redis.}
{--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', [
'--overwrite' => true,
]);
}
}
$this->writeToEnvironment($this->variables);
$this->info($this->console->output());
return 0;
}
}

View 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());
}
}
}

View File

@@ -2,7 +2,7 @@
namespace App\Console\Commands\Overrides;
use App\Console\RequiresDatabaseMigrations;
use App\Traits\Commands\RequiresDatabaseMigrations;
use Illuminate\Database\Console\Seeds\SeedCommand as BaseSeedCommand;
class SeedCommand extends BaseSeedCommand

View File

@@ -2,7 +2,7 @@
namespace App\Console\Commands\Overrides;
use App\Console\RequiresDatabaseMigrations;
use App\Traits\Commands\RequiresDatabaseMigrations;
use Illuminate\Foundation\Console\UpCommand as BaseUpCommand;
class UpCommand extends BaseUpCommand

View File

@@ -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'));
}

View File

@@ -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.

View File

@@ -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',
};
}

View File

@@ -1,10 +0,0 @@
<?php
namespace App\Filament\Clusters;
use Filament\Clusters\Cluster;
class Settings extends Cluster
{
protected static ?string $navigationIcon = 'tabler-settings';
}

View File

@@ -0,0 +1,143 @@
<?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\CheckMigrationsTrait;
use App\Traits\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 CheckMigrationsTrait;
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"
wire:loading.attr="disabled"
>
Finish
<span wire:loading><x-filament::loading-indicator class="h-4 w-4" /></span>
</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,
]);
if (!$this->hasCompletedMigrations()) {
throw new Exception('Migrations didn\'t run successfully. Double check your database configuration.');
}
// 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) {
report($exception);
Notification::make()
->title('Installation Failed')
->body($exception->getMessage())
->danger()
->persistent()
->send();
}
}
}

View 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(),
]);
}
}

View 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');
}
}
});
}
}

View 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')),
]);
}
}

View 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')),
]);
}
}

View 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();
}
});
}
}

View File

@@ -0,0 +1,572 @@
<?php
namespace App\Filament\Pages;
use App\Models\Backup;
use App\Notifications\MailTested;
use App\Traits\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('Host')
->required()
->default(env('MAIL_HOST', config('mail.mailers.smtp.host'))),
TextInput::make('MAIL_PORT')
->label('Port')
->required()
->numeric()
->minValue(1)
->maxValue(65535)
->default(env('MAIL_PORT', config('mail.mailers.smtp.port'))),
TextInput::make('MAIL_USERNAME')
->label('Username')
->required()
->default(env('MAIL_USERNAME', config('mail.mailers.smtp.username'))),
TextInput::make('MAIL_PASSWORD')
->label('Password')
->password()
->revealable()
->default(env('MAIL_PASSWORD')),
ToggleButtons::make('MAIL_ENCRYPTION')
->label('Encryption')
->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('Domain')
->required()
->default(env('MAILGUN_DOMAIN', config('services.mailgun.domain'))),
TextInput::make('MAILGUN_SECRET')
->label('Secret')
->required()
->default(env('MAIL_USERNAME', config('services.mailgun.secret'))),
TextInput::make('MAILGUN_ENDPOINT')
->label('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 [];
}
}

View File

@@ -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()

View File

@@ -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()
@@ -186,7 +187,7 @@ class CreateEgg extends CreateRecord
TextInput::make('script_container')
->required()
->maxLength(255)
->default('alpine:3.4'),
->default('ghcr.io/pelican-eggs/installers:debian'),
Select::make('script_entry')
->selectablePlaceholder(false)

View File

@@ -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(),
]),
@@ -267,16 +270,14 @@ class EditEgg extends EditRecord
Notification::make()
->title('Import Failed')
->body($exception->getMessage())
->danger()
->danger() // Will Robinson
->send();
report($exception);
return;
}
}
if (!empty($data['url'])) {
} elseif (!empty($data['url'])) {
try {
$eggImportService->fromUrl($data['url'], $egg);
} catch (Exception $exception) {

View File

@@ -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;
}
}

View 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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'],
];

View File

@@ -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()

View File

@@ -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')

View File

@@ -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'),

View File

@@ -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'),

View File

@@ -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');
}
}

View File

@@ -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');
}
}

View File

@@ -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);
}
}

View File

@@ -20,6 +20,7 @@ class StoreNodeRequest extends ApplicationApiRequest
return collect($rules ?? Node::getRules())->only([
'public',
'name',
'description',
'fqdn',
'scheme',
'behind_proxy',

View File

@@ -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',

View 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());
}
}
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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));
});

View File

@@ -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',

View File

@@ -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]);
}
}

View File

@@ -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.

View File

@@ -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();
});

View File

@@ -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.
*/

View File

@@ -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')

View File

@@ -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;
}
}

View File

@@ -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,
];
}

View 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());
}
}
}

View File

@@ -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';

View File

@@ -51,12 +51,12 @@ class ServerConfigurationStructureService
'invocation' => $server->startup,
'skip_egg_scripts' => $server->skip_scripts,
'build' => [
'memory_limit' => config('panel.use_binary_prefix') ? $server->memory : $server->memory / 1.048576,
'swap' => config('panel.use_binary_prefix') ? $server->swap : $server->swap / 1.048576,
'memory_limit' => (int) round(config('panel.use_binary_prefix') ? $server->memory : $server->memory / 1.048576),
'swap' => (int) round(config('panel.use_binary_prefix') ? $server->swap : $server->swap / 1.048576),
'io_weight' => $server->io,
'cpu_limit' => $server->cpu,
'threads' => $server->threads,
'disk_space' => config('panel.use_binary_prefix') ? $server->disk : $server->disk / 1.048576,
'disk_space' => (int) round(config('panel.use_binary_prefix') ? $server->disk : $server->disk / 1.048576),
'oom_killer' => $server->oom_killer,
],
'container' => [

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Traits;
use Illuminate\Database\Migrations\Migrator;
trait CheckMigrationsTrait
{
/**
* Checks if the migrations have finished running by comparing the last migration file.
*/
protected function hasCompletedMigrations(): bool
{
/** @var Migrator $migrator */
$migrator = app()->make('migrator');
$files = $migrator->getMigrationFiles(database_path('migrations'));
if (!$migrator->repositoryExists()) {
return false;
}
if (array_diff(array_keys($files), $migrator->getRepository()->getRan())) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,52 @@
<?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')
);
$askForRedisUser = true;
$askForRedisPassword = true;
if (!empty(config('database.redis.default.user'))) {
$this->variables['REDIS_USERNAME'] = config('database.redis.default.user');
$askForRedisUser = $this->confirm(__('commands.appsettings.redis.confirm', ['field' => 'user']));
}
if (!empty(config('database.redis.default.password'))) {
$this->variables['REDIS_PASSWORD'] = config('database.redis.default.password');
$askForRedisPassword = $this->confirm(__('commands.appsettings.redis.confirm', ['field' => 'password']));
}
if ($askForRedisUser) {
$this->output->comment(__('commands.appsettings.redis.comment'));
$this->variables['REDIS_USERNAME'] = $this->option('redis-user') ?? $this->output->askHidden(
'Redis User'
);
}
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_USERNAME'])) {
$this->variables['REDIS_USERNAME'] = 'null';
}
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')
);
}
}

View File

@@ -1,32 +1,16 @@
<?php
namespace App\Console;
namespace App\Traits\Commands;
use App\Traits\CheckMigrationsTrait;
use Illuminate\Console\Command;
/**
* @mixin \Illuminate\Console\Command
* @mixin Command
*/
trait RequiresDatabaseMigrations
{
/**
* Checks if the migrations have finished running by comparing the last migration file.
*/
protected function hasCompletedMigrations(): bool
{
/** @var \Illuminate\Database\Migrations\Migrator $migrator */
$migrator = $this->getLaravel()->make('migrator');
$files = $migrator->getMigrationFiles(database_path('migrations'));
if (!$migrator->repositoryExists()) {
return false;
}
if (array_diff(array_keys($files), $migrator->getRepository()->getRan())) {
return false;
}
return true;
}
use CheckMigrationsTrait;
/**
* Throw a massive error into the console to hopefully catch the users attention and get

View File

@@ -1,8 +1,8 @@
<?php
namespace App\Traits\Commands;
namespace App\Traits;
use App\Exceptions\PanelException;
use Exception;
trait EnvironmentWriterTrait
{
@@ -22,12 +22,13 @@ trait EnvironmentWriterTrait
/**
* Update the .env file for the application using the passed in values.
* @throws Exception
*/
public function writeToEnvironment(array $values = []): void
{
$path = base_path('.env');
if (!file_exists($path)) {
throw new PanelException('Cannot locate .env file, was this software installed correctly?');
throw new Exception('Cannot locate .env file, was this software installed correctly?');
}
$saveContents = file_get_contents($path);

View File

@@ -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]'];
}

View File

@@ -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);
}
}

View File

@@ -8,6 +8,5 @@ return [
App\Providers\Filament\AdminPanelProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\ViewComposerServiceProvider::class,
SocialiteProviders\Manager\ServiceProvider::class,
];

View File

@@ -5,8 +5,9 @@ use Illuminate\Support\Facades\Facade;
return [
'name' => env('APP_NAME', 'Pelican'),
'favicon' => env('APP_FAVICON', '/pelican.ico'),
'version' => 'canary',
'version' => '1.0.0-beta8',
'exceptions' => [
'report_all' => env('APP_REPORT_ALL_EXCEPTIONS', false),

View File

@@ -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
View 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 its 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
View File

@@ -1 +1,2 @@
*.sqlite
*.sqlite.backup

View File

@@ -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",

View File

@@ -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"
}
]

View File

@@ -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"
}
]

View File

@@ -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"
}
]
}
}

View File

@@ -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"
}
]

View File

@@ -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"
}
]

View File

@@ -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"
}
]

View File

@@ -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"
}
]

View File

@@ -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"
}
]

View File

@@ -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"
}
]

View File

@@ -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"
}
]

View File

@@ -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"
}
]

View File

@@ -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"
}
]

View File

@@ -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();
});
}
};

View File

@@ -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
}
};

View File

@@ -0,0 +1,54 @@
<?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 () {
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 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")');
});
DB::statement('PRAGMA foreign_keys = ON');
DB::statement('PRAGMA legacy_alter_table = OFF');
}
/**
* Reverse the migrations.
*/
public function down(): void
{
// Reverse not needed
}
};

View File

@@ -6,11 +6,11 @@ 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.',
'comment' => 'By default a Redis server instance has no password as it is running locally and inaccessible to the outside world. If this is the case, simply hit enter without entering a value.',
'comment' => 'By default a Redis server instance has for username default and no password as it is running locally and inaccessible to the outside world. If this is the case, simply hit enter without entering a value.',
'confirm' => 'It seems a :field is already defined for Redis, would you like to change it?',
],
],
'database_settings' => [

View File

@@ -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.',

View File

@@ -40,6 +40,7 @@
"react-router-dom": "^5.1.2",
"react-transition-group": "^4.4.1",
"reaptcha": "^1.7.2",
"rimraf": "^4",
"sockette": "^2.0.6",
"styled-components": "^5.2.1",
"styled-components-breakpoint": "^3.0.0-preview.20",
@@ -130,7 +131,7 @@
"yarn-deduplicate": "^1.1.1"
},
"scripts": {
"clean": "cd public/assets && find . \\( -name \"*.js\" -o -name \"*.map\" \\) -type f -delete",
"clean": "cd public/assets && rimraf -g *.js *.map",
"test": "jest",
"lint": "eslint ./resources/scripts/**/*.{ts,tsx} --ext .ts,.tsx",
"watch": "cross-env NODE_ENV=development ./node_modules/.bin/webpack --watch --progress",

View File

@@ -5,9 +5,6 @@
![Total Downloads](https://img.shields.io/github/downloads/pelican-dev/panel/total?style=flat&label=Total%20Downloads&labelColor=rgba(0%2C%2070%2C%20114%2C%201)&color=rgba(255%2C%20255%2C%20255%2C%201))
![Latest Release](https://img.shields.io/github/v/release/pelican-dev/panel?style=flat&label=Latest%20Release&labelColor=rgba(0%2C%2070%2C%20114%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
![Stats](https://repobeats.axiom.co/api/embed/4d8cc7012b325141e6fae9c34a22b3669ad5753b.svg "Repobeats analytics image")
*Copyright Pelican® 2024*

View File

@@ -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);

View File

@@ -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`}>

View File

@@ -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`}
>

View File

@@ -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`}>

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,3 @@
<x-filament::widget>
@livewire(\App\Filament\Resources\NodeResource\Widgets\NodeCpuChart::class, ['record'=> $getRecord()])
</x-filament::widget>

View File

@@ -0,0 +1,3 @@
<x-filament::widget>
@livewire(\App\Filament\Resources\NodeResource\Widgets\NodeMemoryChart::class, ['record'=> $getRecord()])
</x-filament::widget>

View File

@@ -0,0 +1,3 @@
<x-filament::widget>
@livewire(\App\Filament\Resources\NodeResource\Widgets\NodeStorageChart::class, ['record'=> $getRecord()])
</x-filament::widget>

View 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>

View 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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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

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