Update phpstan to latest (#804)

* Fix these

* Update phpstan

* Transform these into their identifiers instead

* Fix custom rule

* License is wrong

* Update these

* Pint fixes

* Fix this

* Consolidate these

* Never supported PHP 7

* Better evaluation

* Fixes

* Don’t need ignore

* Replace trait with service

* Subusers are simply the many to many relationship between Servers and Users

* Adjust to remove ignores

* Use new query builder instead!

* wip

* Update composer

* Quick fixes

* Use realtime facade

* Small fixes

* Convert to static to avoid new

* Update to statics

* Don’t modify protected properties directly

* Run pint

* Change to correct method

* Give up and use the facade

* Make sure this route is available

* Filament hasn’t been loaded yet

* This can be readonly

* Typehint

* These are no longer used

* Quick fixes

* Need doc block help

* Always true

* We use caddy with docker

* Pint

* Fix phpstan issues

* Remove unused import

---------

Co-authored-by: MartinOscar <40749467+RMartinOscar@users.noreply.github.com>
This commit is contained in:
Lance Pioch 2025-01-16 14:53:50 -05:00 committed by GitHub
parent 02c4eb19f0
commit ad1a9cd33f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
56 changed files with 566 additions and 1324 deletions

View File

@ -1,75 +0,0 @@
# If using Ubuntu this file should be placed in:
# /etc/nginx/sites-available/
#
# If using CentOS this file should be placed in:
# /etc/nginx/conf.d/
#
# The MIT License (MIT)
#
# Pterodactyl®
# Copyright © Dane Everitt <dane@daneeveritt.com> and contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
server {
listen 80;
server_name _;
root /app/public;
index index.html index.htm index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
access_log off;
error_log /var/log/nginx/panel.app-error.log error;
# allow larger file uploads and longer script runtimes
client_max_body_size 100m;
client_body_timeout 120s;
sendfile off;
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
# the fastcgi_pass path needs to be changed accordingly when using CentOS
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param PHP_VALUE "upload_max_filesize = 100M \n post_max_size=100M";
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTP_PROXY "";
fastcgi_intercept_errors off;
fastcgi_buffer_size 16k;
fastcgi_buffers 4 16k;
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
}
location ~ /\.ht {
deny all;
}
}

View File

@ -1,70 +0,0 @@
# If using Ubuntu this file should be placed in:
# /etc/nginx/sites-available/
#
server {
listen 80;
server_name <domain>;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name <domain>;
root /app/public;
index index.php;
access_log /var/log/nginx/panel.app-access.log;
error_log /var/log/nginx/panel.app-error.log error;
# allow larger file uploads and longer script runtimes
client_max_body_size 100m;
client_body_timeout 120s;
sendfile off;
# strengthen ssl security
ssl_certificate /etc/letsencrypt/live/<domain>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<domain>/privkey.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:ECDHE-RSA-AES128-GCM-SHA256:AES256+EECDH:DHE-RSA-AES128-GCM-SHA256:AES256+EDH:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
# See the link below for more SSL information:
# https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
#
# ssl_dhparam /etc/ssl/certs/dhparam.pem;
# Add headers to serve security related headers
add_header Strict-Transport-Security "max-age=15768000; preload;";
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header X-Robots-Tag none;
add_header Content-Security-Policy "frame-ancestors 'self'";
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param PHP_VALUE "upload_max_filesize = 100M \n post_max_size=100M";
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTP_PROXY "";
fastcgi_intercept_errors off;
fastcgi_buffer_size 16k;
fastcgi_buffers 4 16k;
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
include /etc/nginx/fastcgi_params;
}
location ~ /\.ht {
deny all;
}
}

View File

@ -1,16 +0,0 @@
[www]
user = nginx
group = nginx
listen = 127.0.0.1:9000
listen.owner = nginx
listen.group = nginx
listen.mode = 0750
pm = ondemand
pm.max_children = 9
pm.process_idle_timeout = 10s
pm.max_requests = 200
clear_env = no

View File

