Compare commits

...

59 Commits

Author SHA1 Message Date
Boy132
fe4668a517 Update web installer (again) (#705)
* update web installer (again)

* set default values for mysql/ mariadb and redis

* add own step for queue setup

* create admin user in submit

* disable redis for queue if cache isn't redis

* remove separate user step and make session own step

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

* Remove controller test

* Remove unused exceptions

* Remove unused files

* More small tweaks

* Fix doc block

* Remove unused service

* Restore these

* Add back autoDeploy

* Revert "Add back autoDeploy"

This reverts commit 630c1e08ac.

* Add these back

* Add back exception

* Remove ApiController again

---------

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

* fix old admin area

* show latest wings version on EditNode page

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

* fix: type casts

---------

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

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

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

* use RESOURCE_NAME for transformers

* add permissions field to api key

* add migration for new permissions field

* update tests

* remove debug log

* set column type to "json"

* remove default attribute to fix tests

* fix default value for permissions

* fix after merge

* fix after merge

* allow to "register" custom permissions

* add "role" to default resource names

* fix after merge

* fix phpstan

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

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

* Add cloudflare's ipv6

* Pull from url innstead of hardcoded

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

* add config & settings for turnstile

* publish view to center captcha

* completely replace reCAPTCHA

* update FailedCaptcha event

* add back config for domain verification

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

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

* Move EditPage aswell

* Move Save

* Labels

* Change Edit/Delete

---------

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

* Pinted 3times with --dirty bruh

* Add to Settings

* Fix prune & description

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

resolves #675

* correct version tag

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

Should address issue 672

Closes #672

* Fix Docker image selection in CreateServer page

---------

Co-authored-by: MartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2024-10-27 11:22:12 -04:00
MartinOscar
a70a060350 Add Soft Deletes to webhooks config table (#670) 2024-10-27 00:42:08 -04:00
MartinOscar
590569a131 Remove duplicated spa in AdminPanelProvider (#668) 2024-10-26 23:25:21 -04:00
Charles
7acc8782bb Make description required. (#667) 2024-10-26 22:06:34 -04:00
MartinOscar
f3de185508 Add back auto deploy (#627)
* Add Docker, Refactor, Fix Notification

Co-authored-by: notCharles <charles@pelican.dev>

* Pint

* Required adjustments

* Remove deprecated

* Third time's the charm

---------

Co-authored-by: notCharles <charles@pelican.dev>
2024-10-26 20:43:19 -04:00
Charles
291b514e24 Webhook updates (#666) 2024-10-26 20:40:19 -04:00
Colin DeCarlo
86c369d7ce Implement Webhooks (#548)
* feat: First Webhook PoC draft

* feat: Dispatch Webhooks PoC

* fix: typo in webhook configuration scope

* Update 2024_04_21_162552_create_webhooks_table.php

Co-authored-by: Lance Pioch <lancepioch@gmail.com>

* Update 2024_04_21_162552_create_webhooks_table.php

Co-authored-by: Lance Pioch <lancepioch@gmail.com>

* Update 2024_04_21_162544_create_webhook_configurations_table.php

Co-authored-by: Lance Pioch <lancepioch@gmail.com>

* Update 2024_04_21_162544_create_webhook_configurations_table.php

Co-authored-by: Lance Pioch <lancepioch@gmail.com>

* Update DispatchWebhooks.php

Co-authored-by: Lance Pioch <lancepioch@gmail.com>

* Update DispatchWebhooksJob.php

Co-authored-by: Lance Pioch <lancepioch@gmail.com>

* Update DispatchWebhookForConfiguration.php

Co-authored-by: Lance Pioch <lancepioch@gmail.com>

* Update DispatchWebhookForConfiguration.php

Co-authored-by: Lance Pioch <lancepioch@gmail.com>

* Update DispatchWebhookForConfiguration.php

Co-authored-by: Lance Pioch <lancepioch@gmail.com>

* Update DispatchWebhooksJob.php

Co-authored-by: Lance Pioch <lancepioch@gmail.com>

* Update DispatchWebhooksJob.php

Co-authored-by: Lance Pioch <lancepioch@gmail.com>

* Update DispatchWebhooksJob.php

Co-authored-by: Lance Pioch <lancepioch@gmail.com>

* chore: Implement Webhook Event Discovery

* we got a test working for webhooks

* WIP

* Something is working!

* More tests

* clean up the tests now that they are passing

* WIP

* Don't use model specific events

* WIP

* WIP

* WIP

* WIP

* WIP

* Do it sync

* Reset these

* Don't need restored event type

* Deleted some unused jobs

* Find custom Events

* Remove observers

* Add custom event test

* Run Pint

* Add caching

* Don't cache every single event

* Fix tests

* Run Pint

* Phpstan fixes

* Pint fix

* Test fixes

* Middleware unit test fix

* Pint fixes

* Remove index not working for older dbs

* Use facade instead

---------

Co-authored-by: Pascale Beier <mail@pascalebeier.de>
Co-authored-by: Lance Pioch <lancepioch@gmail.com>
Co-authored-by: Vehikl <go@vehikl.com>
2024-10-26 20:35:25 -04:00
Quinten
5f77deb1fd Panel: Fix wings stoplogic (#407)
* Panel: FIx wings stoplogic

* do not make an expetion for `^C` let wings handle this

* remove withspaces
2024-10-26 19:21:14 -04:00
Charles
5f4429e2c3 Remove Bulk Delete from Nodes (#665)
* Remove Bulk Delete from Nodes

Removes bulk delete option from nodes.

* pint
2024-10-26 18:59:06 -04:00
Lance Pioch
1df3e8d5b0 Don't allow NodeStatisticsJob to be in the queue multiple times (#664)
* Make job unique

* Pint fix
2024-10-26 18:53:32 -04:00
Michael (Parker) Parker
ecb195b2c4 Merge pull request #662 from BlockyBlockling/docker-workflow-rework
Adding fix for forks to use a variable for Docker image reference instead of hard String
2024-10-26 18:45:59 -04:00
BlockyBlockling
86e8a6371e Update docker-publish.yml
Adding fix for forks to use a variable for Docker image reference

Source of information: https://docs.github.com/en/actions/use-cases-and-examples/publishing-packages/publishing-docker-images
2024-10-24 22:05:46 +02:00
Michael (Parker) Parker
d653edb22e Merge pull request #660 from BlockyBlockling/main
Fixing Critical error on Webserver on Pelican Panel Docker Image
2024-10-24 15:21:48 -04:00
BlockyBlockling
741252e395 Update supervisord.conf
Adding username and password dummy to get rid of critical error message
2024-10-24 21:15:03 +02:00
Michael (Parker) Parker
308601e6fe Merge pull request #659 from pelican-dev/issue/629
Make sure the .env can be accessed by the webserver when running Docker
2024-10-24 08:59:34 -04:00
Lance Pioch
3933222d98 Make sure the .env can be accessed 2024-10-23 21:36:48 -04:00
Boy132
c53ef78d89 Make sure schedules run with UTC (#657)
* make sure schedules use UTC for `next_run_at`

* use function from Utilities
2024-10-23 21:59:13 +02:00
Boy132
60792c05c2 Fix required for pinned threads input (#656) 2024-10-23 12:50:09 +02:00
Boy132
94420d06be Add UI for cpu pinning (#652)
* add ui for cpu pinning

* create "advanced" section
2024-10-22 23:34:46 +02:00
Fredrik Falk
6655ccca6e Speed up docker start (#647)
Starting the docker container is hampered due to setting `chown -R www-data:www-data /var/www/html/` on every start, causing it to traverse the entire directory which in our use case is very slow. This PR instead changes it to set permissions as part of the build process.

Sidenote: Is `LE_EMAIL` supposed to be used in addition to `ADMIN_EMAIL`?
2024-10-21 12:46:42 -04:00
Boy132
a193b4f5ab Installer: fix argument types for testConnection & return type for submit (#650)
* fix argument types for `testConnection`

* fix return type of submit
2024-10-21 18:43:16 +02:00
Boy132
3d5c8d14bd Add back trustedproxy config (#651) 2024-10-21 18:43:05 +02:00
Lance Pioch
de002324d7 Deselect all table records when switching primary allocation (#645) 2024-10-21 12:27:23 -04:00
386 changed files with 2574 additions and 22462 deletions

View File

@@ -28,6 +28,7 @@ fi
mkdir /pelican-data/database
ln -s /pelican-data/.env /var/www/html/
chown -h www-data:www-data /var/www/html/.env
ln -s /pelican-data/database/database.sqlite /var/www/html/database/
if ! grep -q "APP_KEY=" .env || grep -q "APP_KEY=$" .env; then
@@ -51,14 +52,14 @@ crond -L /var/log/crond -l 5
export SUPERVISORD_CADDY=false
## disable caddy if SKIP_CADDY is set
if [[ -z $SKIP_CADDY ]]; then
if [[ "${SKIP_CADDY:-}" == "true" ]]; then
echo "Starting PHP-FPM only"
else
echo "Starting PHP-FPM and Caddy"
export SUPERVISORD_CADDY=true
else
echo "Starting PHP-FPM only"
fi
chown -R www-data:www-data . /pelican-data/.env /pelican-data/database
chown -R www-data:www-data /pelican-data/.env /pelican-data/database
echo "Starting Supervisord"
exec "$@"

View File

@@ -1,5 +1,7 @@
[unix_http_server]
file=/tmp/supervisor.sock ; path to your socket file
username=dummy
password=dummy
[supervisord]
logfile=/var/log/supervisord/supervisord.log ; supervisord log file
@@ -18,6 +20,8 @@ supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[supervisorctl]
serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket
username=dummy
password=dummy
[program:php-fpm]
command=/usr/local/sbin/php-fpm -F

View File

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

View File

@@ -9,6 +9,10 @@ on:
types:
- published
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
name: Build and Push
@@ -26,7 +30,7 @@ jobs:
id: docker_meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/pelican-dev/panel
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: |
latest=false
tags: |

View File

@@ -38,7 +38,8 @@ RUN touch .env
RUN composer install --no-dev --optimize-autoloader
# Set file permissions
RUN chmod -R 755 storage bootstrap/cache
RUN chmod -R 755 storage bootstrap/cache \
&& chown -R www-data:www-data ./
# Add scheduler to cron
RUN echo "* * * * * php /var/www/html/artisan schedule:run >> /dev/null 2>&1" | crontab -u www-data -

View File

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

View File

@@ -6,7 +6,6 @@ use Illuminate\Console\Command;
use App\Models\Schedule;
use Illuminate\Database\Eloquent\Builder;
use App\Services\Schedules\ProcessScheduleService;
use Carbon\Carbon;
class ProcessRunnableCommand extends Command
{
@@ -24,7 +23,7 @@ class ProcessRunnableCommand extends Command
->whereRelation('server', fn (Builder $builder) => $builder->whereNull('status'))
->where('is_active', true)
->where('is_processing', false)
->where('next_run_at', '<=', Carbon::now()->toDateTimeString())
->where('next_run_at', '<=', now('UTC')->toDateTimeString())
->get();
if ($schedules->count() < 1) {

View File

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

View File

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

View File

@@ -1,19 +0,0 @@
<?php
namespace App\Events\Server;
use App\Events\Event;
use App\Models\Server;
use Illuminate\Queue\SerializesModels;
class Created extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Server $server)
{
}
}

View File

@@ -1,19 +0,0 @@
<?php
namespace App\Events\Server;
use App\Events\Event;
use App\Models\Server;
use Illuminate\Queue\SerializesModels;
class Creating extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Server $server)
{
}
}

View File

@@ -1,19 +0,0 @@
<?php
namespace App\Events\Server;
use App\Events\Event;
use App\Models\Server;
use Illuminate\Queue\SerializesModels;
class Deleted extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Server $server)
{
}
}

View File

@@ -1,19 +0,0 @@
<?php
namespace App\Events\Server;
use App\Events\Event;
use App\Models\Server;
use Illuminate\Queue\SerializesModels;
class Deleting extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Server $server)
{
}
}

View File

@@ -1,19 +0,0 @@
<?php
namespace App\Events\Server;
use App\Events\Event;
use App\Models\Server;
use Illuminate\Queue\SerializesModels;
class Saved extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Server $server)
{
}
}

View File

@@ -1,19 +0,0 @@
<?php
namespace App\Events\Server;
use App\Events\Event;
use App\Models\Server;
use Illuminate\Queue\SerializesModels;
class Saving extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Server $server)
{
}
}

View File

@@ -1,19 +0,0 @@
<?php
namespace App\Events\Server;
use App\Events\Event;
use App\Models\Server;
use Illuminate\Queue\SerializesModels;
class Updated extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Server $server)
{
}
}

View File

@@ -1,19 +0,0 @@
<?php
namespace App\Events\Server;
use App\Events\Event;
use App\Models\Server;
use Illuminate\Queue\SerializesModels;
class Updating extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Server $server)
{
}
}

View File

@@ -1,19 +0,0 @@
<?php
namespace App\Events\Subuser;
use App\Events\Event;
use App\Models\Subuser;
use Illuminate\Queue\SerializesModels;
class Created extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Subuser $subuser)
{
}
}