@ -6,6 +6,7 @@ use Illuminate\Console\Command;
use App\Models\Schedule; use App\Models\Schedule;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use App\Services\Schedules\ProcessScheduleService; use App\Services\Schedules\ProcessScheduleService;
use Throwable;
class ProcessRunnableCommand extends Command class ProcessRunnableCommand extends Command
{ {
@ -13,10 +14,7 @@ class ProcessRunnableCommand extends Command
protected $description = 'Process schedules in the database and determine which are ready to run.'; protected $description = 'Process schedules in the database and determine which are ready to run.';
/** public function handle(ProcessScheduleService $processScheduleService): int
* Handle command execution.
*/
public function handle(): int
{ {
$schedules = Schedule::query() $schedules = Schedule::query()
->with('tasks') ->with('tasks')
@ -35,7 +33,7 @@ class ProcessRunnableCommand extends Command
$bar = $this->output->createProgressBar(count($schedules)); $bar = $this->output->createProgressBar(count($schedules));
foreach ($schedules as $schedule) { foreach ($schedules as $schedule) {
$bar->clear(); $bar->clear();
$this->processSchedule($schedule); $this->processSchedule($processScheduleService, $schedule);
$bar->advance(); $bar->advance();
$bar->display(); $bar->display();
} }
@ -50,20 +48,20 @@ class ProcessRunnableCommand extends Command
* never throw an exception out, otherwise you'll end up killing the entire run group causing * never throw an exception out, otherwise you'll end up killing the entire run group causing
* any other schedules to not process correctly. * any other schedules to not process correctly.
*/ */
protected function processSchedule(Schedule $schedule): void protected function processSchedule(ProcessScheduleService $processScheduleService, Schedule $schedule): void
{ {
if ($schedule->tasks->isEmpty()) { if ($schedule->tasks->isEmpty()) {
return; return;
} }
try { try {
$this->getLaravel()->make(ProcessScheduleService::class)->handle($schedule); $processScheduleService->handle($schedule);
$this->line(trans('command/messages.schedule.output_line', [ $this->line(trans('command/messages.schedule.output_line', [
'schedule' => $schedule->name, 'schedule' => $schedule->name,
'id' => $schedule->id, 'id' => $schedule->id,
])); ]));
} catch (\Throwable|\Exception $exception) { } catch (Throwable $exception) {
logger()->error($exception, ['schedule_id' => $schedule->id]); logger()->error($exception, ['schedule_id' => $schedule->id]);
$this->error(__('commands.schedule.process.no_tasks') . " #$schedule->id: " . $exception->getMessage()); $this->error(__('commands.schedule.process.no_tasks') . " #$schedule->id: " . $exception->getMessage());

View File

@ -19,26 +19,13 @@ class BulkPowerActionCommand extends Command
protected $description = 'Perform bulk power management on large groupings of servers or nodes at once.'; protected $description = 'Perform bulk power management on large groupings of servers or nodes at once.';
/** public function handle(DaemonPowerRepository $powerRepository, ValidatorFactory $validator): void
* BulkPowerActionCommand constructor.
*/
public function __construct(private DaemonPowerRepository $powerRepository, private ValidatorFactory $validator)
{
parent::__construct();
}
/**
* Handle the bulk power request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function handle(): void
{ {
$action = $this->argument('action'); $action = $this->argument('action');
$nodes = empty($this->option('nodes')) ? [] : explode(',', $this->option('nodes')); $nodes = empty($this->option('nodes')) ? [] : explode(',', $this->option('nodes'));
$servers = empty($this->option('servers')) ? [] : explode(',', $this->option('servers')); $servers = empty($this->option('servers')) ? [] : explode(',', $this->option('servers'));
$validator = $this->validator->make([ $validator = $validator->make([
'action' => $action, 'action' => $action,
'nodes' => $nodes, 'nodes' => $nodes,
'servers' => $servers, 'servers' => $servers,
@ -64,11 +51,14 @@ class BulkPowerActionCommand extends Command
} }
$bar = $this->output->createProgressBar($count); $bar = $this->output->createProgressBar($count);
$powerRepository = $this->powerRepository;
// @phpstan-ignore-next-line $this->getQueryBuilder($servers, $nodes)->get()->each(function ($server, int $index) use ($action, $powerRepository, &$bar): mixed {
$this->getQueryBuilder($servers, $nodes)->each(function (Server $server) use ($action, $powerRepository, &$bar) {
$bar->clear(); $bar->clear();
if (!$server instanceof Server) {
return null;
}
try { try {
$powerRepository->setServer($server)->send($action); $powerRepository->setServer($server)->send($action);
} catch (Exception $exception) { } catch (Exception $exception) {
@ -82,6 +72,8 @@ class BulkPowerActionCommand extends Command
$bar->advance(); $bar->advance();
$bar->display(); $bar->display();
return null;
}); });
$this->line(''); $this->line('');

View File

@ -39,10 +39,6 @@ class UpgradeCommand extends Command
$this->line($this->getUrl()); $this->line($this->getUrl());
} }
if (version_compare(PHP_VERSION, '7.4.0') < 0) {
$this->error(__('commands.upgrade.php_version') . ' [' . PHP_VERSION . '].');
}
$user = 'www-data'; $user = 'www-data';
$group = 'www-data'; $group = 'www-data';
if ($this->input->isInteractive()) { if ($this->input->isInteractive()) {

View File

@ -0,0 +1,24 @@
<?php
namespace App\Eloquent;
use Illuminate\Database\Eloquent\Builder;
/**
* @template TModel of \Illuminate\Database\Eloquent\Model
*
* @extends Builder<TModel>
*/
class BackupQueryBuilder extends Builder
{
public function nonFailed(): self
{
$this->where(function (Builder $query) {
$query
->whereNull('completed_at')
->orWhere('is_successful', true);
});
return $this;
}
}

View File

@ -91,7 +91,7 @@ enum EditorLanguages: string implements HasLabel
case yaml = 'yaml'; case yaml = 'yaml';
case json = 'json'; case json = 'json';
public function getLabel(): ?string public function getLabel(): string
{ {
return $this->name; return $this->name;
} }

View File

@ -20,6 +20,7 @@ use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Mailer\Exception\TransportException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Throwable;
class Handler extends ExceptionHandler class Handler extends ExceptionHandler
{ {
@ -179,10 +180,7 @@ class Handler extends ExceptionHandler
return response()->json(['errors' => $errors], $exception->status); return response()->json(['errors' => $errors], $exception->status);
} }
/** public static function exceptionToArray(Throwable $e, array $override = []): array
* Return the exception as a JSONAPI representation for use on API requests.
*/
protected function convertExceptionToArray(\Throwable $e, array $override = []): array
{ {
$match = self::$exceptionResponseCodes[get_class($e)] ?? null; $match = self::$exceptionResponseCodes[get_class($e)] ?? null;
@ -214,7 +212,7 @@ class Handler extends ExceptionHandler
'trace' => Collection::make($e->getTrace()) 'trace' => Collection::make($e->getTrace())
->map(fn ($trace) => Arr::except($trace, ['args'])) ->map(fn ($trace) => Arr::except($trace, ['args']))
->all(), ->all(),
'previous' => Collection::make($this->extractPrevious($e)) 'previous' => Collection::make(self::extractPrevious($e))
->map(fn ($exception) => $exception->getTrace()) ->map(fn ($exception) => $exception->getTrace())
->map(fn ($trace) => Arr::except($trace, ['args'])) ->map(fn ($trace) => Arr::except($trace, ['args']))
->all(), ->all(),
@ -225,6 +223,14 @@ class Handler extends ExceptionHandler
return ['errors' => [array_merge($error, $override)]]; return ['errors' => [array_merge($error, $override)]];
} }
/**
* Return the exception as a JSONAPI representation for use on API requests.
*/
protected function convertExceptionToArray(Throwable $e, array $override = []): array
{
return self::exceptionToArray($e, $override);
}
/** /**
* Return an array of exceptions that should not be reported. * Return an array of exceptions that should not be reported.
*/ */
@ -251,15 +257,12 @@ class Handler extends ExceptionHandler
* Extracts all the previous exceptions that lead to the one passed into this * Extracts all the previous exceptions that lead to the one passed into this
* function being thrown. * function being thrown.
* *
* @return \Throwable[] * @return Throwable[]
*/ */
protected function extractPrevious(\Throwable $e): array public static function extractPrevious(Throwable $e): array
{ {
$previous = []; $previous = [];
while ($value = $e->getPrevious()) { while ($value = $e->getPrevious()) {
if (!$value instanceof \Throwable) {
break;
}
$previous[] = $value; $previous[] = $value;
$e = $value; $e = $value;
} }
@ -273,7 +276,6 @@ class Handler extends ExceptionHandler
*/ */
public static function toArray(\Throwable $e): array public static function toArray(\Throwable $e): array
{ {
// @phpstan-ignore-next-line return self::exceptionToArray($e);
return (new self(app()))->convertExceptionToArray($e);
} }
} }

View File

@ -1,28 +1,5 @@
<?php <?php
/* The MIT License (MIT)
Pterodactyl®
Copyright © Dane Everitt <dane@daneeveritt.com> and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. */
namespace App\Extensions\Filesystem; namespace App\Extensions\Filesystem;
use Aws\S3\S3ClientInterface; use Aws\S3\S3ClientInterface;

View File

@ -5,6 +5,7 @@ namespace App\Filament\Admin\Resources\UserResource\Pages;
use App\Filament\Admin\Resources\UserResource; use App\Filament\Admin\Resources\UserResource;
use App\Models\Role; use App\Models\Role;
use App\Models\User; use App\Models\User;
use App\Services\Helpers\LanguageService;
use Filament\Actions\DeleteAction; use Filament\Actions\DeleteAction;
use Filament\Forms\Components\CheckboxList; use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Hidden;
@ -40,7 +41,7 @@ class EditUser extends EditRecord
->required() ->required()
->hidden() ->hidden()
->default('en') ->default('en')
->options(fn (User $user) => $user->getAvailableLanguages()), ->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages()),
Hidden::make('skipValidation') Hidden::make('skipValidation')
->default(true), ->default(true),
CheckboxList::make('roles') CheckboxList::make('roles')

View File

@ -34,7 +34,7 @@ class ServersRelationManager extends RelationManager
->label('Suspend All Servers') ->label('Suspend All Servers')
->color('warning') ->color('warning')
->action(function (SuspensionService $suspensionService) use ($user) { ->action(function (SuspensionService $suspensionService) use ($user) {
collect($user->servers()->get())->filter(fn ($server) => !$server->isSuspended()) collect($user->servers)->filter(fn ($server) => !$server->isSuspended())
->each(fn ($server) => $suspensionService->handle($server, SuspendAction::Suspend)); ->each(fn ($server) => $suspensionService->handle($server, SuspendAction::Suspend));
}), }),
Actions\Action::make('toggleUnsuspend') Actions\Action::make('toggleUnsuspend')

View File

@ -10,7 +10,6 @@ use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\Tabs\Tab; use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
class ImportEggAction extends Action class ImportEggAction extends Action
{ {
@ -55,7 +54,6 @@ class ImportEggAction extends Action
$this->action(function (array $data, EggImporterService $eggImportService): void { $this->action(function (array $data, EggImporterService $eggImportService): void {
try { try {
if (!empty($data['egg'])) { if (!empty($data['egg'])) {
/** @var TemporaryUploadedFile[] $eggFile */
$eggFile = $data['egg']; $eggFile = $data['egg'];
foreach ($eggFile as $file) { foreach ($eggFile as $file) {

View File

@ -10,7 +10,6 @@ use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Tables\Actions\Action; use Filament\Tables\Actions\Action;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
class ImportEggAction extends Action class ImportEggAction extends Action
{ {
@ -55,7 +54,6 @@ class ImportEggAction extends Action
$this->action(function (array $data, EggImporterService $eggImportService): void { $this->action(function (array $data, EggImporterService $eggImportService): void {
try { try {
if (!empty($data['egg'])) { if (!empty($data['egg'])) {
/** @var TemporaryUploadedFile[] $eggFile */
$eggFile = $data['egg']; $eggFile = $data['egg'];
foreach ($eggFile as $file) { foreach ($eggFile as $file) {

View File

@ -23,6 +23,6 @@ class DateTimeColumn extends TextColumn
public function getTimezone(): string public function getTimezone(): string
{ {
return auth()->user()?->timezone ?? config('app.timezone', 'UTC'); return auth()->user()->timezone ?? config('app.timezone', 'UTC');
} }
} }

View File

@ -8,6 +8,7 @@ use App\Facades\Activity;
use App\Models\ActivityLog; use App\Models\ActivityLog;
use App\Models\ApiKey; use App\Models\ApiKey;
use App\Models\User; use App\Models\User;
use App\Services\Helpers\LanguageService;
use App\Services\Users\ToggleTwoFactorService; use App\Services\Users\ToggleTwoFactorService;
use App\Services\Users\TwoFactorSetupService; use App\Services\Users\TwoFactorSetupService;
use App\Services\Users\UserUpdateService; use App\Services\Users\UserUpdateService;
@ -115,13 +116,12 @@ class EditProfile extends BaseEditProfile
->prefixIcon('tabler-flag') ->prefixIcon('tabler-flag')
->live() ->live()
->default('en') ->default('en')
->helperText(fn (User $user, $state) => new HtmlString($user->isLanguageTranslated($state) ? '' : " ->helperText(fn ($state, LanguageService $languageService) => new HtmlString($languageService->isLanguageTranslated($state) ? '' : "
Your language ($state) has not been translated yet! Your language ($state) has not been translated yet!
But never fear, you can help fix that by But never fear, you can help fix that by
<a style='color: rgb(56, 189, 248)' href='https://crowdin.com/project/pelican-dev'>contributing directly here</a>. <a style='color: rgb(56, 189, 248)' href='https://crowdin.com/project/pelican-dev'>contributing directly here</a>.
") "))
) ->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages()),
->options(fn (User $user) => $user->getAvailableLanguages()),
]), ]),
Tab::make('OAuth') Tab::make('OAuth')
@ -208,29 +208,17 @@ class EditProfile extends BaseEditProfile
'addLogoSpace' => true, 'addLogoSpace' => true,
'logoSpaceWidth' => 13, 'logoSpaceWidth' => 13,
'logoSpaceHeight' => 13, 'logoSpaceHeight' => 13,
]); 'version' => Version::AUTO,
// 'outputInterface' => QRSvgWithLogo::class,
// https://github.com/chillerlan/php-qrcode/blob/main/examples/svgWithLogo.php 'outputBase64' => false,
'eccLevel' => EccLevel::H, // ECC level H is necessary when using logos
// QROptions 'addQuietzone' => true,
// @phpstan-ignore property.protected // 'drawLightModules' => true,
$options->version = Version::AUTO; 'connectPaths' => true,
// $options->outputInterface = QRSvgWithLogo::class; 'drawCircularModules' => true,
// @phpstan-ignore property.protected // 'circleRadius' => 0.45,
$options->outputBase64 = false; 'svgDefs' => '
// @phpstan-ignore property.protected <linearGradient id="gradient" x1="100%" y2="100%">
$options->eccLevel = EccLevel::H; // ECC level H is necessary when using logos
// @phpstan-ignore property.protected
$options->addQuietzone = true;
// $options->drawLightModules = true;
// @phpstan-ignore property.protected
$options->connectPaths = true;
// @phpstan-ignore property.protected
$options->drawCircularModules = true;
// $options->circleRadius = 0.45;
// @phpstan-ignore property.protected
$options->svgDefs = '<linearGradient id="gradient" x1="100%" y2="100%">
<stop stop-color="#7dd4fc" offset="0"/> <stop stop-color="#7dd4fc" offset="0"/>
<stop stop-color="#38bdf8" offset="0.5"/> <stop stop-color="#38bdf8" offset="0.5"/>
<stop stop-color="#0369a1" offset="1"/> <stop stop-color="#0369a1" offset="1"/>
@ -238,7 +226,11 @@ class EditProfile extends BaseEditProfile
<style><![CDATA[ <style><![CDATA[
.dark{fill: url(#gradient);} .dark{fill: url(#gradient);}
.light{fill: #000;} .light{fill: #000;}
]]></style>'; ]]></style>
',
]);
// https://github.com/chillerlan/php-qrcode/blob/main/examples/svgWithLogo.php
$image = (new QRCode($options))->render($url); $image = (new QRCode($options))->render($url);

View File

@ -6,7 +6,6 @@ use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction;
use App\Filament\Components\Tables\Columns\DateTimeColumn; use App\Filament\Components\Tables\Columns\DateTimeColumn;
use App\Filament\Server\Resources\DatabaseResource; use App\Filament\Server\Resources\DatabaseResource;
use App\Models\Database; use App\Models\Database;
use App\Models\DatabaseHost;
use App\Models\Permission; use App\Models\Permission;
use App\Models\Server; use App\Models\Server;
use App\Services\Databases\DatabaseManagementService; use App\Services\Databases\DatabaseManagementService;
@ -99,7 +98,7 @@ class ListDatabases extends ListRecords
->columnSpan(2) ->columnSpan(2)
->required() ->required()
->placeholder('Select Database Host') ->placeholder('Select Database Host')
->options(fn () => $server->node->databaseHosts->mapWithKeys(fn (DatabaseHost $databaseHost) => [$databaseHost->id => $databaseHost->name])), ->options(fn () => $server->node->databaseHosts->mapWithKeys(fn ($databaseHost) => [$databaseHost->id => $databaseHost->name])),
TextInput::make('database') TextInput::make('database')
->columnSpan(1) ->columnSpan(1)
->label('Database Name') ->label('Database Name')

View File

@ -355,8 +355,7 @@ class ListFiles extends ListRecords
->action(function (Collection $files, $data, DaemonFileRepository $fileRepository) use ($server) { ->action(function (Collection $files, $data, DaemonFileRepository $fileRepository) use ($server) {
$location = resolve_path(join_paths($this->path, $data['location'])); $location = resolve_path(join_paths($this->path, $data['location']));
// @phpstan-ignore-next-line $files = $files->map(fn ($file) => ['to' => $location, 'from' => $file['name']])->toArray();
$files = $files->map(fn ($file) => ['to' => $location, 'from' => $file->name])->toArray();
$fileRepository $fileRepository
->setServer($server) ->setServer($server)
->renameFiles($this->path, $files); ->renameFiles($this->path, $files);
@ -374,8 +373,7 @@ class ListFiles extends ListRecords
BulkAction::make('archive') BulkAction::make('archive')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server)) ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
->action(function (Collection $files, DaemonFileRepository $fileRepository) use ($server) { ->action(function (Collection $files, DaemonFileRepository $fileRepository) use ($server) {
// @phpstan-ignore-next-line $files = $files->map(fn ($file) => $file['name'])->toArray();
$files = $files->map(fn ($file) => $file->name)->toArray();
$fileRepository $fileRepository
->setServer($server) ->setServer($server)
@ -396,8 +394,7 @@ class ListFiles extends ListRecords
DeleteBulkAction::make() DeleteBulkAction::make()
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server)) ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server))
->action(function (Collection $files, DaemonFileRepository $fileRepository) use ($server) { ->action(function (Collection $files, DaemonFileRepository $fileRepository) use ($server) {
// @phpstan-ignore-next-line $files = $files->map(fn ($file) => $file['name'])->toArray();
$files = $files->map(fn ($file) => $file->name)->toArray();
$fileRepository $fileRepository
->setServer($server) ->setServer($server)
->deleteFiles($this->path, $files); ->deleteFiles($this->path, $files);

View File

@ -30,16 +30,25 @@ class ServerConsole extends Widget
public string $input = ''; public string $input = '';
private GetUserPermissionsService $getUserPermissionsService;
private NodeJWTService $nodeJWTService;
public function boot(GetUserPermissionsService $getUserPermissionsService, NodeJWTService $nodeJWTService): void
{
$this->getUserPermissionsService = $getUserPermissionsService;
$this->nodeJWTService = $nodeJWTService;
}
protected function getToken(): string protected function getToken(): string
{ {
if (!$this->user || !$this->server || $this->user->cannot(Permission::ACTION_WEBSOCKET_CONNECT, $this->server)) { if (!$this->user || !$this->server || $this->user->cannot(Permission::ACTION_WEBSOCKET_CONNECT, $this->server)) {
throw new HttpForbiddenException('You do not have permission to connect to this server\'s websocket.'); throw new HttpForbiddenException('You do not have permission to connect to this server\'s websocket.');
} }
// @phpstan-ignore-next-line
$permissions = app(GetUserPermissionsService::class)->handle($this->server, $this->user);
// @phpstan-ignore-next-line $permissions = $this->getUserPermissionsService->handle($this->server, $this->user);
return app(NodeJWTService::class)
return $this->nodeJWTService
->setExpiresAt(now()->addMinutes(10)->toImmutable()) ->setExpiresAt(now()->addMinutes(10)->toImmutable())
->setUser($this->user) ->setUser($this->user)
->setClaims([ ->setClaims([

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Api\Client; namespace App\Http\Controllers\Api\Client;
use Illuminate\Auth\SessionGuard;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Auth\AuthManager; use Illuminate\Auth\AuthManager;
@ -63,8 +64,7 @@ class AccountController extends ClientApiController
// other devices functionality to work. // other devices functionality to work.
$guard->setUser($user); $guard->setUser($user);
// This method doesn't exist in the stateless Sanctum world. if ($guard instanceof SessionGuard) {
if (method_exists($guard, 'logoutOtherDevices')) {
$guard->logoutOtherDevices($request->input('password')); $guard->logoutOtherDevices($request->input('password'));
} }

View File

@ -22,14 +22,11 @@ use App\Http\Requests\Api\Client\Servers\Backups\RestoreBackupRequest;
class BackupController extends ClientApiController class BackupController extends ClientApiController
{ {
/**
* BackupController constructor.
*/
public function __construct( public function __construct(
private DaemonBackupRepository $daemonRepository, private readonly DaemonBackupRepository $daemonRepository,
private DeleteBackupService $deleteBackupService, private readonly DeleteBackupService $deleteBackupService,
private InitiateBackupService $initiateBackupService, private readonly InitiateBackupService $initiateBackupService,
private DownloadLinkService $downloadLinkService, private readonly DownloadLinkService $downloadLinkService,
) { ) {
parent::__construct(); parent::__construct();
} }
@ -38,7 +35,7 @@ class BackupController extends ClientApiController
* Returns all the backups for a given server instance in a paginated * Returns all the backups for a given server instance in a paginated
* result set. * result set.
* *
* @throws \Illuminate\Auth\Access\AuthorizationException * @throws AuthorizationException
*/ */
public function index(Request $request, Server $server): array public function index(Request $request, Server $server): array
{ {

View File

@ -5,7 +5,6 @@ namespace App\Http\Controllers\Api\Client\Servers;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use App\Models\Server; use App\Models\Server;
use App\Facades\Activity; use App\Facades\Activity;
use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Exception\BadResponseException; use GuzzleHttp\Exception\BadResponseException;
use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\HttpException;
use App\Http\Controllers\Api\Client\ClientApiController; use App\Http\Controllers\Api\Client\ClientApiController;
@ -28,7 +27,7 @@ class CommandController extends ClientApiController
$previous = $exception->getPrevious(); $previous = $exception->getPrevious();
if ($previous instanceof BadResponseException) { if ($previous instanceof BadResponseException) {
if ($previous->getResponse() instanceof ResponseInterface && $previous->getResponse()->getStatusCode() === Response::HTTP_BAD_GATEWAY) { if ($previous->getResponse()->getStatusCode() === Response::HTTP_BAD_GATEWAY) {
throw new HttpException(Response::HTTP_BAD_GATEWAY, 'Server must be online in order to send commands.', $exception); throw new HttpException(Response::HTTP_BAD_GATEWAY, 'Server must be online in order to send commands.', $exception);
} }
} }

View File

@ -158,6 +158,6 @@ class SftpAuthenticationController extends Controller
{ {
$username = explode('.', strrev($request->input('username', ''))); $username = explode('.', strrev($request->input('username', '')));
return strtolower(strrev($username[0] ?? '') . '|' . $request->ip()); return strtolower(strrev($username[0]) . '|' . $request->ip());
} }
} }

View File

@ -1,106 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use Illuminate\Http\Request;
use App\Models\User;
use Illuminate\Auth\AuthManager;
use Illuminate\Http\JsonResponse;
use Illuminate\Auth\Events\Failed;
use Illuminate\Container\Container;
use Illuminate\Support\Facades\Event;
use App\Events\Auth\DirectLogin;
use App\Exceptions\DisplayException;
use App\Http\Controllers\Controller;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
abstract class AbstractLoginController extends Controller
{
use AuthenticatesUsers;
protected AuthManager $auth;
/**
* Lockout time for failed login requests.
*/
protected int $lockoutTime;
/**
* After how many attempts should logins be throttled and locked.
*/
protected int $maxLoginAttempts;
/**
* Where to redirect users after login / registration.
*/
protected string $redirectTo = '/';
/**
* LoginController constructor.
*/
public function __construct()
{
$this->lockoutTime = config('auth.lockout.time');
$this->maxLoginAttempts = config('auth.lockout.attempts');
$this->auth = Container::getInstance()->make(AuthManager::class);
}
/**
* Get the failed login response instance.
*
* @throws \App\Exceptions\DisplayException
*/
protected function sendFailedLoginResponse(Request $request, ?Authenticatable $user = null, ?string $message = null): never
{
$this->incrementLoginAttempts($request);
$this->fireFailedLoginEvent($user, [
$this->getField($request->input('user')) => $request->input('user'),
]);
if ($request->route()->named('auth.login-checkpoint')) {
throw new DisplayException($message ?? trans('auth.two_factor.checkpoint_failed'));
}
throw new DisplayException(trans('auth.failed'));
}
/**
* Send the response after the user was authenticated.
*/
protected function sendLoginResponse(User $user, Request $request): JsonResponse
{
$request->session()->remove('auth_confirmation_token');
$request->session()->regenerate();
$this->clearLoginAttempts($request);
$this->auth->guard()->login($user, true);
Event::dispatch(new DirectLogin($user, true));
return new JsonResponse([
'data' => [
'complete' => true,
'intended' => $this->redirectPath(),
'user' => $user->toReactObject(),
],
]);
}
/**
* Determine if the user is logging in using an email or username.
*/
protected function getField(?string $input = null): string
{
return ($input && str_contains($input, '@')) ? 'email' : 'username';
}
/**
* Fire a failed login event.
*/
protected function fireFailedLoginEvent(?Authenticatable $user = null, array $credentials = []): void
{
Event::dispatch(new Failed('auth', $user, $credentials));
}
}

View File

@ -1,38 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Password;
use App\Http\Controllers\Controller;
use App\Events\Auth\FailedPasswordReset;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
class ForgotPasswordController extends Controller
{
use SendsPasswordResetEmails;
/**
* Get the response for a failed password reset link.
*/
protected function sendResetLinkFailedResponse(Request $request, string $response): JsonResponse
{
// As noted in #358 we will return success even if it failed
// to avoid pointing out that an account does or does not
// exist on the system.
event(new FailedPasswordReset($request->ip(), $request->input('email')));
return $this->sendResetLinkResponse($request, Password::RESET_LINK_SENT);
}
/**
* Get the response for a successful password reset link.
*/
protected function sendResetLinkResponse(Request $request, string $response): JsonResponse
{
return response()->json([
'status' => trans($response),
]);
}
}

View File

@ -1,124 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use PragmaRX\Google2FA\Google2FA;
use Illuminate\Support\Facades\Event;
use App\Events\Auth\ProvidedAuthenticationToken;
use App\Http\Requests\Auth\LoginCheckpointRequest;
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
class LoginCheckpointController extends AbstractLoginController
{
private const TOKEN_EXPIRED_MESSAGE = 'The authentication token provided has expired, please refresh the page and try again.';
/**
* LoginCheckpointController constructor.
*/
public function __construct(
private Google2FA $google2FA,
private ValidationFactory $validation
) {
parent::__construct();
}
/**
* Handle a login where the user is required to provide a TOTP authentication
* token. Once a user has reached this stage it is assumed that they have already
* provided a valid username and password.
*
* @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException
* @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
* @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
* @throws \Exception
* @throws \Illuminate\Validation\ValidationException
*/
public function __invoke(LoginCheckpointRequest $request): JsonResponse
{
if ($this->hasTooManyLoginAttempts($request)) {
$this->sendLockoutResponse($request);
}
$details = $request->session()->get('auth_confirmation_token');
if (!$this->hasValidSessionData($details)) {
$this->sendFailedLoginResponse($request, null, self::TOKEN_EXPIRED_MESSAGE);
}
if (!hash_equals($request->input('confirmation_token') ?? '', $details['token_value'])) {
$this->sendFailedLoginResponse($request);
}
$user = User::query()->find($details['user_id']);
if (!$user) {
$this->sendFailedLoginResponse($request, null, self::TOKEN_EXPIRED_MESSAGE);
}
// Recovery tokens go through a slightly different pathway for usage.
if (!is_null($recoveryToken = $request->input('recovery_token'))) {
if ($this->isValidRecoveryToken($user, $recoveryToken)) {
Event::dispatch(new ProvidedAuthenticationToken($user, true));
return $this->sendLoginResponse($user, $request);
}
} else {
if ($this->google2FA->verifyKey($user->totp_secret, (string) $request->input('authentication_code'), config('panel.auth.2fa.window'))) {
Event::dispatch(new ProvidedAuthenticationToken($user));
return $this->sendLoginResponse($user, $request);
}
}
$this->sendFailedLoginResponse($request, $user, !empty($recoveryToken) ? 'The recovery token provided is not valid.' : null);
}
/**
* Determines if a given recovery token is valid for the user account. If we find a matching token
* it will be deleted from the database.
*
* @throws \Exception
*/
protected function isValidRecoveryToken(User $user, string $value): bool
{
foreach ($user->recoveryTokens as $token) {
if (password_verify($value, $token->token)) {
$token->delete();
return true;
}
}
return false;
}
/**
* Determines if the data provided from the session is valid or not. This
* will return false if the data is invalid, or if more time has passed than
* was configured when the session was written.
*/
protected function hasValidSessionData(array $data): bool
{
$validator = $this->validation->make($data, [
'user_id' => 'required|integer|min:1',
'token_value' => 'required|string',
'expires_at' => 'required',
]);
if ($validator->fails()) {
return false;
}
if (!$data['expires_at'] instanceof CarbonInterface) {
return false;
}
if ($data['expires_at']->isBefore(CarbonImmutable::now())) {
return false;
}
return true;
}
}

View File

@ -1,77 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Livewire\Installer\PanelInstaller;
use Carbon\CarbonImmutable;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use App\Facades\Activity;
use Illuminate\View\View;
class LoginController extends AbstractLoginController
{
/**
* Handle all incoming requests for the authentication routes and render the
* base authentication view component. React will take over at this point and
* turn the login area into an SPA.
*/
public function index(): View|RedirectResponse
{
if (!PanelInstaller::isInstalled()) {
return redirect('/installer');
}
return view('templates/auth.core');
}
/**
* Handle a login request to the application.
*
* @throws \App\Exceptions\DisplayException
* @throws \Illuminate\Validation\ValidationException
*/
public function login(Request $request): JsonResponse
{
if ($this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);
$this->sendLockoutResponse($request);
}
$username = $request->input('user');
$user = User::query()->where($this->getField($username), $username)->first();
if (!$user) {
$this->sendFailedLoginResponse($request);
}
// Ensure that the account is using a valid username and password before trying to
// continue. Previously this was handled in the 2FA checkpoint, however that has
// a flaw in which you can discover if an account exists simply by seeing if you
// can proceed to the next step in the login process.
if (!password_verify($request->input('password'), $user->password)) {
$this->sendFailedLoginResponse($request, $user);
}
if (!$user->use_totp) {
return $this->sendLoginResponse($user, $request);
}
Activity::event('auth:checkpoint')->withRequestMetadata()->subject($user)->log();
$request->session()->put('auth_confirmation_token', [
'user_id' => $user->id,
'token_value' => $token = Str::random(64),
'expires_at' => CarbonImmutable::now()->addMinutes(5),
]);
return new JsonResponse([
'data' => [
'complete' => false,
'confirmation_token' => $token,
],
]);
}
}

View File

@ -16,12 +16,9 @@ use Illuminate\Http\Request;
class OAuthController extends Controller class OAuthController extends Controller
{ {
/**
* OAuthController constructor.
*/
public function __construct( public function __construct(
private AuthManager $auth, private readonly AuthManager $auth,
private UserUpdateService $updateService private readonly UserUpdateService $updateService
) {} ) {}
/** /**

View File

@ -1,100 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Models\User;
use Illuminate\Support\Str;
use Illuminate\Http\JsonResponse;
use Illuminate\Contracts\Hashing\Hasher;
use Illuminate\Support\Facades\Password;
use Illuminate\Auth\Events\PasswordReset;
use App\Exceptions\DisplayException;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
use App\Http\Requests\Auth\ResetPasswordRequest;
class ResetPasswordController extends Controller
{
use ResetsPasswords;
/**
* The URL to redirect users to after password reset.
*/
public string $redirectTo = '/';
protected bool $hasTwoFactor = false;
/**
* ResetPasswordController constructor.
*/
public function __construct(
private Hasher $hasher,
) {}
/**
* Reset the given user's password.
*
* @throws \App\Exceptions\DisplayException
*/
public function __invoke(ResetPasswordRequest $request): JsonResponse
{
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise, we will parse the error and return the response.
$response = $this->broker()->reset(
$this->credentials($request),
function ($user, $password) {
$this->resetPassword($user, $password);
}
);
// If the password was successfully reset, we will redirect the user back to
// the application's home authenticated view. If there is an error we can
// redirect them back to where they came from with their error message.
if ($response === Password::PASSWORD_RESET) {
return $this->sendResetResponse();
}
throw new DisplayException(trans($response));
}
/**
* Reset the given user's password. If the user has two-factor authentication enabled on their
* account do not automatically log them in. In those cases, send the user back to the login
* form with a note telling them their password was changed and to log back in.
*
* @param \Illuminate\Contracts\Auth\CanResetPassword|\App\Models\User $user
* @param string $password
*
* @throws \App\Exceptions\Model\DataValidationException
*/
protected function resetPassword($user, $password): void
{
/** @var User $user */
$user->password = $this->hasher->make($password);
$user->setRememberToken(Str::random(60));
$user->save();
event(new PasswordReset($user));
// If the user is not using 2FA log them in, otherwise skip this step and force a
// fresh login where they'll be prompted to enter a token.
if (!$user->use_totp) {
$this->guard()->login($user);
}
$this->hasTwoFactor = $user->use_totp;
}
/**
* Send a successful password reset response back to the callee.
*/
protected function sendResetResponse(): JsonResponse
{
return response()->json([
'success' => true,
'redirect_to' => $this->redirectTo,
'send_to_login' => $this->hasTwoFactor,
]);
}
}

View File

@ -1,23 +0,0 @@
<?php
namespace App\Http\Controllers\Base;
use Illuminate\View\View;
use Illuminate\View\Factory as ViewFactory;
use App\Http\Controllers\Controller;
class IndexController extends Controller
{
/**
* IndexController constructor.
*/
public function __construct(protected ViewFactory $view) {}
/**
* Returns listing of user's servers.
*/
public function index(): View
{
return view('templates/base.core');
}
}

View File

@ -1,73 +0,0 @@
<?php
namespace App\Http\Controllers\Base;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Translation\Translator;
use Illuminate\Contracts\Translation\Loader;
use App\Http\Controllers\Controller;
class LocaleController extends Controller
{
protected Loader $loader;
public function __construct(Translator $translator)
{
$this->loader = $translator->getLoader();
}
/**
* Returns translation data given a specific locale and namespace.
*/
public function __invoke(Request $request): JsonResponse
{
$locales = explode(' ', $request->input('locale') ?? '');
$namespaces = explode(' ', $request->input('namespace') ?? '');
$response = [];
foreach ($locales as $locale) {
$response[$locale] = [];
foreach ($namespaces as $namespace) {
$response[$locale][$namespace] = $this->i18n(
$this->loader->load($locale, str_replace('.', '/', $namespace))
);
}
}
return new JsonResponse($response, 200, [
// Cache this in the browser for an hour, and allow the browser to use a stale
// cache for up to a day after it was created while it fetches an updated set
// of translation keys.
'Cache-Control' => 'public, max-age=3600, stale-while-revalidate=86400',
'ETag' => md5(json_encode($response, JSON_THROW_ON_ERROR)),
]);
}
/**
* Convert standard Laravel translation keys that look like ":foo"
* into key structures that are supported by the front-end i18n
* library, like "{{foo}}".
*/
protected function i18n(array $data): array
{
foreach ($data as $key => $value) {
if (is_array($value)) {
$data[$key] = $this->i18n($value);
} else {
// Find a Laravel style translation replacement in the string and replace it with
// one that the front-end is able to use. This won't always be present, especially
// for complex strings or things where we'd never have a backend component anyways.
//
// For example:
// "Hello :name, the :notifications.0.title notification needs :count actions :foo.0.bar."
//
// Becomes:
// "Hello {{name}}, the {{notifications.0.title}} notification needs {{count}} actions {{foo.0.bar}}."
$data[$key] = preg_replace('/:([\w.-]+\w)([^\w:]?|$)/m', '{{$1}}$2', $value);
}
}
return $data;
}
}

View File

@ -2,24 +2,17 @@
namespace App\Http\Middleware; namespace App\Http\Middleware;
use App\Filament\App\Resources\ServerResource\Pages\ListServers;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Auth\AuthManager; use Illuminate\Auth\AuthManager;
class RedirectIfAuthenticated readonly class RedirectIfAuthenticated
{ {
/**
* RedirectIfAuthenticated constructor.
*/
public function __construct(private AuthManager $authManager) {} public function __construct(private AuthManager $authManager) {}
/**
* Handle an incoming request.
*/
public function handle(Request $request, \Closure $next, ?string $guard = null): mixed public function handle(Request $request, \Closure $next, ?string $guard = null): mixed
{ {
if ($this->authManager->guard($guard)->check()) { if ($this->authManager->guard($guard)->check()) {
return redirect(ListServers::getUrl()); return redirect('/');
} }
return $next($request); return $next($request);

View File

@ -47,8 +47,7 @@ class PanelInstaller extends SimplePage implements HasForms
public static function isInstalled(): bool public static function isInstalled(): bool
{ {
// This defaults to true so existing panels count as "installed" return config('app.installed');
return env('APP_INSTALLED', true);
} }
public function mount(): void public function mount(): void

View File

@ -14,7 +14,8 @@ class RequirementsStep
public static function make(): Step public static function make(): Step
{ {
$correctPhpVersion = version_compare(PHP_VERSION, self::MIN_PHP_VERSION) >= 0; $compare = version_compare(phpversion(), self::MIN_PHP_VERSION);
$correctPhpVersion = $compare >= 0;
$fields = [ $fields = [
Section::make('PHP Version') Section::make('PHP Version')

View File

@ -85,12 +85,7 @@ class ActivityLog extends Model
public function actor(): MorphTo public function actor(): MorphTo
{ {
$morph = $this->morphTo(); return $this->morphTo()->withTrashed();
if (method_exists($morph, 'withTrashed')) {
return $morph->withTrashed();
}
return $morph;
} }
public function subjects(): HasMany public function subjects(): HasMany

View File

@ -60,7 +60,7 @@ class AuditLog extends Model
*/ */
public static function instance(string $action, array $metadata, bool $isSystem = false): self public static function instance(string $action, array $metadata, bool $isSystem = false): self
{ {
/** @var \Illuminate\Http\Request $request */ /** @var ?Request $request */
$request = Container::getInstance()->make('request'); $request = Container::getInstance()->make('request');
if ($isSystem || !$request instanceof Request) { if ($isSystem || !$request instanceof Request) {
$request = null; $request = null;

View File

@ -2,7 +2,7 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Builder; use App\Eloquent\BackupQueryBuilder;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -81,10 +81,11 @@ class Backup extends Model
} }
/** /**
* Returns a query filtering only non-failed backups for a specific server. * @param \Illuminate\Database\Query\Builder $query
* @return BackupQueryBuilder<\Illuminate\Database\Eloquent\Model>
*/ */
public function scopeNonFailed(Builder $query): void public function newEloquentBuilder($query): BackupQueryBuilder
{ {
$query->whereNull('completed_at')->orWhere('is_successful', true); return new BackupQueryBuilder($query);
} }
} }

View File

@ -130,8 +130,7 @@ class File extends Model
public function getRows(): array public function getRows(): array
{ {
try { try {
/** @var DaemonFileRepository $fileRepository */ $fileRepository = (new DaemonFileRepository())->setServer(self::$server);
$fileRepository = app(DaemonFileRepository::class)->setServer(self::$server); // @phpstan-ignore-line
if (!is_null(self::$searchTerm)) { if (!is_null(self::$searchTerm)) {
$contents = cache()->remember('file_search_' . self::$path . '_' . self::$searchTerm, now()->addMinute(), fn () => $fileRepository->search(self::$searchTerm, self::$path)); $contents = cache()->remember('file_search_' . self::$path . '_' . self::$searchTerm, now()->addMinute(), fn () => $fileRepository->search(self::$searchTerm, self::$path));

View File

@ -307,8 +307,7 @@ class Node extends Model
{ {
return once(function () { return once(function () {
try { try {
// @phpstan-ignore-next-line return (new DaemonConfigurationRepository())
return resolve(DaemonConfigurationRepository::class)
->setNode($this) ->setNode($this)
->getSystemInformation(); ->getSystemInformation();
} catch (Exception $exception) { } catch (Exception $exception) {

View File

@ -342,6 +342,9 @@ class Server extends Model
return $this->hasOne(ServerTransfer::class)->whereNull('successful')->orderByDesc('id'); return $this->hasOne(ServerTransfer::class)->whereNull('successful')->orderByDesc('id');
} }
/**
* @return HasMany<Backup, $this>
*/
public function backups(): HasMany public function backups(): HasMany
{ {
return $this->hasMany(Backup::class); return $this->hasMany(Backup::class);
@ -487,7 +490,7 @@ class Server extends Model
public function condition(): Attribute public function condition(): Attribute
{ {
return Attribute::make( return Attribute::make(
get: fn () => $this->isSuspended() ? ServerState::Suspended : $this->status?->value ?? $this->retrieveStatus(), get: fn () => $this->isSuspended() ? ServerState::Suspended : $this->status->value ?? $this->retrieveStatus(),
); );
} }

View File

@ -20,7 +20,6 @@ use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use App\Models\Traits\HasAccessTokens; use App\Models\Traits\HasAccessTokens;
use Illuminate\Auth\Passwords\CanResetPassword; use Illuminate\Auth\Passwords\CanResetPassword;
use App\Traits\Helpers\AvailableLanguages;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\Access\Authorizable; use Illuminate\Foundation\Auth\Access\Authorizable;
use Illuminate\Database\Eloquent\Relations\MorphToMany; use Illuminate\Database\Eloquent\Relations\MorphToMany;
@ -30,6 +29,7 @@ use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use App\Notifications\SendPasswordReset as ResetPasswordNotification; use App\Notifications\SendPasswordReset as ResetPasswordNotification;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Illuminate\Database\Eloquent\Model as IlluminateModel; use Illuminate\Database\Eloquent\Model as IlluminateModel;
use ResourceBundle;
use Spatie\Permission\Traits\HasRoles; use Spatie\Permission\Traits\HasRoles;
/** /**
@ -89,7 +89,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
{ {
use Authenticatable; use Authenticatable;
use Authorizable {can as protected canned; } use Authorizable {can as protected canned; }
use AvailableLanguages;
use CanResetPassword; use CanResetPassword;
use HasAccessTokens; use HasAccessTokens;
use HasRoles; use HasRoles;
@ -179,9 +178,8 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
protected static function booted(): void protected static function booted(): void
{ {
static::creating(function (self $user) { static::creating(function (self $user) {
$user->uuid = Str::uuid()->toString(); $user->uuid ??= Str::uuid()->toString();
$user->timezone ??= config('app.timezone');
$user->timezone = env('APP_TIMEZONE', 'UTC');
return true; return true;
}); });
@ -206,8 +204,8 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
{ {
$rules = parent::getRules(); $rules = parent::getRules();
$rules['language'][] = new In(array_keys((new self())->getAvailableLanguages())); $rules['language'][] = new In(array_values(array_filter(ResourceBundle::getLocales(''), fn ($lang) => preg_match('/^[a-z]{2}$/', $lang))));
$rules['timezone'][] = new In(array_values(DateTimeZone::listIdentifiers())); $rules['timezone'][] = new In(DateTimeZone::listIdentifiers());
$rules['username'][] = new Username(); $rules['username'][] = new Username();
return $rules; return $rules;
@ -257,6 +255,8 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
/** /**
* Returns all servers that a user owns. * Returns all servers that a user owns.
*
* @return HasMany<Server, $this>
*/ */
public function servers(): HasMany public function servers(): HasMany
{ {

View File

@ -6,6 +6,7 @@ use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope; use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule; use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
class ForbiddenGlobalFunctionsRule implements Rule class ForbiddenGlobalFunctionsRule implements Rule
{ {
@ -28,7 +29,10 @@ class ForbiddenGlobalFunctionsRule implements Rule
$functionName = (string) $node->name; $functionName = (string) $node->name;
if (in_array($functionName, $this->forbiddenFunctions, true)) { if (in_array($functionName, $this->forbiddenFunctions, true)) {
return [ return [
sprintf('Usage of global function "%s" is forbidden.', $functionName), RuleErrorBuilder::message(sprintf(
'Usage of global function "%s" is forbidden.',
$functionName,
))->identifier('myCustomRules.forbiddenGlobalFunctions')->build(),
]; ];
} }
} }

View File

@ -3,6 +3,7 @@
namespace App\Services\Activity; namespace App\Services\Activity;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Throwable;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use App\Models\ActivityLog; use App\Models\ActivityLog;
@ -141,9 +142,8 @@ class ActivityLogService
try { try {
return $this->save(); return $this->save();
} catch (\Throwable|\Exception $exception) { } catch (Throwable $exception) {
if (config('app.env') !== 'production') { if (config('app.env') !== 'production') {
/* @noinspection PhpUnhandledExceptionInspection */
throw $exception; throw $exception;
} }
@ -216,10 +216,8 @@ class ActivityLogService
if ($actor = $this->targetable->actor()) { if ($actor = $this->targetable->actor()) {
$this->actor($actor); $this->actor($actor);
} elseif ($user = $this->manager->guard()->user()) { } elseif ($user = $this->manager->guard()->user()) {
if ($user instanceof Model) {
$this->actor($user); $this->actor($user);
} }
}
return $this->activity; return $this->activity;
} }

View File

@ -14,7 +14,7 @@ use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
class InitiateBackupService class InitiateBackupService
{ {
private ?array $ignoredFiles; private array $ignoredFiles;
private bool $isLocked = false; private bool $isLocked = false;
@ -22,10 +22,10 @@ class InitiateBackupService
* InitiateBackupService constructor. * InitiateBackupService constructor.
*/ */
public function __construct( public function __construct(
private ConnectionInterface $connection, private readonly ConnectionInterface $connection,
private DaemonBackupRepository $daemonBackupRepository, private readonly DaemonBackupRepository $daemonBackupRepository,
private DeleteBackupService $deleteBackupService, private readonly DeleteBackupService $deleteBackupService,
private BackupManager $backupManager private readonly BackupManager $backupManager
) {} ) {}
/** /**

View File

@ -8,18 +8,10 @@ use App\Models\Database;
use App\Models\DatabaseHost; use App\Models\DatabaseHost;
use App\Exceptions\Service\Database\NoSuitableDatabaseHostException; use App\Exceptions\Service\Database\NoSuitableDatabaseHostException;
class DeployServerDatabaseService readonly class DeployServerDatabaseService
{ {
/**
* DeployServerDatabaseService constructor.
*/
public function __construct(private DatabaseManagementService $managementService) {} public function __construct(private DatabaseManagementService $managementService) {}
/**
* @throws \Throwable
* @throws \App\Exceptions\Service\Database\TooManyDatabasesException
* @throws \App\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException
*/
public function handle(Server $server, array $data): Database public function handle(Server $server, array $data): Database
{ {
Assert::notEmpty($data['database'] ?? null); Assert::notEmpty($data['database'] ?? null);

View File

@ -0,0 +1,40 @@
<?php
namespace App\Services\Helpers;
use Illuminate\Support\Facades\File;
use Locale;
class LanguageService
{
public const TRANSLATED_COMPLETELY = [
'ar',
'cz',
'da',
'de',
'dk',
'en',
'es',
'fi',
'ja',
'nl',
'pl',
'sk',
'ru',
'tr',
];
public function isLanguageTranslated(string $countryCode = 'en'): bool
{
return in_array($countryCode, self::TRANSLATED_COMPLETELY, true);
}
public function getAvailableLanguages(string $path = 'lang'): array
{
return collect(File::directories(base_path($path)))->mapWithKeys(function ($path) {
$code = basename($path);
return [$code => title_case(Locale::getDisplayName($code, $code))];
})->toArray();
}
}

View File

@ -3,6 +3,7 @@
namespace App\Services\Nodes; namespace App\Services\Nodes;
use Carbon\CarbonImmutable; use Carbon\CarbonImmutable;
use DateTimeImmutable;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use App\Models\Node; use App\Models\Node;
use App\Models\User; use App\Models\User;
@ -18,7 +19,7 @@ class NodeJWTService
private ?User $user = null; private ?User $user = null;
private ?\DateTimeImmutable $expiresAt; private DateTimeImmutable $expiresAt;
private ?string $subject = null; private ?string $subject = null;
@ -73,9 +74,7 @@ class NodeJWTService
->issuedAt(CarbonImmutable::now()) ->issuedAt(CarbonImmutable::now())
->canOnlyBeUsedAfter(CarbonImmutable::now()->subMinutes(5)); ->canOnlyBeUsedAfter(CarbonImmutable::now()->subMinutes(5));
if ($this->expiresAt) {
$builder = $builder->expiresAt($this->expiresAt); $builder = $builder->expiresAt($this->expiresAt);
}
if (!empty($this->subject)) { if (!empty($this->subject)) {
$builder = $builder->relatedTo($this->subject)->withHeader('sub', $this->subject); $builder = $builder->relatedTo($this->subject)->withHeader('sub', $this->subject);

View File

@ -2,6 +2,7 @@
namespace App\Services\Schedules; namespace App\Services\Schedules;
use App\Models\Task;
use Exception; use Exception;
use App\Models\Schedule; use App\Models\Schedule;
use Illuminate\Contracts\Bus\Dispatcher; use Illuminate\Contracts\Bus\Dispatcher;
@ -12,18 +13,14 @@ use App\Repositories\Daemon\DaemonServerRepository;
class ProcessScheduleService class ProcessScheduleService
{ {
/**
* ProcessScheduleService constructor.
*/
public function __construct(private ConnectionInterface $connection, private Dispatcher $dispatcher, private DaemonServerRepository $serverRepository) {} public function __construct(private ConnectionInterface $connection, private Dispatcher $dispatcher, private DaemonServerRepository $serverRepository) {}
/** /**
* Process a schedule and push the first task onto the queue worker. * Process a schedule and push the first task onto the queue worker.
*
* @throws \Throwable
*/ */
public function handle(Schedule $schedule, bool $now = false): void public function handle(Schedule $schedule, bool $now = false): void
{ {
/** @var ?Task $task */
$task = $schedule->tasks()->orderBy('sequence_id')->first(); $task = $schedule->tasks()->orderBy('sequence_id')->first();
if (!$task) { if (!$task) {

View File

@ -1,57 +0,0 @@
<?php
namespace App\Traits\Helpers;
use Locale;
use Illuminate\Filesystem\Filesystem;
trait AvailableLanguages
{
private ?Filesystem $filesystem = null;
public const TRANSLATED = [
'ar',
'cz',
'da',
'de',
'dk',
'en',
'es',
'fi',
'ja',
'nl',
'pl',
'sk',
'ru',
'tr',
];
/**
* Return all the available languages on the Panel based on those
* that are present in the language folder.
*/
public function getAvailableLanguages(): array
{
return collect($this->getFilesystemInstance()->directories(base_path('lang')))->mapWithKeys(function ($path) {
$code = basename($path);
$value = Locale::getDisplayName($code, $code);
return [$code => title_case($value)];
})->toArray();
}
public function isLanguageTranslated(string $countryCode = 'en'): bool
{
return in_array($countryCode, self::TRANSLATED, true);
}
/**
* Return an instance of the filesystem for getting a folder listing.
*/
private function getFilesystemInstance(): Filesystem
{
// @phpstan-ignore-next-line
return $this->filesystem = $this->filesystem ?: app()->make(Filesystem::class);
}
}

View File

@ -24,7 +24,7 @@ abstract class BaseTransformer extends TransformerAbstract
/** /**
* BaseTransformer constructor. * BaseTransformer constructor.
*/ */
public function __construct() final public function __construct()
{ {
// Transformers allow for dependency injection on the handle method. // Transformers allow for dependency injection on the handle method.
if (method_exists($this, 'handle')) { if (method_exists($this, 'handle')) {
@ -40,7 +40,7 @@ abstract class BaseTransformer extends TransformerAbstract
/** /**
* Sets the request on the instance. * Sets the request on the instance.
*/ */
public function setRequest(Request $request): self public function setRequest(Request $request): static
{ {
$this->request = $request; $this->request = $request;
@ -49,13 +49,10 @@ abstract class BaseTransformer extends TransformerAbstract
/** /**
* Returns a new transformer instance with the request set on the instance. * Returns a new transformer instance with the request set on the instance.
*
* @return static
*/ */
public static function fromRequest(Request $request): self public static function fromRequest(Request $request): static
{ {
// @phpstan-ignore-next-line return (new static())->setRequest($request);
return app(static::class)->setRequest($request);
} }
/** /**

View File

@ -49,8 +49,7 @@ if (!function_exists('join_paths')) {
if (!function_exists('resolve_path')) { if (!function_exists('resolve_path')) {
function resolve_path(string $path): string function resolve_path(string $path): string
{ {
// @phpstan-ignore-next-line $parts = array_filter(explode('/', $path), fn (string $p) => strlen($p) > 0);
$parts = array_filter(explode('/', $path), 'strlen');
$absolutes = []; $absolutes = [];
foreach ($parts as $part) { foreach ($parts as $part) {

View File

@ -49,7 +49,7 @@
"require-dev": { "require-dev": {
"barryvdh/laravel-ide-helper": "^3.0", "barryvdh/laravel-ide-helper": "^3.0",
"fakerphp/faker": "^1.23.1", "fakerphp/faker": "^1.23.1",
"larastan/larastan": "^2.9.6", "larastan/larastan": "^3.0",
"laravel/pint": "^1.15.3", "laravel/pint": "^1.15.3",
"laravel/sail": "^1.29.1", "laravel/sail": "^1.29.1",
"mockery/mockery": "^1.6.11", "mockery/mockery": "^1.6.11",

702
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,8 @@ return [
'timezone' => 'UTC', 'timezone' => 'UTC',
'installed' => env('APP_INSTALLED', true),
'exceptions' => [ 'exceptions' => [
'report_all' => env('APP_REPORT_ALL_EXCEPTIONS', false), 'report_all' => env('APP_REPORT_ALL_EXCEPTIONS', false),
], ],

View File

@ -12,6 +12,15 @@ parameters:
level: 6 level: 6
ignoreErrors: ignoreErrors:
- '#no value type specified in iterable#' - identifier: argument.templateType
- '#Unable to resolve the template type#' - identifier: missingType.generics
- '#does not specify its types#' - identifier: missingType.iterableValue
- identifier: property.notFound
# We are getting and setting environment variables directly
-
identifier: larastan.noEnvCallsOutsideOfConfig
paths:
- app/Console/Commands/Environment/*.php
- app/Extensions/OAuth/Providers/*.php
- app/Filament/Admin/Pages/Settings.php

View File

@ -3,5 +3,7 @@
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Auth; use App\Http\Controllers\Auth;
Route::redirect('/login', '/login')->name('auth.login');
Route::get('/oauth/redirect/{driver}', [Auth\OAuthController::class, 'redirect'])->name('auth.oauth.redirect'); Route::get('/oauth/redirect/{driver}', [Auth\OAuthController::class, 'redirect'])->name('auth.oauth.redirect');
Route::get('/oauth/callback/{driver}', [Auth\OAuthController::class, 'callback'])->name('auth.oauth.callback')->withoutMiddleware('guest'); Route::get('/oauth/callback/{driver}', [Auth\OAuthController::class, 'callback'])->name('auth.oauth.callback')->withoutMiddleware('guest');