View File

@@ -1,19 +0,0 @@
<?php
namespace App\Events\Subuser;
use App\Events\Event;
use App\Models\Subuser;
use Illuminate\Queue\SerializesModels;
class Creating extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Subuser $subuser)
{
}
}

View File

@@ -1,19 +0,0 @@
<?php
namespace App\Events\Subuser;
use App\Events\Event;
use App\Models\Subuser;
use Illuminate\Queue\SerializesModels;
class Deleted extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Subuser $subuser)
{
}
}

View File

@@ -1,19 +0,0 @@
<?php
namespace App\Events\Subuser;
use App\Events\Event;
use App\Models\Subuser;
use Illuminate\Queue\SerializesModels;
class Deleting extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Subuser $subuser)
{
}
}

View File

@@ -1,19 +0,0 @@
<?php
namespace App\Events\User;
use App\Models\User;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
class Created extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public User $user)
{
}
}

View File

@@ -1,19 +0,0 @@
<?php
namespace App\Events\User;
use App\Models\User;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
class Creating extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public User $user)
{
}
}

View File

@@ -1,19 +0,0 @@
<?php
namespace App\Events\User;
use App\Models\User;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
class Deleted extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public User $user)
{
}
}

View File

@@ -1,19 +0,0 @@
<?php
namespace App\Events\User;
use App\Models\User;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
class Deleting extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public User $user)
{
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,7 +19,7 @@ class ApiKeyResource extends Resource
public static function getNavigationBadge(): ?string
{
return static::getModel()::where('key_type', '2')->count() ?: null;
return static::getModel()::where('key_type', ApiKey::TYPE_APPLICATION)->count() ?: null;
}
public static function canEdit(Model $record): bool

View File

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

View File

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

View File

@@ -4,9 +4,12 @@ namespace App\Filament\Resources\NodeResource\Pages;
use App\Filament\Resources\NodeResource;
use App\Models\Node;
use App\Services\Helpers\SoftwareVersionService;
use App\Services\Nodes\NodeAutoDeployService;
use App\Services\Nodes\NodeUpdateService;
use Filament\Actions;
use Filament\Forms;
use Filament\Forms\Components\Actions as FormActions;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Placeholder;
@@ -21,6 +24,7 @@ use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use Filament\Support\Enums\Alignment;
use Illuminate\Support\HtmlString;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
@@ -52,7 +56,7 @@ class EditNode extends EditRecord
->schema([
Placeholder::make('')
->label('Wings Version')
->content(fn (Node $node) => $node->systemInformation()['version'] ?? 'Unknown'),
->content(fn (Node $node, SoftwareVersionService $versionService) => ($node->systemInformation()['version'] ?? 'Unknown') . ' (Latest: ' . $versionService->latestWingsVersion() . ')'),
Placeholder::make('')
->label('CPU Threads')
->content(fn (Node $node) => $node->systemInformation()['cpu_count'] ?? 0),
@@ -149,19 +153,9 @@ class EditNode extends EditRecord
true => 'success',
false => 'danger',
])
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 1,
]),
->columnSpan(1),
TextInput::make('daemon_listen')
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 1,
])
->columnSpan(1)
->label(trans('strings.port'))
->helperText('If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.')
->minValue(1)
@@ -182,12 +176,7 @@ class EditNode extends EditRecord
->maxLength(100),
ToggleButtons::make('scheme')
->label('Communicate over SSL')
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 1,
])
->columnSpan(1)
->inline()
->helperText(function (Get $get) {
if (request()->isSecure()) {
@@ -215,23 +204,48 @@ class EditNode extends EditRecord
])
->default(fn () => request()->isSecure() ? 'https' : 'http'), ]),
Tab::make('Advanced Settings')
->columns(['default' => 1, 'sm' => 1, 'md' => 4, 'lg' => 6])
->columns([
'default' => 1,
'sm' => 1,
'md' => 4,
'lg' => 6,
])
->icon('tabler-server-cog')
->schema([
TextInput::make('id')
->label('Node ID')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 1])
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 2,
'lg' => 1,
])
->disabled(),
TextInput::make('uuid')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 2,
'lg' => 2,
])
->label('Node UUID')
->hintAction(CopyAction::make())
->disabled(),
TagsInput::make('tags')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 2,
'lg' => 2,
])
->placeholder('Add Tags'),
TextInput::make('upload_size')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 1])
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 2,
'lg' => 1,
])
->label('Upload Limit')
->hintIcon('tabler-question-mark')
->hintIconTooltip('Enter the maximum size of files that can be uploaded through the web-based file manager.')
@@ -240,7 +254,12 @@ class EditNode extends EditRecord
->maxValue(1024)
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'),
TextInput::make('daemon_sftp')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 3,
])
->label('SFTP Port')
->minValue(1)
->maxValue(65535)
@@ -248,11 +267,21 @@ class EditNode extends EditRecord
->required()
->integer(),
TextInput::make('daemon_sftp_alias')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 3,
])
->label('SFTP Alias')
->helperText('Display alias for the SFTP address. Leave empty to use the Node FQDN.'),
ToggleButtons::make('public')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 3,
])
->label('Use Node for deployment?')->inline()
->options([
true => 'Yes',
@@ -263,7 +292,12 @@ class EditNode extends EditRecord
false => 'danger',
]),
ToggleButtons::make('maintenance_mode')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 3,
])
->label('Maintenance Mode')->inline()
->hinticon('tabler-question-mark')
->hintIconTooltip("If the node is marked 'Under Maintenance' users won't be able to access servers that are on this node.")
@@ -276,7 +310,12 @@ class EditNode extends EditRecord
true => 'danger',
]),
Grid::make()
->columns(['default' => 1, 'sm' => 1, 'md' => 3, 'lg' => 6])
->columns([
'default' => 1,
'sm' => 1,
'md' => 3,
'lg' => 6,
])
->columnSpanFull()
->schema([
ToggleButtons::make('unlimited_mem')
@@ -293,14 +332,24 @@ class EditNode extends EditRecord
true => 'primary',
false => 'warning',
])
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2]),
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 2,
]),
TextInput::make('memory')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_mem'))
->label('Memory Limit')->inlineLabel()
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
->required()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 2,
])
->numeric()
->minValue(0),
TextInput::make('memory_overallocate')
@@ -310,14 +359,24 @@ class EditNode extends EditRecord
->hidden(fn (Get $get) => $get('unlimited_mem'))
->hintIcon('tabler-question-mark')
->hintIconTooltip('The % allowable to go over the set limit.')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 2,
])
->numeric()
->minValue(-1)
->maxValue(100)
->suffix('%'),
]),
Grid::make()
->columns(['default' => 1, 'sm' => 1, 'md' => 3, 'lg' => 6])
->columns([
'default' => 1,
'sm' => 1,
'md' => 3,
'lg' => 6,
])
->schema([
ToggleButtons::make('unlimited_disk')
->label('Disk')->inlineLabel()->inline()
@@ -333,14 +392,24 @@ class EditNode extends EditRecord
true => 'primary',
false => 'warning',
])
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2]),
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 2,
]),
TextInput::make('disk')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_disk'))
->label('Disk Limit')->inlineLabel()
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
->required()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 2,
])
->numeric()
->minValue(0),
TextInput::make('disk_overallocate')
@@ -349,7 +418,12 @@ class EditNode extends EditRecord
->label('Overallocate')->inlineLabel()
->hintIcon('tabler-question-mark')
->hintIconTooltip('The % allowable to go over the set limit.')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 2,
])
->required()
->numeric()
->minValue(-1)
@@ -412,19 +486,61 @@ class EditNode extends EditRecord
->rows(19)
->hintAction(CopyAction::make())
->columnSpanFull(),
Forms\Components\Actions::make([
Forms\Components\Actions\Action::make('resetKey')
->label('Reset Daemon Token')
->color('danger')
->requiresConfirmation()
->modalHeading('Reset Daemon Token?')
->modalDescription('Resetting the daemon token will void any request coming from the old token. This token is used for all sensitive operations on the daemon including server creation and deletion. We suggest changing this token regularly for security.')
->action(function (NodeUpdateService $nodeUpdateService, Node $node) {
$nodeUpdateService->handle($node, [], true);
Notification::make()->success()->title('Daemon Key Reset')->send();
$this->fillForm();
}),
]),
Grid::make()
->columns()
->schema([
FormActions::make([
FormActions\Action::make('autoDeploy')
->label('Auto Deploy Command')
->color('primary')
->modalHeading('Auto Deploy Command')
->icon('tabler-rocket')
->modalSubmitAction(false)
->modalCancelAction(false)
->modalFooterActionsAlignment(Alignment::Center)
->form([
ToggleButtons::make('docker')
->label('Type')
->live()
->helperText('Choose between Standalone and Docker install.')
->inline()
->default(false)
->afterStateUpdated(fn (bool $state, NodeAutoDeployService $service, Node $node, Set $set) => $set('generatedToken', $service->handle(request(), $node, $state)))
->options([
false => 'Standalone',
true => 'Docker',
])
->colors([
false => 'primary',
true => 'success',
])
->columnSpan(1),
Textarea::make('generatedToken')
->label('To auto-configure your node run the following command:')
->readOnly()
->autosize()
->hintAction(fn (string $state) => CopyAction::make()->copyable($state))
->formatStateUsing(fn (NodeAutoDeployService $service, Node $node, Set $set, Get $get) => $set('generatedToken', $service->handle(request(), $node, $get('docker')))),
])
->mountUsing(function (Forms\Form $form) {
Notification::make()->success()->title('Autodeploy Generated')->send();
$form->fill();
}),
])->fullWidth(),
FormActions::make([
FormActions\Action::make('resetKey')
->label('Reset Daemon Token')
->color('danger')
->requiresConfirmation()
->modalHeading('Reset Daemon Token?')
->modalDescription('Resetting the daemon token will void any request coming from the old token. This token is used for all sensitive operations on the daemon including server creation and deletion. We suggest changing this token regularly for security.')
->action(function (NodeUpdateService $nodeUpdateService, Node $node) {
$nodeUpdateService->handle($node, [], true);
Notification::make()->success()->title('Daemon Key Reset')->send();
$this->fillForm();
}),
])->fullWidth(),
]),
]),
]),
]);

View File

@@ -6,9 +6,7 @@ use App\Filament\Resources\NodeResource;
use App\Models\Node;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\BulkActionGroup;
use Filament\Tables\Actions\CreateAction;
use Filament\Tables\Actions\DeleteBulkAction;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
@@ -83,12 +81,6 @@ class ListNodes extends ListRecords
->actions([
EditAction::make(),
])
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make()
->authorize(fn () => auth()->user()->can('delete node')),
]),
])
->emptyStateIcon('tabler-server-2')
->emptyStateDescription('')
->emptyStateHeading('No Nodes')

View File

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

View File

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

View File

@@ -627,14 +627,24 @@ class CreateServer extends CreateRecord
->minValue(0)
->helperText('100% equals one CPU core.'),
]),
]),
Fieldset::make('Advanced Limits')
->columnSpan(6)
->columns([
'default' => 1,
'sm' => 2,
'md' => 3,
'lg' => 3,
])
->schema([
Grid::make()
->columns(4)
->columnSpanFull()
->schema([
ToggleButtons::make('swap_support')
->live()
->label('Enable Swap Memory')
->label('Swap Memory')
->inlineLabel()
->inline()
->columnSpan(2)
@@ -681,6 +691,36 @@ class CreateServer extends CreateRecord
->label('Block IO Proportion')
->default(500),
Grid::make()
->columns(4)
->columnSpanFull()
->schema([
ToggleButtons::make('cpu_pinning')
->label('CPU Pinning')->inlineLabel()->inline()
->default(false)
->afterStateUpdated(fn (Set $set) => $set('threads', []))
->live()
->options([
false => 'Disabled',
true => 'Enabled',
])
->colors([
false => 'success',
true => 'warning',
])
->columnSpan(2),
TagsInput::make('threads')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => !$get('cpu_pinning'))
->label('Pinned Threads')->inlineLabel()
->required(fn (Get $get) => $get('cpu_pinning'))
->columnSpan(2)
->separator()
->splitKeys([','])
->placeholder('Add pinned thread, e.g. 0 or 2-4'),
]),
Grid::make()
->columns(4)
->columnSpanFull()
@@ -747,6 +787,7 @@ class CreateServer extends CreateRecord
->schema([
Select::make('select_image')
->label('Image Name')
->live()
->afterStateUpdated(fn (Set $set, $state) => $set('image', $state))
->options(function ($state, Get $get, Set $set) {
$egg = Egg::query()->find($get('egg_id'));
@@ -771,7 +812,7 @@ class CreateServer extends CreateRecord
TextInput::make('image')
->label('Image')
->debounce(500)
->required()
->afterStateUpdated(function ($state, Get $get, Set $set) {
$egg = Egg::query()->find($get('egg_id'));
$images = $egg->docker_images ?? [];

View File

@@ -5,16 +5,19 @@ namespace App\Filament\Resources\ServerResource\Pages;
use App\Enums\ContainerStatus;
use App\Enums\ServerState;
use App\Filament\Resources\ServerResource;
use App\Http\Controllers\Admin\ServersController;
use App\Filament\Resources\ServerResource\RelationManagers\AllocationsRelationManager;
use App\Models\Database;
use App\Models\Egg;
use App\Models\Mount;
use App\Models\Server;
use App\Models\ServerVariable;
use App\Services\Databases\DatabaseManagementService;
use App\Services\Databases\DatabasePasswordService;
use App\Services\Servers\RandomWordService;
use App\Services\Servers\ReinstallServerService;
use App\Services\Servers\ServerDeletionService;
use App\Services\Servers\SuspensionService;
use App\Services\Servers\ToggleInstallService;
use App\Services\Servers\TransferServerService;
use Closure;
use Exception;
@@ -24,10 +27,13 @@ use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\KeyValue;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
@@ -263,14 +269,23 @@ class EditServer extends EditRecord
->numeric()
->minValue(0),
]),
]),
Fieldset::make('Advanced Limits')
->columns([
'default' => 1,
'sm' => 2,
'md' => 3,
'lg' => 3,
])
->schema([
Grid::make()
->columns(4)
->columnSpanFull()
->schema([
ToggleButtons::make('swap_support')
->live()
->label('Enable Swap Memory')->inlineLabel()->inline()
->label('Swap Memory')->inlineLabel()->inline()
->columnSpan(2)
->afterStateUpdated(function ($state, Set $set) {
$value = match ($state) {
@@ -315,10 +330,41 @@ class EditServer extends EditRecord
->integer(),
]),
Forms\Components\Hidden::make('io')
Hidden::make('io')
->helperText('The IO performance relative to other running containers')
->label('Block IO Proportion'),
Grid::make()
->columns(4)
->columnSpanFull()
->schema([
ToggleButtons::make('cpu_pinning')
->label('CPU Pinning')->inlineLabel()->inline()
->default(false)
->afterStateUpdated(fn (Set $set) => $set('threads', []))
->formatStateUsing(fn (Get $get) => !empty($get('threads')))
->live()
->options([
false => 'Disabled',
true => 'Enabled',
])
->colors([
false => 'success',
true => 'warning',
])
->columnSpan(2),
TagsInput::make('threads')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => !$get('cpu_pinning'))
->label('Pinned Threads')->inlineLabel()
->required(fn (Get $get) => $get('cpu_pinning'))
->columnSpan(2)
->separator()
->splitKeys([','])
->placeholder('Add pinned thread, e.g. 0 or 2-4'),
]),
Grid::make()
->columns(4)
->columnSpanFull()
@@ -375,6 +421,7 @@ class EditServer extends EditRecord
->schema([
Select::make('select_image')
->label('Image Name')
->live()
->afterStateUpdated(fn (Set $set, $state) => $set('image', $state))
->options(function ($state, Get $get, Set $set) {
$egg = Egg::query()->find($get('egg_id'));
@@ -394,7 +441,7 @@ class EditServer extends EditRecord
TextInput::make('image')
->label('Image')
->debounce(500)
->required()
->afterStateUpdated(function ($state, Get $get, Set $set) {
$egg = Egg::query()->find($get('egg_id'));
$images = $egg->docker_images ?? [];
@@ -408,7 +455,7 @@ class EditServer extends EditRecord
->placeholder('Enter a custom Image')
->columnSpan(2),
Forms\Components\KeyValue::make('docker_labels')
KeyValue::make('docker_labels')
->label('Container Labels')
->keyLabel('Label Name')
->valueLabel('Label Description')
@@ -554,8 +601,8 @@ class EditServer extends EditRecord
->schema([
CheckboxList::make('mounts')
->relationship('mounts')
->options(fn (Server $server) => $server->node->mounts->mapWithKeys(fn ($mount) => [$mount->id => $mount->name]))
->descriptions(fn (Server $server) => $server->node->mounts->mapWithKeys(fn ($mount) => [$mount->id => "$mount->source -> $mount->target"]))
->options(fn (Server $server) => $server->node->mounts->filter(fn (Mount $mount) => $mount->eggs->contains($server->egg))->mapWithKeys(fn (Mount $mount) => [$mount->id => $mount->name]))
->descriptions(fn (Server $server) => $server->node->mounts->mapWithKeys(fn (Mount $mount) => [$mount->id => "$mount->source -> $mount->target"]))
->label('Mounts')
->helperText(fn (Server $server) => $server->node->mounts->isNotEmpty() ? '' : 'No Mounts exist for this Node')
->columnSpanFull(),
@@ -631,8 +678,8 @@ class EditServer extends EditRecord
Action::make('toggleInstall')
->label('Toggle Install Status')
->disabled(fn (Server $server) => $server->isSuspended())
->action(function (ServersController $serversController, Server $server) {
$serversController->toggleInstall($server);
->action(function (ToggleInstallService $service, Server $server) {
$service->handle($server);
$this->refreshFormData(['status', 'docker']);
}),
@@ -718,7 +765,7 @@ class EditServer extends EditRecord
->modalHeading('Are you sure you want to reinstall this server?')
->modalDescription('!! This can result in unrecoverable data loss !!')
->disabled(fn (Server $server) => $server->isSuspended())
->action(fn (ServersController $serversController, Server $server) => $serversController->reinstallServer($server)),
->action(fn (ReinstallServerService $service, Server $server) => $service->handle($server)),
])->fullWidth(),
ToggleButtons::make('')
->hint('This will reinstall the server with the assigned egg install script.'),
@@ -784,7 +831,7 @@ class EditServer extends EditRecord
public function getRelationManagers(): array
{
return [
ServerResource\RelationManagers\AllocationsRelationManager::class,
AllocationsRelationManager::class,
];
}

View File

@@ -40,6 +40,7 @@ class AllocationsRelationManager extends RelationManager
public function table(Table $table): Table
{
return $table
->selectCurrentPageOnly()
->recordTitleAttribute('ip')
->recordTitle(fn (Allocation $allocation) => "$allocation->ip:$allocation->port")
->checkIfRecordIsSelectableUsing(fn (Allocation $record) => $record->id !== $this->getOwnerRecord()->allocation_id)
@@ -57,13 +58,13 @@ class AllocationsRelationManager extends RelationManager
true => 'warning',
default => 'gray',
})
->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]))
->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]) && $this->deselectAllTableRecords())
->default(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id)
->label('Primary'),
])
->actions([
Action::make('make-primary')
->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]))
->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]) && $this->deselectAllTableRecords())
->label(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id ? '' : 'Make Primary'),
])
->headerActions([
@@ -149,7 +150,7 @@ class AllocationsRelationManager extends RelationManager
->multiple()
->associateAnother(false)
->preloadRecordSelect()
->recordSelectOptionsQuery(fn ($query) => $query->whereBelongsTo($this->getOwnerRecord()->node))
->recordSelectOptionsQuery(fn ($query) => $query->whereBelongsTo($this->getOwnerRecord()->node)->whereNull('server_id'))
->label('Add Allocation'),
])
->bulkActions([

View File

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

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\WebhookResource\Pages;
use App\Models\WebhookConfiguration;
use Filament\Resources\Resource;
class WebhookResource extends Resource
{
protected static ?string $model = WebhookConfiguration::class;
protected static ?string $navigationIcon = 'tabler-webhook';
protected static ?string $navigationGroup = 'Advanced';
protected static ?string $label = 'Webhooks';
public static function getNavigationBadge(): ?string
{
return static::getModel()::count() ?: null;
}
public static function getPages(): array
{
return [
'index' => Pages\ListWebhookConfigurations::route('/'),
'create' => Pages\CreateWebhookConfiguration::route('/create'),
'edit' => Pages\EditWebhookConfiguration::route('/{record}/edit'),
];
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -40,7 +40,7 @@ class Utilities
{
return Carbon::instance((new CronExpression(
sprintf('%s %s %s %s %s', $minute, $hour, $dayOfMonth, $month, $dayOfWeek)
))->getNextRunDate());
))->getNextRunDate(now('UTC')));
}
public static function checked(string $name, mixed $default): string

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -29,7 +29,7 @@ class ApiKeyController extends ClientApiController
*/
public function store(StoreApiKeyRequest $request): array
{
if ($request->user()->apiKeys->count() >= 25) {
if ($request->user()->apiKeys->count() >= config('panel.api.key_limit')) {
throw new DisplayException('You have reached the account limit for number of API keys.');
}

View File

@@ -3,6 +3,7 @@
namespace App\Http\Controllers\Api\Client\Servers;
use App\Models\User;
use App\Notifications\RemovedFromServer;
use Illuminate\Http\Request;
use App\Models\Server;
use Illuminate\Http\JsonResponse;
@@ -144,6 +145,11 @@ class SubuserController extends ClientApiController
$log->transaction(function ($instance) use ($server, $subuser) {
$subuser->delete();
$subuser->user->notify(new RemovedFromServer([
'user' => $subuser->user->name_first,
'name' => $subuser->server->name,
]));
try {
$this->serverRepository->setServer($server)->revokeUserJTI($subuser->user_id);
} catch (DaemonConnectionException $exception) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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