diff --git a/.env.example b/.env.example index c242132286..f6911efe38 100644 --- a/.env.example +++ b/.env.example @@ -1,34 +1,7 @@ APP_ENV=production APP_DEBUG=false APP_KEY= -APP_TIMEZONE=UTC APP_URL=http://panel.test -APP_LOCALE=en APP_INSTALLED=false - -LOG_CHANNEL=daily -LOG_STACK=single -LOG_DEPRECATIONS_CHANNEL=null -LOG_LEVEL=debug - -DB_CONNECTION=sqlite - -CACHE_STORE=file -QUEUE_CONNECTION=database -SESSION_DRIVER=file - -MAIL_MAILER=log -MAIL_HOST=smtp.example.com -MAIL_PORT=25 -MAIL_USERNAME= -MAIL_PASSWORD= -MAIL_ENCRYPTION=tls -MAIL_FROM_ADDRESS=no-reply@example.com -MAIL_FROM_NAME="Pelican Admin" -# Set this to your domain to prevent it defaulting to 'localhost', causing mail servers such as Gmail to reject your mail -# MAIL_EHLO_DOMAIN=panel.example.com - -SESSION_ENCRYPT=false -SESSION_PATH=/ -SESSION_DOMAIN=null -SESSION_COOKIE=pelican_session +APP_TIMEZONE=UTC +APP_LOCALE=en diff --git a/.github/docker/entrypoint.sh b/.github/docker/entrypoint.sh index b6aac4daae..ad1b359b64 100644 --- a/.github/docker/entrypoint.sh +++ b/.github/docker/entrypoint.sh @@ -21,6 +21,9 @@ else echo -e "APP_KEY exists in environment, using that." echo -e "APP_KEY=$APP_KEY" > /pelican-data/.env fi + + ## enable installer + echo -e "APP_INSTALLED=false" >> /pelican-data/.env fi mkdir /pelican-data/database @@ -55,7 +58,7 @@ 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 "$@" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5e2ee88471..f5fede50b7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -3,10 +3,8 @@ name: Tests on: push: branches: - - '**' + - main pull_request: - branches: - - '**' jobs: mysql: diff --git a/Caddyfile b/Caddyfile index 1c835bf057..e307609598 100644 --- a/Caddyfile +++ b/Caddyfile @@ -1,4 +1,5 @@ { + admin off email {$ADMIN_EMAIL} } diff --git a/Dockerfile b/Dockerfile index 8c99bcc252..3e2da64825 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,9 @@ WORKDIR /build COPY . ./ -RUN yarn install --frozen-lockfile && yarn run build:production +RUN yarn config set network-timeout 300000 \ + && yarn install --frozen-lockfile \ + && yarn run build:production FROM php:8.3-fpm-alpine # FROM --platform=$TARGETOS/$TARGETARCH php:8.3-fpm-alpine @@ -36,8 +38,8 @@ RUN touch .env RUN composer install --no-dev --optimize-autoloader # Set file permissions -RUN chmod -R 755 /var/www/html/storage \ - && chmod -R 755 /var/www/html/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 - @@ -49,8 +51,7 @@ RUN cp .github/docker/supervisord.conf /etc/supervisord.conf && \ HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \ CMD curl -f http://localhost/up || exit 1 -EXPOSE 80:2019 -EXPOSE 443 +EXPOSE 80 443 VOLUME /pelican-data diff --git a/app/Console/Commands/Egg/CheckEggUpdatesCommand.php b/app/Console/Commands/Egg/CheckEggUpdatesCommand.php new file mode 100644 index 0000000000..fa7c83ac0f --- /dev/null +++ b/app/Console/Commands/Egg/CheckEggUpdatesCommand.php @@ -0,0 +1,43 @@ +update_url)) { + $this->comment("{$egg->name}: Skipping (no update url set)"); + + continue; + } + + $currentJson = json_decode($exporterService->handle($egg->id)); + unset($currentJson->exported_at); + + $updatedJson = json_decode(file_get_contents($egg->update_url)); + unset($updatedJson->exported_at); + + if (md5(json_encode($currentJson)) === md5(json_encode($updatedJson))) { + $this->info("{$egg->name}: Up-to-date"); + cache()->put("eggs.{$egg->uuid}.update", false, now()->addHour()); + } else { + $this->warn("{$egg->name}: Found update"); + cache()->put("eggs.{$egg->uuid}.update", true, now()->addHour()); + } + } catch (Exception $exception) { + $this->error("{$egg->name}: Error ({$exception->getMessage()})"); + } + } + } +} diff --git a/app/Console/Commands/Environment/EmailSettingsCommand.php b/app/Console/Commands/Environment/EmailSettingsCommand.php index 87c8186c14..23061bf04d 100644 --- a/app/Console/Commands/Environment/EmailSettingsCommand.php +++ b/app/Console/Commands/Environment/EmailSettingsCommand.php @@ -70,7 +70,7 @@ public function handle(): void /** * Handle variables for SMTP driver. */ - private function setupSmtpDriverVariables() + private function setupSmtpDriverVariables(): void { $this->variables['MAIL_HOST'] = $this->option('host') ?? $this->ask( trans('command/messages.environment.mail.ask_smtp_host'), @@ -101,7 +101,7 @@ private function setupSmtpDriverVariables() /** * Handle variables for mailgun driver. */ - private function setupMailgunDriverVariables() + private function setupMailgunDriverVariables(): void { $this->variables['MAILGUN_DOMAIN'] = $this->option('host') ?? $this->ask( trans('command/messages.environment.mail.ask_mailgun_domain'), @@ -122,7 +122,7 @@ private function setupMailgunDriverVariables() /** * Handle variables for mandrill driver. */ - private function setupMandrillDriverVariables() + private function setupMandrillDriverVariables(): void { $this->variables['MANDRILL_SECRET'] = $this->option('password') ?? $this->ask( trans('command/messages.environment.mail.ask_mandrill_secret'), @@ -133,7 +133,7 @@ private function setupMandrillDriverVariables() /** * Handle variables for postmark driver. */ - private function setupPostmarkDriverVariables() + private function setupPostmarkDriverVariables(): void { $this->variables['MAIL_DRIVER'] = 'smtp'; $this->variables['MAIL_HOST'] = 'smtp.postmarkapp.com'; diff --git a/app/Console/Commands/Schedule/ProcessRunnableCommand.php b/app/Console/Commands/Schedule/ProcessRunnableCommand.php index d1d71ae024..d773cb2b2f 100644 --- a/app/Console/Commands/Schedule/ProcessRunnableCommand.php +++ b/app/Console/Commands/Schedule/ProcessRunnableCommand.php @@ -6,7 +6,6 @@ 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 @@ public function handle(): int ->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) { @@ -51,7 +50,7 @@ public function handle(): int * never throw an exception out, otherwise you'll end up killing the entire run group causing * any other schedules to not process correctly. */ - protected function processSchedule(Schedule $schedule) + protected function processSchedule(Schedule $schedule): void { if ($schedule->tasks->isEmpty()) { return; diff --git a/app/Console/Commands/UpgradeCommand.php b/app/Console/Commands/UpgradeCommand.php index 75a3c185f7..145e6e0c6f 100644 --- a/app/Console/Commands/UpgradeCommand.php +++ b/app/Console/Commands/UpgradeCommand.php @@ -178,7 +178,7 @@ public function handle(): void $this->info(__('commands.upgrade.success')); } - protected function withProgress(ProgressBar $bar, \Closure $callback) + protected function withProgress(ProgressBar $bar, \Closure $callback): void { $bar->clear(); $callback(); diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 4a9bbee17d..3c2fc96fad 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -2,15 +2,16 @@ namespace App\Console; +use App\Console\Commands\Egg\CheckEggUpdatesCommand; +use App\Console\Commands\Maintenance\CleanServiceBackupFilesCommand; +use App\Console\Commands\Maintenance\PruneImagesCommand; +use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand; +use App\Console\Commands\Schedule\ProcessRunnableCommand; use App\Jobs\NodeStatistics; use App\Models\ActivityLog; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Database\Console\PruneCommand; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; -use App\Console\Commands\Schedule\ProcessRunnableCommand; -use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand; -use App\Console\Commands\Maintenance\CleanServiceBackupFilesCommand; -use App\Console\Commands\Maintenance\PruneImagesCommand; class Kernel extends ConsoleKernel { @@ -35,6 +36,7 @@ protected function schedule(Schedule $schedule): void $schedule->command(CleanServiceBackupFilesCommand::class)->daily(); $schedule->command(PruneImagesCommand::class)->daily(); + $schedule->command(CheckEggUpdatesCommand::class)->hourly(); $schedule->job(new NodeStatistics())->everyFiveSeconds()->withoutOverlapping(); diff --git a/app/Exceptions/DisplayException.php b/app/Exceptions/DisplayException.php index 6c44c356c1..a004e36699 100644 --- a/app/Exceptions/DisplayException.php +++ b/app/Exceptions/DisplayException.php @@ -4,6 +4,8 @@ use Exception; use Filament\Notifications\Notification; +use Illuminate\Http\JsonResponse; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Psr\Log\LoggerInterface; use Illuminate\Http\Response; @@ -14,8 +16,11 @@ class DisplayException extends PanelException implements HttpExceptionInterface { public const LEVEL_DEBUG = 'debug'; + public const LEVEL_INFO = 'info'; + public const LEVEL_WARNING = 'warning'; + public const LEVEL_ERROR = 'error'; /** @@ -46,7 +51,7 @@ public function getHeaders(): array * and then redirecting them back to the page that they came from. If the * request originated from an API hit, return the error in JSONAPI spec format. */ - public function render(Request $request) + public function render(Request $request): bool|RedirectResponse|JsonResponse { if ($request->is('livewire/update')) { Notification::make() @@ -55,13 +60,14 @@ public function render(Request $request) ->danger() ->send(); - return; + return false; } if ($request->expectsJson()) { 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(); @@ -73,10 +79,10 @@ public function render(Request $request) * * @throws \Throwable */ - public function report() + public function report(): void { if (!$this->getPrevious() instanceof \Exception || !Handler::isReportable($this->getPrevious())) { - return null; + return; } try { @@ -85,6 +91,6 @@ public function report() throw $this->getPrevious(); } - return $logger->{$this->getErrorLevel()}($this->getPrevious()); + $logger->{$this->getErrorLevel()}($this->getPrevious()); } } diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index d30475f668..2ca296e6d6 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -114,7 +114,7 @@ class_basename($exception), /** * Render an exception into an HTTP response. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * * @throws \Throwable */ @@ -140,7 +140,7 @@ public function render($request, \Throwable $e): Response * Transform a validation exception into a consistent format to be returned for * calls to the API. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request */ public function invalidJson($request, ValidationException $exception): JsonResponse { @@ -236,7 +236,7 @@ public static function isReportable(\Exception $exception): bool /** * Convert an authentication exception into an unauthenticated response. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request */ protected function unauthenticated($request, AuthenticationException $exception): JsonResponse|RedirectResponse { @@ -273,6 +273,7 @@ protected function extractPrevious(\Throwable $e): array */ public static function toArray(\Throwable $e): array { + // @phpstan-ignore-next-line return (new self(app()))->convertExceptionToArray($e); } } diff --git a/app/Exceptions/Http/HttpForbiddenException.php b/app/Exceptions/Http/HttpForbiddenException.php index b0536257e0..318ab3b6c7 100644 --- a/app/Exceptions/Http/HttpForbiddenException.php +++ b/app/Exceptions/Http/HttpForbiddenException.php @@ -10,7 +10,7 @@ class HttpForbiddenException extends HttpException /** * HttpForbiddenException constructor. */ - public function __construct(string $message = null, \Throwable $previous = null) + public function __construct(?string $message = null, ?\Throwable $previous = null) { parent::__construct(Response::HTTP_FORBIDDEN, $message, $previous); } diff --git a/app/Exceptions/Http/Server/ServerStateConflictException.php b/app/Exceptions/Http/Server/ServerStateConflictException.php index 1e20082a33..c5b53b4daf 100644 --- a/app/Exceptions/Http/Server/ServerStateConflictException.php +++ b/app/Exceptions/Http/Server/ServerStateConflictException.php @@ -12,7 +12,7 @@ class ServerStateConflictException extends ConflictHttpException * Exception thrown when the server is in an unsupported state for API access or * certain operations within the codebase. */ - public function __construct(Server $server, \Throwable $previous = null) + public function __construct(Server $server, ?\Throwable $previous = null) { $message = 'This server is currently in an unsupported state, please try again later.'; if ($server->isSuspended()) { diff --git a/app/Exceptions/Http/TwoFactorAuthRequiredException.php b/app/Exceptions/Http/TwoFactorAuthRequiredException.php index 6b9804812b..fb9c09ebc1 100644 --- a/app/Exceptions/Http/TwoFactorAuthRequiredException.php +++ b/app/Exceptions/Http/TwoFactorAuthRequiredException.php @@ -11,7 +11,7 @@ class TwoFactorAuthRequiredException extends HttpException implements HttpExcept /** * TwoFactorAuthRequiredException constructor. */ - public function __construct(\Throwable $previous = null) + public function __construct(?\Throwable $previous = null) { parent::__construct(Response::HTTP_BAD_REQUEST, 'Two-factor authentication is required on this account in order to access this endpoint.', $previous); } diff --git a/app/Exceptions/Service/ServiceLimitExceededException.php b/app/Exceptions/Service/ServiceLimitExceededException.php index 3609be97da..2570197d6d 100644 --- a/app/Exceptions/Service/ServiceLimitExceededException.php +++ b/app/Exceptions/Service/ServiceLimitExceededException.php @@ -10,7 +10,7 @@ class ServiceLimitExceededException extends DisplayException * Exception thrown when something goes over a defined limit, such as allocated * ports, tasks, databases, etc. */ - public function __construct(string $message, \Throwable $previous = null) + public function __construct(string $message, ?\Throwable $previous = null) { parent::__construct($message, $previous, self::LEVEL_WARNING); } diff --git a/app/Exceptions/Service/User/TwoFactorAuthenticationTokenInvalid.php b/app/Exceptions/Service/User/TwoFactorAuthenticationTokenInvalid.php index d5f09884c4..0c058d0d86 100644 --- a/app/Exceptions/Service/User/TwoFactorAuthenticationTokenInvalid.php +++ b/app/Exceptions/Service/User/TwoFactorAuthenticationTokenInvalid.php @@ -7,6 +7,7 @@ class TwoFactorAuthenticationTokenInvalid extends DisplayException { public string $title = 'Invalid 2FA Code'; + public string $icon = 'tabler-2fa'; public function __construct() diff --git a/app/Extensions/Backups/BackupManager.php b/app/Extensions/Backups/BackupManager.php index ad1cfdbbc4..9922a55c6f 100644 --- a/app/Extensions/Backups/BackupManager.php +++ b/app/Extensions/Backups/BackupManager.php @@ -34,7 +34,7 @@ public function __construct(protected Application $app) /** * Returns a backup adapter instance. */ - public function adapter(string $name = null): FilesystemAdapter + public function adapter(?string $name = null): FilesystemAdapter { return $this->get($name ?: $this->getDefaultAdapter()); } @@ -145,7 +145,7 @@ public function setDefaultAdapter(string $name): void /** * Unset the given adapter instances. * - * @param string|string[] $adapter + * @param string|string[] $adapter */ public function forget(array|string $adapter): self { diff --git a/app/Extensions/DynamicDatabaseConnection.php b/app/Extensions/DynamicDatabaseConnection.php index d144074273..147f78064e 100644 --- a/app/Extensions/DynamicDatabaseConnection.php +++ b/app/Extensions/DynamicDatabaseConnection.php @@ -7,7 +7,9 @@ class DynamicDatabaseConnection { public const DB_CHARSET = 'utf8'; + public const DB_COLLATION = 'utf8_unicode_ci'; + public const DB_DRIVER = 'mysql'; /** diff --git a/app/Extensions/Themes/Theme.php b/app/Extensions/Themes/Theme.php index 2badc3dcb0..f6f55d8c86 100644 --- a/app/Extensions/Themes/Theme.php +++ b/app/Extensions/Themes/Theme.php @@ -4,17 +4,17 @@ class Theme { - public function js($path): string + public function js(string $path): string { return sprintf('' . PHP_EOL, $this->getUrl($path)); } - public function css($path): string + public function css(string $path): string { return sprintf('' . PHP_EOL, $this->getUrl($path)); } - protected function getUrl($path): string + protected function getUrl(string $path): string { return '/themes/panel/' . ltrim($path, '/'); } diff --git a/app/Filament/Pages/Dashboard.php b/app/Filament/Pages/Dashboard.php index af76c00f54..5415cea799 100644 --- a/app/Filament/Pages/Dashboard.php +++ b/app/Filament/Pages/Dashboard.php @@ -28,16 +28,20 @@ public function getTitle(): string public string $activeTab = 'nodes'; - public function getViewData(): array + private SoftwareVersionService $softwareVersionService; + + public function mount(SoftwareVersionService $softwareVersionService): void { - /** @var SoftwareVersionService $softwareVersionService */ - $softwareVersionService = app(SoftwareVersionService::class); + $this->softwareVersionService = $softwareVersionService; + } + public function getViewData(): array + { return [ 'inDevelopment' => config('app.version') === 'canary', - 'version' => $softwareVersionService->versionData()['version'], - 'latestVersion' => $softwareVersionService->getPanel(), - 'isLatest' => $softwareVersionService->isLatestPanel(), + 'version' => $this->softwareVersionService->versionData()['version'], + 'latestVersion' => $this->softwareVersionService->getPanel(), + 'isLatest' => $this->softwareVersionService->isLatestPanel(), 'eggsCount' => Egg::query()->count(), 'nodesList' => ListNodes::getUrl(), 'nodesCount' => Node::query()->count(), @@ -67,7 +71,7 @@ public function getViewData(): array CreateAction::make() ->label(trans('dashboard/index.sections.intro-support.button_donate')) ->icon('tabler-cash') - ->url($softwareVersionService->getDonations(), true) + ->url($this->softwareVersionService->getDonations(), true) ->color('success'), ], 'helpActions' => [ diff --git a/app/Filament/Pages/Installer/PanelInstaller.php b/app/Filament/Pages/Installer/PanelInstaller.php index 55da7add71..fba27eade8 100644 --- a/app/Filament/Pages/Installer/PanelInstaller.php +++ b/app/Filament/Pages/Installer/PanelInstaller.php @@ -2,6 +2,7 @@ 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\DatabaseStep; @@ -13,7 +14,6 @@ use App\Traits\CheckMigrationsTrait; use App\Traits\EnvironmentWriterTrait; use Exception; -use Filament\Facades\Filament; use Filament\Forms\Components\Actions\Action; use Filament\Forms\Components\Wizard; use Filament\Forms\Concerns\InteractsWithForms; @@ -24,6 +24,8 @@ 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; @@ -37,7 +39,7 @@ class PanelInstaller extends SimplePage implements HasForms use EnvironmentWriterTrait; use InteractsWithForms; - public $data = []; + public array $data = []; protected static string $view = 'filament.pages.installer'; @@ -54,7 +56,7 @@ public static function isInstalled(): bool return env('APP_INSTALLED', true); } - public function mount() + public function mount(): void { abort_if(self::isInstalled(), 404); @@ -93,7 +95,7 @@ protected function getFormStatePath(): ?string return 'data'; } - public function submit() + public function submit(): Redirector|RedirectResponse { // Disable installer $this->writeToEnvironment(['APP_INSTALLED' => 'true']); @@ -103,7 +105,7 @@ public function submit() auth()->guard()->login($this->user, true); // Redirect to admin panel - return redirect(Filament::getPanel('admin')->getUrl()); + return redirect(Dashboard::getUrl()); } public function writeToEnv(string $key): void @@ -159,12 +161,12 @@ public function runMigrations(string $driver): void } } - public function createAdminUser(): void + public function createAdminUser(UserCreationService $userCreationService): void { try { $userData = array_get($this->data, 'user'); $userData['root_admin'] = true; - $this->user = app(UserCreationService::class)->handle($userData); + $this->user = $userCreationService->handle($userData); } catch (Exception $exception) { report($exception); diff --git a/app/Filament/Pages/Installer/Steps/AdminUserStep.php b/app/Filament/Pages/Installer/Steps/AdminUserStep.php index f5c2a19b34..5bdfe38d94 100644 --- a/app/Filament/Pages/Installer/Steps/AdminUserStep.php +++ b/app/Filament/Pages/Installer/Steps/AdminUserStep.php @@ -3,6 +3,7 @@ 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; @@ -28,6 +29,6 @@ public static function make(PanelInstaller $installer): Step ->password() ->revealable(), ]) - ->afterValidation(fn () => $installer->createAdminUser()); + ->afterValidation(fn (UserCreationService $service) => $installer->createAdminUser($service)); } } diff --git a/app/Filament/Pages/Installer/Steps/DatabaseStep.php b/app/Filament/Pages/Installer/Steps/DatabaseStep.php index 29c47b3290..3a4449777e 100644 --- a/app/Filament/Pages/Installer/Steps/DatabaseStep.php +++ b/app/Filament/Pages/Installer/Steps/DatabaseStep.php @@ -30,25 +30,25 @@ public static function make(PanelInstaller $installer): Step ->label('Database Host') ->hintIcon('tabler-question-mark') ->hintIconTooltip('The host of your database. Make sure it is reachable.') - ->required() - ->default(env('DB_HOST', '127.0.0.1')) + ->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'), TextInput::make('env_database.DB_PORT') ->label('Database Port') ->hintIcon('tabler-question-mark') ->hintIconTooltip('The port of your database.') - ->required() + ->required(fn (Get $get) => $get('env_general.DB_CONNECTION') !== 'sqlite') ->numeric() ->minValue(1) ->maxValue(65535) - ->default(env('DB_PORT', 3306)) + ->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'), TextInput::make('env_database.DB_USERNAME') ->label('Database Username') ->hintIcon('tabler-question-mark') ->hintIconTooltip('The name of your database user.') - ->required() - ->default(env('DB_USERNAME', 'pelican')) + ->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'), TextInput::make('env_database.DB_PASSWORD') ->label('Database Password') @@ -56,7 +56,7 @@ public static function make(PanelInstaller $installer): Step ->hintIconTooltip('The password of your database user. Can be empty.') ->password() ->revealable() - ->default(env('DB_PASSWORD')) + ->default(fn (Get $get) => $get('env_general.DB_CONNECTION') !== 'sqlite' ? env('DB_PASSWORD') : null) ->hidden(fn (Get $get) => $get('env_general.DB_CONNECTION') === 'sqlite'), ]) ->afterValidation(function (Get $get) use ($installer) { @@ -72,7 +72,7 @@ public static function make(PanelInstaller $installer): Step }); } - private static function testConnection(string $driver, $host, $port, $database, $username, $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; diff --git a/app/Filament/Pages/Installer/Steps/RedisStep.php b/app/Filament/Pages/Installer/Steps/RedisStep.php index 00ecb18afe..2f257c4016 100644 --- a/app/Filament/Pages/Installer/Steps/RedisStep.php +++ b/app/Filament/Pages/Installer/Steps/RedisStep.php @@ -56,7 +56,7 @@ public static function make(PanelInstaller $installer): Step }); } - private static function testConnection($host, $port, $username, $password): bool + private static function testConnection(string $host, null|string|int $port, ?string $username, ?string $password): bool { try { config()->set('database.redis._panel_install_test', [ diff --git a/app/Filament/Pages/Settings.php b/app/Filament/Pages/Settings.php index f3ef2c2b9a..f8d18a01c2 100644 --- a/app/Filament/Pages/Settings.php +++ b/app/Filament/Pages/Settings.php @@ -38,6 +38,7 @@ class Settings extends Page implements HasForms use InteractsWithHeaderActions; protected static ?string $navigationIcon = 'tabler-settings'; + protected static ?string $navigationGroup = 'Advanced'; protected static string $view = 'filament.pages.settings'; diff --git a/app/Filament/Resources/ApiKeyResource.php b/app/Filament/Resources/ApiKeyResource.php index ed9219f902..46350714eb 100644 --- a/app/Filament/Resources/ApiKeyResource.php +++ b/app/Filament/Resources/ApiKeyResource.php @@ -5,12 +5,16 @@ use App\Filament\Resources\ApiKeyResource\Pages; use App\Models\ApiKey; use Filament\Resources\Resource; +use Illuminate\Database\Eloquent\Model; class ApiKeyResource extends Resource { protected static ?string $model = ApiKey::class; + protected static ?string $label = 'API Key'; + protected static ?string $navigationIcon = 'tabler-key'; + protected static ?string $navigationGroup = 'Advanced'; public static function getNavigationBadge(): ?string @@ -18,7 +22,7 @@ public static function getNavigationBadge(): ?string return static::getModel()::where('key_type', ApiKey::TYPE_APPLICATION)->count() ?: null; } - public static function canEdit($record): bool + public static function canEdit(Model $record): bool { return false; } diff --git a/app/Filament/Resources/DatabaseHostResource.php b/app/Filament/Resources/DatabaseHostResource.php index 7576fbc3a8..790895983a 100644 --- a/app/Filament/Resources/DatabaseHostResource.php +++ b/app/Filament/Resources/DatabaseHostResource.php @@ -13,6 +13,7 @@ class DatabaseHostResource extends Resource protected static ?string $label = 'Database Host'; protected static ?string $navigationIcon = 'tabler-database'; + protected static ?string $navigationGroup = 'Advanced'; public static function getNavigationBadge(): ?string diff --git a/app/Filament/Resources/DatabaseHostResource/Pages/CreateDatabaseHost.php b/app/Filament/Resources/DatabaseHostResource/Pages/CreateDatabaseHost.php index d3e07bcb93..606b9ca82b 100644 --- a/app/Filament/Resources/DatabaseHostResource/Pages/CreateDatabaseHost.php +++ b/app/Filament/Resources/DatabaseHostResource/Pages/CreateDatabaseHost.php @@ -4,6 +4,8 @@ use App\Filament\Resources\DatabaseHostResource; use App\Services\Databases\Hosts\HostCreationService; +use Closure; +use Exception; use Filament\Forms; use Filament\Forms\Components\Section; use Filament\Forms\Components\Select; @@ -16,6 +18,8 @@ class CreateDatabaseHost extends CreateRecord { + private HostCreationService $service; + protected static string $resource = DatabaseHostResource::class; protected ?string $heading = 'Database Hosts'; @@ -24,6 +28,11 @@ class CreateDatabaseHost extends CreateRecord protected ?string $subheading = '(database servers that can have individual databases)'; + public function boot(HostCreationService $service): void + { + $this->service = $service; + } + public function form(Form $form): Form { return $form @@ -94,10 +103,10 @@ protected function getFormActions(): array protected function handleRecordCreation(array $data): Model { - return resolve(HostCreationService::class)->handle($data); + return $this->service->handle($data); } - public function exception($e, $stopPropagation): void + public function exception(Exception $e, Closure $stopPropagation): void { if ($e instanceof PDOException) { Notification::make() diff --git a/app/Filament/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php b/app/Filament/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php index 862c33b8cb..5d0176699a 100644 --- a/app/Filament/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php +++ b/app/Filament/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php @@ -6,6 +6,8 @@ use App\Filament\Resources\DatabaseHostResource\RelationManagers\DatabasesRelationManager; use App\Models\DatabaseHost; use App\Services\Databases\Hosts\HostUpdateService; +use Closure; +use Exception; use Filament\Actions; use Filament\Forms; use Filament\Forms\Components\Section; @@ -21,6 +23,13 @@ class EditDatabaseHost extends EditRecord { protected static string $resource = DatabaseHostResource::class; + private HostUpdateService $hostUpdateService; + + public function boot(HostUpdateService $hostUpdateService): void + { + $this->hostUpdateService = $hostUpdateService; + } + public function form(Form $form): Form { return $form @@ -97,12 +106,16 @@ public function getRelationManagers(): array ]; } - protected function handleRecordUpdate($record, array $data): Model + protected function handleRecordUpdate(Model $record, array $data): Model { - return resolve(HostUpdateService::class)->handle($record->id, $data); + if (!$record instanceof DatabaseHost) { + return $record; + } + + return $this->hostUpdateService->handle($record, $data); } - public function exception($e, $stopPropagation): void + public function exception(Exception $e, Closure $stopPropagation): void { if ($e instanceof PDOException) { Notification::make() diff --git a/app/Filament/Resources/DatabaseHostResource/Pages/ListDatabaseHosts.php b/app/Filament/Resources/DatabaseHostResource/Pages/ListDatabaseHosts.php index 384f9cda63..c5db9ea855 100644 --- a/app/Filament/Resources/DatabaseHostResource/Pages/ListDatabaseHosts.php +++ b/app/Filament/Resources/DatabaseHostResource/Pages/ListDatabaseHosts.php @@ -32,13 +32,16 @@ public function table(Table $table): Table ->sortable(), TextColumn::make('username') ->searchable(), - TextColumn::make('max_databases') - ->numeric() - ->sortable(), + TextColumn::make('databases_count') + ->counts('databases') + ->icon('tabler-database') + ->label('Databases'), TextColumn::make('node.name') - ->numeric() + ->icon('tabler-server-2') + ->placeholder('No Nodes') ->sortable(), ]) + ->checkIfRecordIsSelectableUsing(fn (DatabaseHost $databaseHost) => !$databaseHost->databases_count) ->actions([ EditAction::make(), ]) diff --git a/app/Filament/Resources/DatabaseHostResource/RelationManagers/DatabasesRelationManager.php b/app/Filament/Resources/DatabaseHostResource/RelationManagers/DatabasesRelationManager.php index 4dfcdd955b..17cdb62487 100644 --- a/app/Filament/Resources/DatabaseHostResource/RelationManagers/DatabasesRelationManager.php +++ b/app/Filament/Resources/DatabaseHostResource/RelationManagers/DatabasesRelationManager.php @@ -8,6 +8,7 @@ use Filament\Forms\Components\TextInput; use Filament\Forms\Form; use Filament\Forms\Get; +use Filament\Forms\Set; use Filament\Resources\RelationManagers\RelationManager; use Filament\Tables\Actions\DeleteAction; use Filament\Tables\Actions\ViewAction; @@ -40,6 +41,7 @@ public function form(Form $form): Form ->formatStateUsing(fn (Get $get, Database $database) => 'jdbc:mysql://' . $get('username') . ':' . urlencode($database->password) . '@' . $database->host->host . ':' . $database->host->port . '/' . $get('database')), ]); } + public function table(Table $table): Table { return $table @@ -60,7 +62,7 @@ public function table(Table $table): Table ]); } - protected function rotatePassword(DatabasePasswordService $service, Database $database, $set, $get): void + protected function rotatePassword(DatabasePasswordService $service, Database $database, Set $set, Get $get): void { $newPassword = $service->handle($database); $jdbcString = 'jdbc:mysql://' . $get('username') . ':' . urlencode($newPassword) . '@' . $database->host->host . ':' . $database->host->port . '/' . $get('database'); diff --git a/app/Filament/Resources/DatabaseResource.php b/app/Filament/Resources/DatabaseResource.php index 92580dfdff..912f6fe939 100644 --- a/app/Filament/Resources/DatabaseResource.php +++ b/app/Filament/Resources/DatabaseResource.php @@ -13,6 +13,7 @@ class DatabaseResource extends Resource protected static ?string $navigationIcon = 'tabler-database'; protected static bool $shouldRegisterNavigation = false; + protected static ?string $navigationGroup = 'Advanced'; public static function getNavigationBadge(): ?string diff --git a/app/Filament/Resources/DatabaseResource/Pages/CreateDatabase.php b/app/Filament/Resources/DatabaseResource/Pages/CreateDatabase.php index 0e85e0a6a0..71d57d3611 100644 --- a/app/Filament/Resources/DatabaseResource/Pages/CreateDatabase.php +++ b/app/Filament/Resources/DatabaseResource/Pages/CreateDatabase.php @@ -21,9 +21,12 @@ public function form(Form $form): Form ->searchable() ->preload() ->required(), - TextInput::make('database_host_id') - ->required() - ->numeric(), + Select::make('database_host_id') + ->relationship('host', 'name') + ->searchable() + ->selectablePlaceholder(false) + ->preload() + ->required(), TextInput::make('database') ->required() ->maxLength(255), diff --git a/app/Filament/Resources/EggResource/Pages/CreateEgg.php b/app/Filament/Resources/EggResource/Pages/CreateEgg.php index a663540cb5..4ec91ea24c 100644 --- a/app/Filament/Resources/EggResource/Pages/CreateEgg.php +++ b/app/Filament/Resources/EggResource/Pages/CreateEgg.php @@ -27,6 +27,7 @@ class CreateEgg extends CreateRecord protected static string $resource = EggResource::class; protected static bool $canCreateAnother = false; + public function form(Form $form): Form { return $form diff --git a/app/Filament/Resources/EggResource/Pages/EditEgg.php b/app/Filament/Resources/EggResource/Pages/EditEgg.php index d1d59c0640..28a09103c0 100644 --- a/app/Filament/Resources/EggResource/Pages/EditEgg.php +++ b/app/Filament/Resources/EggResource/Pages/EditEgg.php @@ -280,10 +280,7 @@ protected function getHeaderActions(): array ->contained(false), ]) - ->action(function (array $data, Egg $egg): void { - /** @var EggImporterService $eggImportService */ - $eggImportService = resolve(EggImporterService::class); - + ->action(function (array $data, Egg $egg, EggImporterService $eggImportService): void { if (!empty($data['egg'])) { try { $eggImportService->fromFile($data['egg'], $egg); diff --git a/app/Filament/Resources/EggResource/Pages/ListEggs.php b/app/Filament/Resources/EggResource/Pages/ListEggs.php index 1a6a3b6509..0069abed98 100644 --- a/app/Filament/Resources/EggResource/Pages/ListEggs.php +++ b/app/Filament/Resources/EggResource/Pages/ListEggs.php @@ -14,7 +14,7 @@ use Filament\Forms\Components\TextInput; use Filament\Notifications\Notification; use Filament\Resources\Pages\ListRecords; -use Filament\Tables; +use Filament\Tables\Actions\Action; use Filament\Tables\Actions\BulkActionGroup; use Filament\Tables\Actions\DeleteBulkAction; use Filament\Tables\Actions\EditAction; @@ -49,7 +49,7 @@ public function table(Table $table): Table ]) ->actions([ EditAction::make(), - Tables\Actions\Action::make('export') + Action::make('export') ->icon('tabler-download') ->label('Export') ->color('primary') @@ -57,6 +57,39 @@ public function table(Table $table): Table echo $service->handle($egg->id); }, 'egg-' . $egg->getKebabName() . '.json')) ->authorize(fn () => auth()->user()->can('export egg')), + Action::make('update') + ->icon('tabler-cloud-download') + ->label('Update') + ->color('success') + ->requiresConfirmation() + ->modalHeading('Are you sure you want to update this egg?') + ->modalDescription('If you made any changes to the egg they will be overwritten!') + ->modalIconColor('danger') + ->modalSubmitAction(fn (Actions\StaticAction $action) => $action->color('danger')) + ->action(function (Egg $egg, EggImporterService $eggImporterService) { + try { + $eggImporterService->fromUrl($egg->update_url, $egg); + + cache()->forget("eggs.{$egg->uuid}.update"); + } catch (Exception $exception) { + Notification::make() + ->title('Egg Update failed') + ->body($exception->getMessage()) + ->danger() + ->send(); + + report($exception); + + return; + } + + Notification::make() + ->title('Egg updated') + ->success() + ->send(); + }) + ->authorize(fn () => auth()->user()->can('import egg')) + ->visible(fn (Egg $egg) => cache()->get("eggs.{$egg->uuid}.update", false)), ]) ->bulkActions([ BulkActionGroup::make([ @@ -65,6 +98,7 @@ public function table(Table $table): Table ]), ]); } + protected function getHeaderActions(): array { return [ @@ -97,10 +131,7 @@ protected function getHeaderActions(): array ->contained(false), ]) - ->action(function (array $data): void { - /** @var EggImporterService $eggImportService */ - $eggImportService = resolve(EggImporterService::class); - + ->action(function (array $data, EggImporterService $eggImportService): void { if (!empty($data['egg'])) { /** @var TemporaryUploadedFile[] $eggFile */ $eggFile = $data['egg']; diff --git a/app/Filament/Resources/MountResource.php b/app/Filament/Resources/MountResource.php index a2d7a187e3..a30f586e1a 100644 --- a/app/Filament/Resources/MountResource.php +++ b/app/Filament/Resources/MountResource.php @@ -11,6 +11,7 @@ class MountResource extends Resource protected static ?string $model = Mount::class; protected static ?string $navigationIcon = 'tabler-layers-linked'; + protected static ?string $navigationGroup = 'Advanced'; public static function getNavigationBadge(): ?string diff --git a/app/Filament/Resources/MountResource/Pages/EditMount.php b/app/Filament/Resources/MountResource/Pages/EditMount.php index 71c6febb5c..e44fb581b4 100644 --- a/app/Filament/Resources/MountResource/Pages/EditMount.php +++ b/app/Filament/Resources/MountResource/Pages/EditMount.php @@ -96,6 +96,7 @@ public function form(Form $form): Form 'lg' => 2, ]); } + protected function getHeaderActions(): array { return [ diff --git a/app/Filament/Resources/MountResource/Pages/ListMounts.php b/app/Filament/Resources/MountResource/Pages/ListMounts.php index d39c5d972d..10bfbc6dd0 100644 --- a/app/Filament/Resources/MountResource/Pages/ListMounts.php +++ b/app/Filament/Resources/MountResource/Pages/ListMounts.php @@ -17,6 +17,7 @@ class ListMounts extends ListRecords { protected static string $resource = MountResource::class; + public function table(Table $table): Table { return $table @@ -56,6 +57,7 @@ public function table(Table $table): Table ->button(), ]); } + protected function getHeaderActions(): array { return [ diff --git a/app/Filament/Resources/NodeResource/Pages/CreateNode.php b/app/Filament/Resources/NodeResource/Pages/CreateNode.php index 10fc403280..c8d9bbc30a 100644 --- a/app/Filament/Resources/NodeResource/Pages/CreateNode.php +++ b/app/Filament/Resources/NodeResource/Pages/CreateNode.php @@ -398,7 +398,7 @@ public function form(Forms\Form $form): Forms\Form protected function getRedirectUrlParameters(): array { return [ - 'tab' => '-configuration-tab', + 'tab' => '-configuration-file-tab', ]; } diff --git a/app/Filament/Resources/NodeResource/Pages/EditNode.php b/app/Filament/Resources/NodeResource/Pages/EditNode.php index 3b28aab8bf..11f77abfe9 100644 --- a/app/Filament/Resources/NodeResource/Pages/EditNode.php +++ b/app/Filament/Resources/NodeResource/Pages/EditNode.php @@ -85,7 +85,7 @@ public function form(Forms\Form $form): Forms\Form if (request()->isSecure()) { return ' Your panel is currently secured via an SSL certificate and that means your nodes require one too. - You must use a domain name, because you cannot get SSL certificates for IP Addresses + You must use a domain name, because you cannot get SSL certificates for IP Addresses. '; } @@ -100,7 +100,7 @@ public function form(Forms\Form $form): Forms\Form ->hintColor('danger') ->hint(function ($state) { if (is_ip($state) && request()->isSecure()) { - return 'You cannot connect to an IP Address over SSL'; + return 'You cannot connect to an IP Address over SSL!'; } return ''; @@ -132,11 +132,9 @@ public function form(Forms\Form $form): Forms\Form $set('dns', false); }) ->maxLength(255), - TextInput::make('ip') ->disabled() ->hidden(), - ToggleButtons::make('dns') ->label('DNS Record Check') ->helperText('This lets you know if your DNS record correctly points to an IP Address.') @@ -159,7 +157,6 @@ public function form(Forms\Form $form): Forms\Form 'md' => 1, 'lg' => 1, ]), - TextInput::make('daemon_listen') ->columnSpan([ 'default' => 1, @@ -174,7 +171,6 @@ public function form(Forms\Form $form): Forms\Form ->default(8080) ->required() ->integer(), - TextInput::make('name') ->label('Display Name') ->columnSpan([ @@ -186,7 +182,6 @@ public function form(Forms\Form $form): Forms\Form ->required() ->helperText('This name is for display only and can be changed later.') ->maxLength(100), - ToggleButtons::make('scheme') ->label('Communicate over SSL') ->columnSpan([ @@ -490,6 +485,16 @@ protected function mutateFormDataBeforeFill(array $data): array $data['config'] = $node->getYamlConfiguration(); + if (!is_ip($node->fqdn)) { + $validRecords = gethostbynamel($node->fqdn); + if ($validRecords) { + $data['dns'] = true; + $data['ip'] = collect($validRecords)->first(); + } else { + $data['dns'] = false; + } + } + return $data; } @@ -497,6 +502,7 @@ protected function getFormActions(): array { return []; } + protected function getHeaderActions(): array { return [ @@ -512,11 +518,12 @@ protected function afterSave(): void $this->fillForm(); } - protected function getColumnSpan() + protected function getColumnSpan(): ?int { return null; } - protected function getColumnStart() + + protected function getColumnStart(): ?int { return null; } diff --git a/app/Filament/Resources/NodeResource/RelationManagers/AllocationsRelationManager.php b/app/Filament/Resources/NodeResource/RelationManagers/AllocationsRelationManager.php index 01cc5e50e5..5cdf2b1721 100644 --- a/app/Filament/Resources/NodeResource/RelationManagers/AllocationsRelationManager.php +++ b/app/Filament/Resources/NodeResource/RelationManagers/AllocationsRelationManager.php @@ -148,7 +148,7 @@ public function table(Table $table): Table ->splitKeys(['Tab', ' ', ',']) ->required(), ]) - ->action(fn (array $data) => resolve(AssignmentService::class)->handle($this->getOwnerRecord(), $data)), + ->action(fn (array $data, AssignmentService $service) => $service->handle($this->getOwnerRecord(), $data)), ]) ->bulkActions([ BulkActionGroup::make([ diff --git a/app/Filament/Resources/NodeResource/Widgets/NodeCpuChart.php b/app/Filament/Resources/NodeResource/Widgets/NodeCpuChart.php index 476b8b5336..89d1cb254f 100644 --- a/app/Filament/Resources/NodeResource/Widgets/NodeCpuChart.php +++ b/app/Filament/Resources/NodeResource/Widgets/NodeCpuChart.php @@ -12,6 +12,7 @@ class NodeCpuChart extends ChartWidget { protected static ?string $pollingInterval = '5s'; + protected static ?string $maxHeight = '300px'; public ?Model $record = null; diff --git a/app/Filament/Resources/NodeResource/Widgets/NodeMemoryChart.php b/app/Filament/Resources/NodeResource/Widgets/NodeMemoryChart.php index 9d75fd1167..0cabbed517 100644 --- a/app/Filament/Resources/NodeResource/Widgets/NodeMemoryChart.php +++ b/app/Filament/Resources/NodeResource/Widgets/NodeMemoryChart.php @@ -12,6 +12,7 @@ class NodeMemoryChart extends ChartWidget { protected static ?string $pollingInterval = '5s'; + protected static ?string $maxHeight = '300px'; public ?Model $record = null; diff --git a/app/Filament/Resources/NodeResource/Widgets/NodeStorageChart.php b/app/Filament/Resources/NodeResource/Widgets/NodeStorageChart.php index b841d84ef0..61db8c98d0 100644 --- a/app/Filament/Resources/NodeResource/Widgets/NodeStorageChart.php +++ b/app/Filament/Resources/NodeResource/Widgets/NodeStorageChart.php @@ -9,7 +9,9 @@ class NodeStorageChart extends ChartWidget { protected static ?string $heading = 'Storage'; + protected static ?string $pollingInterval = '60s'; + protected static ?string $maxHeight = '300px'; public ?Model $record = null; diff --git a/app/Filament/Resources/ServerResource/Pages/CreateServer.php b/app/Filament/Resources/ServerResource/Pages/CreateServer.php index c1f6aa22e6..c5a5997177 100644 --- a/app/Filament/Resources/ServerResource/Pages/CreateServer.php +++ b/app/Filament/Resources/ServerResource/Pages/CreateServer.php @@ -45,10 +45,18 @@ class CreateServer extends CreateRecord { protected static string $resource = ServerResource::class; + protected static bool $canCreateAnother = false; public ?Node $node = null; + private ServerCreationService $serverCreationService; + + public function boot(ServerCreationService $serverCreationService): void + { + $this->serverCreationService = $serverCreationService; + } + public function form(Form $form): Form { return $form @@ -118,8 +126,9 @@ public function form(Form $form): Form ->hintIconTooltip('Providing a user password is optional. New user email will prompt users to create a password the first time they login.') ->password(), ]) - ->createOptionUsing(function ($data) { - resolve(UserCreationService::class)->handle($data); + ->createOptionUsing(function ($data, UserCreationService $service) { + $service->handle($data); + $this->refreshForm(); }) ->required(), @@ -262,9 +271,9 @@ public function form(Form $form): Form ->splitKeys(['Tab', ' ', ',']) ->required(), ]) - ->createOptionUsing(function (array $data, Get $get): int { + ->createOptionUsing(function (array $data, Get $get, AssignmentService $assignmentService): int { return collect( - resolve(AssignmentService::class)->handle(Node::find($get('node_id')), $data) + $assignmentService->handle(Node::find($get('node_id')), $data) )->first(); }) ->required(), @@ -618,14 +627,24 @@ public function form(Form $form): Form ->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) @@ -672,6 +691,36 @@ public function form(Form $form): Form ->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() @@ -825,10 +874,7 @@ protected function handleRecordCreation(array $data): Model { $data['allocation_additional'] = collect($data['allocation_additional'])->filter()->all(); - /** @var ServerCreationService $service */ - $service = resolve(ServerCreationService::class); - - return $service->handle($data); + return $this->serverCreationService->handle($data); } private function shouldHideComponent(Get $get, Component $component): bool diff --git a/app/Filament/Resources/ServerResource/Pages/EditServer.php b/app/Filament/Resources/ServerResource/Pages/EditServer.php index 0fdb603fa7..5c0ff49185 100644 --- a/app/Filament/Resources/ServerResource/Pages/EditServer.php +++ b/app/Filament/Resources/ServerResource/Pages/EditServer.php @@ -24,10 +24,12 @@ use Filament\Forms\Components\CheckboxList; use Filament\Forms\Components\Fieldset; use Filament\Forms\Components\Grid; +use Filament\Forms\Components\Hidden; 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 +265,23 @@ public function form(Form $form): Form ->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 +326,41 @@ public function form(Form $form): Form ->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() @@ -741,6 +783,7 @@ protected function transferServer(Form $form): Form ]); } + protected function getHeaderActions(): array { return [ @@ -749,11 +792,12 @@ protected function getHeaderActions(): array ->color('danger') ->label('Delete') ->requiresConfirmation() - ->action(function (Server $server) { - resolve(ServerDeletionService::class)->handle($server); + ->action(function (Server $server, ServerDeletionService $service) { + $service->handle($server); return redirect(ListServers::getUrl()); - }), + }) + ->authorize(fn (Server $server) => auth()->user()->can('delete server', $server)), Actions\Action::make('console') ->label('Console') ->icon('tabler-terminal') @@ -762,6 +806,7 @@ protected function getHeaderActions(): array ]; } + protected function getFormActions(): array { return []; @@ -812,7 +857,7 @@ private function getSelectOptionsFromRules(ServerVariable $serverVariable): arra ->all(); } - protected function rotatePassword(DatabasePasswordService $service, $record, $set, $get): void + protected function rotatePassword(DatabasePasswordService $service, Database $record, Set $set, Get $get): void { $newPassword = $service->handle($record); $jdbcString = 'jdbc:mysql://' . $get('username') . ':' . urlencode($newPassword) . '@' . $record->host->host . ':' . $record->host->port . '/' . $get('database'); diff --git a/app/Filament/Resources/ServerResource/Pages/ListServers.php b/app/Filament/Resources/ServerResource/Pages/ListServers.php index 326499898a..dd6fc4a16a 100644 --- a/app/Filament/Resources/ServerResource/Pages/ListServers.php +++ b/app/Filament/Resources/ServerResource/Pages/ListServers.php @@ -96,6 +96,7 @@ public function table(Table $table): Table ->button(), ]); } + protected function getHeaderActions(): array { return [ diff --git a/app/Filament/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php b/app/Filament/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php index 2060c127f3..c43132b585 100644 --- a/app/Filament/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php +++ b/app/Filament/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php @@ -57,13 +57,13 @@ public function table(Table $table): Table 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([ @@ -144,7 +144,7 @@ public function table(Table $table): Table ->splitKeys(['Tab', ' ', ',']) ->required(), ]) - ->action(fn (array $data) => resolve(AssignmentService::class)->handle($this->getOwnerRecord()->node, $data, $this->getOwnerRecord())), + ->action(fn (array $data, AssignmentService $service) => $service->handle($this->getOwnerRecord()->node, $data, $this->getOwnerRecord())), AssociateAction::make() ->multiple() ->associateAnother(false) diff --git a/app/Filament/Resources/UserResource/Pages/EditProfile.php b/app/Filament/Resources/UserResource/Pages/EditProfile.php index f5c5f5828d..5b35deff32 100644 --- a/app/Filament/Resources/UserResource/Pages/EditProfile.php +++ b/app/Filament/Resources/UserResource/Pages/EditProfile.php @@ -13,7 +13,9 @@ use chillerlan\QRCode\Common\Version; use chillerlan\QRCode\QRCode; use chillerlan\QRCode\QROptions; +use Closure; use DateTimeZone; +use Exception; use Filament\Forms\Components\Actions\Action; use Filament\Forms\Components\Grid; use Filament\Forms\Components\Placeholder; @@ -38,6 +40,13 @@ */ class EditProfile extends \Filament\Pages\Auth\EditProfile { + private ToggleTwoFactorService $toggleTwoFactorService; + + public function boot(ToggleTwoFactorService $toggleTwoFactorService): void + { + $this->toggleTwoFactorService = $toggleTwoFactorService; + } + protected function getForms(): array { return [ @@ -106,7 +115,7 @@ protected function getForms(): array Tab::make('2FA') ->icon('tabler-shield-lock') - ->schema(function () { + ->schema(function (TwoFactorSetupService $setupService) { if ($this->getUser()->use_totp) { return [ Placeholder::make('2fa-already-enabled') @@ -124,8 +133,6 @@ protected function getForms(): array ->helperText('Enter your current 2FA code to disable Two Factor Authentication'), ]; } - /** @var TwoFactorSetupService */ - $setupService = app(TwoFactorSetupService::class); ['image_url_data' => $url, 'secret' => $secret] = cache()->remember( "users.{$this->getUser()->id}.2fa.state", @@ -274,23 +281,21 @@ protected function getForms(): array ]; } - protected function handleRecordUpdate($record, $data): Model + protected function handleRecordUpdate(Model $record, array $data): Model { - if ($token = $data['2facode'] ?? null) { - /** @var ToggleTwoFactorService $service */ - $service = resolve(ToggleTwoFactorService::class); + if (!$record instanceof User) { + return $record; + } - $tokens = $service->handle($record, $token, true); + if ($token = $data['2facode'] ?? null) { + $tokens = $this->toggleTwoFactorService->handle($record, $token, true); cache()->set("users.$record->id.2fa.tokens", implode("\n", $tokens), now()->addSeconds(15)); $this->redirectRoute('filament.admin.auth.profile', ['tab' => '-2fa-tab']); } if ($token = $data['2fa-disable-code'] ?? null) { - /** @var ToggleTwoFactorService $service */ - $service = resolve(ToggleTwoFactorService::class); - - $service->handle($record, $token, false); + $this->toggleTwoFactorService->handle($record, $token, false); cache()->forget("users.$record->id.2fa.state"); } @@ -298,7 +303,7 @@ protected function handleRecordUpdate($record, $data): Model return parent::handleRecordUpdate($record, $data); } - public function exception($e, $stopPropagation): void + public function exception(Exception $e, Closure $stopPropagation): void { if ($e instanceof TwoFactorAuthenticationTokenInvalid) { Notification::make() diff --git a/app/Filament/Resources/UserResource/Pages/EditUser.php b/app/Filament/Resources/UserResource/Pages/EditUser.php index 777f20d730..3fa6a55712 100644 --- a/app/Filament/Resources/UserResource/Pages/EditUser.php +++ b/app/Filament/Resources/UserResource/Pages/EditUser.php @@ -18,6 +18,7 @@ class EditUser extends EditRecord { protected static string $resource = UserResource::class; + public function form(Form $form): Form { return $form @@ -46,6 +47,7 @@ public function form(Form $form): Form ])->columns(), ]); } + protected function getHeaderActions(): array { return [ diff --git a/app/Filament/Resources/UserResource/Pages/ListUsers.php b/app/Filament/Resources/UserResource/Pages/ListUsers.php index 3d9a1af185..5105302369 100644 --- a/app/Filament/Resources/UserResource/Pages/ListUsers.php +++ b/app/Filament/Resources/UserResource/Pages/ListUsers.php @@ -78,6 +78,7 @@ public function table(Table $table): Table ]), ]); } + protected function getHeaderActions(): array { return [ @@ -110,13 +111,11 @@ protected function getHeaderActions(): array ]), ]) ->successRedirectUrl(route('filament.admin.resources.users.index')) - ->action(function (array $data) { + ->action(function (array $data, UserCreationService $creationService) { $roles = $data['roles']; $roles = collect($roles)->map(fn ($role) => Role::findById($role)); unset($data['roles']); - /** @var UserCreationService $creationService */ - $creationService = resolve(UserCreationService::class); $user = $creationService->handle($data); $user->syncRoles($roles); diff --git a/app/Filament/Resources/UserResource/RelationManagers/ServersRelationManager.php b/app/Filament/Resources/UserResource/RelationManagers/ServersRelationManager.php index 98c69ba185..01a73c3738 100644 --- a/app/Filament/Resources/UserResource/RelationManagers/ServersRelationManager.php +++ b/app/Filament/Resources/UserResource/RelationManagers/ServersRelationManager.php @@ -32,18 +32,18 @@ public function table(Table $table): Table ) ->label('Suspend All Servers') ->color('warning') - ->action(function () use ($user) { + ->action(function (SuspensionService $suspensionService) use ($user) { foreach ($user->servers()->whereNot('status', ServerState::Suspended)->get() as $server) { - resolve(SuspensionService::class)->toggle($server); + $suspensionService->toggle($server); } }), Actions\Action::make('toggleUnsuspend') ->hidden(fn () => $user->servers()->where('status', ServerState::Suspended)->count() === 0) ->label('Unsuspend All Servers') ->color('primary') - ->action(function () use ($user) { + ->action(function (SuspensionService $suspensionService) use ($user) { foreach ($user->servers()->where('status', ServerState::Suspended)->get() as $server) { - resolve(SuspensionService::class)->toggle($server, SuspensionService::ACTION_UNSUSPEND); + $suspensionService->toggle($server, SuspensionService::ACTION_UNSUSPEND); } }), ]) diff --git a/app/Helpers/Utilities.php b/app/Helpers/Utilities.php index e0c824d01a..c810788cc2 100644 --- a/app/Helpers/Utilities.php +++ b/app/Helpers/Utilities.php @@ -40,7 +40,7 @@ public static function getScheduleNextRunDate(string $minute, string $hour, stri { 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 diff --git a/app/Http/Controllers/Admin/Eggs/EggController.php b/app/Http/Controllers/Admin/Eggs/EggController.php index 692ff79424..2f3de5ae1c 100644 --- a/app/Http/Controllers/Admin/Eggs/EggController.php +++ b/app/Http/Controllers/Admin/Eggs/EggController.php @@ -127,7 +127,7 @@ public function destroy(Egg $egg): RedirectResponse /** * Normalizes a string of docker image data into the expected egg format. */ - protected function normalizeDockerImages(string $input = null): array + protected function normalizeDockerImages(?string $input = null): array { $data = array_map(fn ($value) => trim($value), explode("\n", $input ?? '')); diff --git a/app/Http/Controllers/Admin/Nodes/NodeViewController.php b/app/Http/Controllers/Admin/Nodes/NodeViewController.php index 95dc7ede0a..115b7b6b1d 100644 --- a/app/Http/Controllers/Admin/Nodes/NodeViewController.php +++ b/app/Http/Controllers/Admin/Nodes/NodeViewController.php @@ -15,6 +15,7 @@ class NodeViewController extends Controller use JavascriptInjection; public const THRESHOLD_PERCENTAGE_LOW = 75; + public const THRESHOLD_PERCENTAGE_MEDIUM = 90; /** diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index 9ddd9c7e61..9c7836e39a 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -70,7 +70,7 @@ public function setDetails(Request $request, Server $server): RedirectResponse * @throws \App\Exceptions\DisplayException * @throws \App\Exceptions\Model\DataValidationException */ - public function toggleInstall(Server $server) + public function toggleInstall(Server $server): void { if ($server->status === ServerState::InstallFailed) { throw new DisplayException(trans('admin/server.exceptions.marked_as_failed')); @@ -84,8 +84,6 @@ public function toggleInstall(Server $server) ->body(trans('admin/server.alerts.install_toggled')) ->success() ->send(); - - return null; } /** @@ -94,7 +92,7 @@ public function toggleInstall(Server $server) * @throws \App\Exceptions\DisplayException * @throws \App\Exceptions\Model\DataValidationException */ - public function reinstallServer(Server $server) + public function reinstallServer(Server $server): void { $this->reinstallService->handle($server); diff --git a/app/Http/Controllers/Api/Application/ApplicationApiController.php b/app/Http/Controllers/Api/Application/ApplicationApiController.php index 174e6243a4..0078105803 100644 --- a/app/Http/Controllers/Api/Application/ApplicationApiController.php +++ b/app/Http/Controllers/Api/Application/ApplicationApiController.php @@ -40,7 +40,7 @@ public function __construct() * Perform dependency injection of certain classes needed for core functionality * without littering the constructors of classes that extend this abstract. */ - public function loadDependencies(Fractal $fractal, Request $request) + public function loadDependencies(Fractal $fractal, Request $request): void { $this->fractal = $fractal; $this->request = $request; @@ -51,8 +51,7 @@ public function loadDependencies(Fractal $fractal, Request $request) * * @template T of \App\Transformers\Api\Application\BaseTransformer * - * @param class-string $abstract - * + * @param class-string $abstract * @return T * * @noinspection PhpDocSignatureInspection diff --git a/app/Http/Controllers/Api/Client/ClientApiController.php b/app/Http/Controllers/Api/Client/ClientApiController.php index 4cf081fec3..8d409a754e 100644 --- a/app/Http/Controllers/Api/Client/ClientApiController.php +++ b/app/Http/Controllers/Api/Client/ClientApiController.php @@ -41,8 +41,7 @@ protected function parseIncludes(): array * * @template T of \App\Transformers\Api\Client\BaseClientTransformer * - * @param class-string $abstract - * + * @param class-string $abstract * @return T * * @noinspection PhpDocSignatureInspection diff --git a/app/Http/Controllers/Api/Remote/ActivityProcessingController.php b/app/Http/Controllers/Api/Remote/ActivityProcessingController.php index e07584d23f..3b6badaa5a 100644 --- a/app/Http/Controllers/Api/Remote/ActivityProcessingController.php +++ b/app/Http/Controllers/Api/Remote/ActivityProcessingController.php @@ -14,7 +14,7 @@ class ActivityProcessingController extends Controller { - public function __invoke(ActivityEventRequest $request) + public function __invoke(ActivityEventRequest $request): void { $tz = Carbon::now()->getTimezone(); diff --git a/app/Http/Controllers/Auth/AbstractLoginController.php b/app/Http/Controllers/Auth/AbstractLoginController.php index e5b50d0adf..4a6fa719f1 100644 --- a/app/Http/Controllers/Auth/AbstractLoginController.php +++ b/app/Http/Controllers/Auth/AbstractLoginController.php @@ -51,7 +51,7 @@ public function __construct() * * @throws \App\Exceptions\DisplayException */ - protected function sendFailedLoginResponse(Request $request, Authenticatable $user = null, string $message = null) + protected function sendFailedLoginResponse(Request $request, ?Authenticatable $user = null, ?string $message = null): never { $this->incrementLoginAttempts($request); $this->fireFailedLoginEvent($user, [ @@ -91,7 +91,7 @@ protected function sendLoginResponse(User $user, Request $request): JsonResponse /** * Determine if the user is logging in using an email or username. */ - protected function getField(string $input = null): string + protected function getField(?string $input = null): string { return ($input && str_contains($input, '@')) ? 'email' : 'username'; } @@ -99,7 +99,7 @@ protected function getField(string $input = null): string /** * Fire a failed login event. */ - protected function fireFailedLoginEvent(Authenticatable $user = null, array $credentials = []) + protected function fireFailedLoginEvent(?Authenticatable $user = null, array $credentials = []): void { Event::dispatch(new Failed('auth', $user, $credentials)); } diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php index 21ef4fee85..f58aded823 100644 --- a/app/Http/Controllers/Auth/ForgotPasswordController.php +++ b/app/Http/Controllers/Auth/ForgotPasswordController.php @@ -16,7 +16,7 @@ class ForgotPasswordController extends Controller /** * Get the response for a failed password reset link. */ - protected function sendResetLinkFailedResponse(Request $request, $response): JsonResponse + 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 @@ -28,10 +28,8 @@ protected function sendResetLinkFailedResponse(Request $request, $response): Jso /** * Get the response for a successful password reset link. - * - * @param string $response */ - protected function sendResetLinkResponse(Request $request, $response): JsonResponse + protected function sendResetLinkResponse(Request $request, string $response): JsonResponse { return response()->json([ 'status' => trans($response), diff --git a/app/Http/Controllers/Auth/LoginCheckpointController.php b/app/Http/Controllers/Auth/LoginCheckpointController.php index 540aca5a00..8797f533be 100644 --- a/app/Http/Controllers/Auth/LoginCheckpointController.php +++ b/app/Http/Controllers/Auth/LoginCheckpointController.php @@ -72,7 +72,7 @@ public function __invoke(LoginCheckpointRequest $request): JsonResponse } } - return $this->sendFailedLoginResponse($request, $user, !empty($recoveryToken) ? 'The recovery token provided is not valid.' : null); + $this->sendFailedLoginResponse($request, $user, !empty($recoveryToken) ? 'The recovery token provided is not valid.' : null); } /** diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 28c3c4f5ee..ea2a595943 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -4,11 +4,13 @@ use App\Filament\Pages\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 { @@ -17,7 +19,7 @@ class LoginController extends AbstractLoginController * base authentication view component. React will take over at this point and * turn the login area into an SPA. */ - public function index() + public function index(): View|RedirectResponse { if (!PanelInstaller::isInstalled()) { return redirect('/installer'); diff --git a/app/Http/Controllers/Auth/ResetPasswordController.php b/app/Http/Controllers/Auth/ResetPasswordController.php index b4c338ee98..da8f5b7931 100644 --- a/app/Http/Controllers/Auth/ResetPasswordController.php +++ b/app/Http/Controllers/Auth/ResetPasswordController.php @@ -64,12 +64,12 @@ function ($user, $password) { * 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 + * @param \Illuminate\Contracts\Auth\CanResetPassword|\App\Models\User $user + * @param string $password * * @throws \App\Exceptions\Model\DataValidationException */ - protected function resetPassword($user, $password) + protected function resetPassword($user, $password): void { /** @var User $user */ $user->password = $this->hasher->make($password); diff --git a/app/Http/Middleware/EnsureStatefulRequests.php b/app/Http/Middleware/EnsureStatefulRequests.php index 66689fe11e..35c1795cc5 100644 --- a/app/Http/Middleware/EnsureStatefulRequests.php +++ b/app/Http/Middleware/EnsureStatefulRequests.php @@ -15,7 +15,7 @@ class EnsureStatefulRequests extends EnsureFrontendRequestsAreStateful * We don't want to support API usage using the cookies, except for requests stemming * from the front-end we control. */ - public static function fromFrontend($request) + public static function fromFrontend($request): bool { if (parent::fromFrontend($request)) { return true; diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php index 38af84f7e0..f645f552ed 100644 --- a/app/Http/Middleware/RedirectIfAuthenticated.php +++ b/app/Http/Middleware/RedirectIfAuthenticated.php @@ -17,7 +17,7 @@ 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()) { return redirect()->route('index'); diff --git a/app/Http/Middleware/RequireTwoFactorAuthentication.php b/app/Http/Middleware/RequireTwoFactorAuthentication.php index 470cc73b30..58ff1564d6 100644 --- a/app/Http/Middleware/RequireTwoFactorAuthentication.php +++ b/app/Http/Middleware/RequireTwoFactorAuthentication.php @@ -10,7 +10,9 @@ class RequireTwoFactorAuthentication { public const LEVEL_NONE = 0; + public const LEVEL_ADMIN = 1; + public const LEVEL_ALL = 2; /** diff --git a/app/Http/Middleware/VerifyReCaptcha.php b/app/Http/Middleware/VerifyReCaptcha.php index bfe09ce984..4ba454ce4c 100644 --- a/app/Http/Middleware/VerifyReCaptcha.php +++ b/app/Http/Middleware/VerifyReCaptcha.php @@ -3,23 +3,26 @@ 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 Symfony\Component\HttpKernel\Exception\HttpException; -class VerifyReCaptcha +readonly class VerifyReCaptcha { - /** - * Handle an incoming request. - */ + public function __construct(private Application $app) + { + + } + public function handle(Request $request, \Closure $next): mixed { if (!config('recaptcha.enabled')) { return $next($request); } - if (app()->isLocal()) { + if ($this->app->isLocal()) { return $next($request); } diff --git a/app/Http/Requests/Admin/AdminFormRequest.php b/app/Http/Requests/Admin/AdminFormRequest.php index 54e9f5e92c..adfb97fb8e 100644 --- a/app/Http/Requests/Admin/AdminFormRequest.php +++ b/app/Http/Requests/Admin/AdminFormRequest.php @@ -28,7 +28,7 @@ public function authorize(): bool * 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 + public function normalize(?array $only = null): array { return $this->only($only ?? array_keys($this->rules())); } diff --git a/app/Http/Requests/Admin/Egg/EggFormRequest.php b/app/Http/Requests/Admin/Egg/EggFormRequest.php index d1af8a0852..aae5ae0136 100644 --- a/app/Http/Requests/Admin/Egg/EggFormRequest.php +++ b/app/Http/Requests/Admin/Egg/EggFormRequest.php @@ -3,6 +3,7 @@ namespace App\Http\Requests\Admin\Egg; use App\Http\Requests\Admin\AdminFormRequest; +use Illuminate\Validation\Validator; class EggFormRequest extends AdminFormRequest { @@ -25,7 +26,7 @@ public function rules(): array return $rules; } - public function withValidator($validator) + public function withValidator(Validator $validator): void { $validator->sometimes('config_from', 'exists:eggs,id', function () { return (int) $this->input('config_from') !== 0; diff --git a/app/Http/Requests/Admin/Settings/MailSettingsFormRequest.php b/app/Http/Requests/Admin/Settings/MailSettingsFormRequest.php index 9e62869d83..38fa792d5d 100644 --- a/app/Http/Requests/Admin/Settings/MailSettingsFormRequest.php +++ b/app/Http/Requests/Admin/Settings/MailSettingsFormRequest.php @@ -27,7 +27,7 @@ public function rules(): array * Override the default normalization function for this type of request * as we need to accept empty values on the keys. */ - public function normalize(array $only = null): array + public function normalize(?array $only = null): array { $keys = array_flip(array_keys($this->rules())); diff --git a/app/Http/Requests/Api/Application/ApplicationApiRequest.php b/app/Http/Requests/Api/Application/ApplicationApiRequest.php index a0e17c15dd..b8feb094dd 100644 --- a/app/Http/Requests/Api/Application/ApplicationApiRequest.php +++ b/app/Http/Requests/Api/Application/ApplicationApiRequest.php @@ -74,8 +74,7 @@ public function withValidator(Validator $validator): void * * @template T of \Illuminate\Database\Eloquent\Model * - * @param class-string $expect - * + * @param class-string $expect * @return T * * @noinspection PhpDocSignatureInspection diff --git a/app/Http/Requests/Api/Application/DatabaseHosts/StoreDatabaseHostRequest.php b/app/Http/Requests/Api/Application/DatabaseHosts/StoreDatabaseHostRequest.php index aedd5109ba..435a6d86bf 100644 --- a/app/Http/Requests/Api/Application/DatabaseHosts/StoreDatabaseHostRequest.php +++ b/app/Http/Requests/Api/Application/DatabaseHosts/StoreDatabaseHostRequest.php @@ -12,7 +12,7 @@ class StoreDatabaseHostRequest extends ApplicationApiRequest protected int $permission = AdminAcl::WRITE; - public function rules(array $rules = null): array + public function rules(?array $rules = null): array { return $rules ?? DatabaseHost::getRules(); } diff --git a/app/Http/Requests/Api/Application/DatabaseHosts/UpdateDatabaseHostRequest.php b/app/Http/Requests/Api/Application/DatabaseHosts/UpdateDatabaseHostRequest.php index 111560bbfd..f14b9b90ff 100644 --- a/app/Http/Requests/Api/Application/DatabaseHosts/UpdateDatabaseHostRequest.php +++ b/app/Http/Requests/Api/Application/DatabaseHosts/UpdateDatabaseHostRequest.php @@ -6,11 +6,11 @@ class UpdateDatabaseHostRequest extends StoreDatabaseHostRequest { - public function rules(array $rules = null): array + public function rules(?array $rules = null): array { /** @var DatabaseHost $databaseHost */ $databaseHost = $this->route()->parameter('database_host'); - return $rules ?? DatabaseHost::getRulesForUpdate($databaseHost->id); + return $rules ?? DatabaseHost::getRulesForUpdate($databaseHost); } } diff --git a/app/Http/Requests/Api/Application/Mounts/UpdateMountRequest.php b/app/Http/Requests/Api/Application/Mounts/UpdateMountRequest.php index 7f76ec4201..05e7d0a235 100644 --- a/app/Http/Requests/Api/Application/Mounts/UpdateMountRequest.php +++ b/app/Http/Requests/Api/Application/Mounts/UpdateMountRequest.php @@ -9,11 +9,11 @@ class UpdateMountRequest extends StoreMountRequest /** * Apply validation rules to this request. */ - public function rules(array $rules = null): array + public function rules(?array $rules = null): array { /** @var Mount $mount */ $mount = $this->route()->parameter('mount'); - return Mount::getRulesForUpdate($mount->id); + return Mount::getRulesForUpdate($mount); } } diff --git a/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php b/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php index 913204b836..eae3242335 100644 --- a/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php +++ b/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php @@ -15,7 +15,7 @@ class StoreNodeRequest extends ApplicationApiRequest /** * Validation rules to apply to this request. */ - public function rules(array $rules = null): array + public function rules(?array $rules = null): array { return collect($rules ?? Node::getRules())->only([ 'public', diff --git a/app/Http/Requests/Api/Application/Nodes/UpdateNodeRequest.php b/app/Http/Requests/Api/Application/Nodes/UpdateNodeRequest.php index ab636dff02..5907860a2b 100644 --- a/app/Http/Requests/Api/Application/Nodes/UpdateNodeRequest.php +++ b/app/Http/Requests/Api/Application/Nodes/UpdateNodeRequest.php @@ -10,11 +10,11 @@ class UpdateNodeRequest extends StoreNodeRequest * Apply validation rules to this request. Uses the parent class rules() * function but passes in the rules for updating rather than creating. */ - public function rules(array $rules = null): array + public function rules(?array $rules = null): array { /** @var Node $node */ $node = $this->route()->parameter('node'); - return parent::rules(Node::getRulesForUpdate($node->id)); + return parent::rules(Node::getRulesForUpdate($node)); } } diff --git a/app/Http/Requests/Api/Application/Roles/StoreRoleRequest.php b/app/Http/Requests/Api/Application/Roles/StoreRoleRequest.php index 7968449a73..4cf01400ec 100644 --- a/app/Http/Requests/Api/Application/Roles/StoreRoleRequest.php +++ b/app/Http/Requests/Api/Application/Roles/StoreRoleRequest.php @@ -11,7 +11,7 @@ class StoreRoleRequest extends ApplicationApiRequest protected int $permission = AdminAcl::WRITE; - public function rules(array $rules = null): array + public function rules(?array $rules = null): array { return [ 'name' => 'required|string', diff --git a/app/Http/Requests/Api/Application/Users/AssignUserRolesRequest.php b/app/Http/Requests/Api/Application/Users/AssignUserRolesRequest.php index c0af95102b..61e191c203 100644 --- a/app/Http/Requests/Api/Application/Users/AssignUserRolesRequest.php +++ b/app/Http/Requests/Api/Application/Users/AssignUserRolesRequest.php @@ -7,7 +7,7 @@ class AssignUserRolesRequest extends StoreUserRequest /** * Return the validation rules for this request. */ - public function rules(array $rules = null): array + public function rules(?array $rules = null): array { return [ 'roles' => 'array', diff --git a/app/Http/Requests/Api/Application/Users/StoreUserRequest.php b/app/Http/Requests/Api/Application/Users/StoreUserRequest.php index 43603639c4..cf1b6c244e 100644 --- a/app/Http/Requests/Api/Application/Users/StoreUserRequest.php +++ b/app/Http/Requests/Api/Application/Users/StoreUserRequest.php @@ -15,7 +15,7 @@ class StoreUserRequest extends ApplicationApiRequest /** * Return the validation rules for this request. */ - public function rules(array $rules = null): array + public function rules(?array $rules = null): array { $rules = $rules ?? User::getRules(); diff --git a/app/Http/Requests/Api/Application/Users/UpdateUserRequest.php b/app/Http/Requests/Api/Application/Users/UpdateUserRequest.php index ecccbdd5aa..9ec5067a50 100644 --- a/app/Http/Requests/Api/Application/Users/UpdateUserRequest.php +++ b/app/Http/Requests/Api/Application/Users/UpdateUserRequest.php @@ -9,10 +9,10 @@ class UpdateUserRequest extends StoreUserRequest /** * Return the validation rules for this request. */ - public function rules(array $rules = null): array + public function rules(?array $rules = null): array { - $userId = $this->parameter('user', User::class)->id; + $user = $this->parameter('user', User::class); - return parent::rules(User::getRulesForUpdate($userId)); + return parent::rules(User::getRulesForUpdate($user)); } } diff --git a/app/Http/Requests/Api/Client/Servers/Subusers/SubuserRequest.php b/app/Http/Requests/Api/Client/Servers/Subusers/SubuserRequest.php index bd68fb8908..349db06168 100644 --- a/app/Http/Requests/Api/Client/Servers/Subusers/SubuserRequest.php +++ b/app/Http/Requests/Api/Client/Servers/Subusers/SubuserRequest.php @@ -49,7 +49,7 @@ public function authorize(): bool * * @throws \Illuminate\Contracts\Container\BindingResolutionException */ - protected function validatePermissionsCanBeAssigned(array $permissions) + protected function validatePermissionsCanBeAssigned(array $permissions): void { $user = $this->user(); /** @var \App\Models\Server $server */ diff --git a/app/Jobs/NodeStatistics.php b/app/Jobs/NodeStatistics.php index 19fae9b9db..9546f0ba0d 100644 --- a/app/Jobs/NodeStatistics.php +++ b/app/Jobs/NodeStatistics.php @@ -42,5 +42,4 @@ public function handle(): void } } } - } diff --git a/app/Jobs/Schedule/RunTaskJob.php b/app/Jobs/Schedule/RunTaskJob.php index 6a660e5c05..158950c45d 100644 --- a/app/Jobs/Schedule/RunTaskJob.php +++ b/app/Jobs/Schedule/RunTaskJob.php @@ -90,7 +90,7 @@ public function handle( /** * Handle a failure while sending the action to the daemon or otherwise processing the job. */ - public function failed(\Exception $exception = null) + public function failed(?\Exception $exception = null): void { $this->markTaskNotQueued(); $this->markScheduleComplete(); @@ -99,7 +99,7 @@ public function failed(\Exception $exception = null) /** * Get the next task in the schedule and queue it for running after the defined period of wait time. */ - private function queueNextTask() + private function queueNextTask(): void { /** @var \App\Models\Task|null $nextTask */ $nextTask = Task::query()->where('schedule_id', $this->task->schedule_id) @@ -121,7 +121,7 @@ private function queueNextTask() /** * Marks the parent schedule as being complete. */ - private function markScheduleComplete() + private function markScheduleComplete(): void { $this->task->schedule()->update([ 'is_processing' => false, @@ -132,7 +132,7 @@ private function markScheduleComplete() /** * Mark a specific task as no longer being queued. */ - private function markTaskNotQueued() + private function markTaskNotQueued(): void { $this->task->update(['is_queued' => false]); } diff --git a/app/Livewire/NodeSystemInformation.php b/app/Livewire/NodeSystemInformation.php index b291902510..55f71f7ce5 100644 --- a/app/Livewire/NodeSystemInformation.php +++ b/app/Livewire/NodeSystemInformation.php @@ -3,19 +3,21 @@ namespace App\Livewire; use App\Models\Node; +use Illuminate\View\View; use Livewire\Component; class NodeSystemInformation extends Component { public Node $node; + public string $sizeClasses; - public function render() + public function render(): View { return view('livewire.node-system-information'); } - public function placeholder() + public function placeholder(): string { return <<<'HTML'
diff --git a/app/Models/ActivityLog.php b/app/Models/ActivityLog.php index bb6149c142..20fb579e06 100644 --- a/app/Models/ActivityLog.php +++ b/app/Models/ActivityLog.php @@ -123,7 +123,7 @@ public function scopeForActor(Builder $builder, IlluminateModel $actor): Builder * * @see https://laravel.com/docs/9.x/eloquent#pruning-models */ - public function prunable() + public function prunable(): Builder { if (is_null(config('activity.prune_days'))) { throw new \LogicException('Cannot prune activity logs: no "prune_days" configuration value is set.'); @@ -136,7 +136,7 @@ public function prunable() * Boots the model event listeners. This will trigger an activity log event every * time a new model is inserted which can then be captured and worked with as needed. */ - protected static function boot() + protected static function boot(): void { parent::boot(); @@ -149,7 +149,7 @@ protected static function boot() }); } - public function htmlable() + public function htmlable(): string { $user = $this->actor; if (!$user instanceof User) { diff --git a/app/Models/ActivityLogSubject.php b/app/Models/ActivityLogSubject.php index 037d344f05..f6a7291fae 100644 --- a/app/Models/ActivityLogSubject.php +++ b/app/Models/ActivityLogSubject.php @@ -3,8 +3,9 @@ namespace App\Models; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\Relations\Pivot; -use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Database\Eloquent\SoftDeletingScope; /** * \App\Models\ActivityLogSubject. @@ -25,6 +26,7 @@ class ActivityLogSubject extends Pivot { public $incrementing = true; + public $timestamps = false; protected $table = 'activity_log_subjects'; @@ -36,15 +38,8 @@ public function activityLog(): BelongsTo return $this->belongsTo(ActivityLog::class); } - public function subject() + public function subject(): MorphTo { - $morph = $this->morphTo(); - - if (in_array(SoftDeletes::class, class_uses_recursive($morph::class))) { - /** @var self|Backup|UserSSHKey $morph - cannot use traits in doc blocks */ - return $morph->withTrashed(); - } - - return $morph; + return $this->morphTo()->withoutGlobalScope(SoftDeletingScope::class); } } diff --git a/app/Models/ApiKey.php b/app/Models/ApiKey.php index fbc81a1824..edf8ebb439 100644 --- a/app/Models/ApiKey.php +++ b/app/Models/ApiKey.php @@ -63,6 +63,7 @@ class ApiKey extends Model * API representation using fractal. */ public const RESOURCE_NAME = 'api_key'; + /** * Maximum number of Api keys that a user can have. */ @@ -71,12 +72,22 @@ class ApiKey extends Model * Different API keys that can exist on the system. */ public const TYPE_NONE = 0; + public const TYPE_ACCOUNT = 1; + public const TYPE_APPLICATION = 2; + + /* @deprecated */ + public const TYPE_DAEMON_USER = 3; + + /* @deprecated */ + public const TYPE_DAEMON_APPLICATION = 4; + /** * The length of API key identifiers. */ public const IDENTIFIER_LENGTH = 16; + /** * The length of the actual API key that is encrypted and stored * in the database. diff --git a/app/Models/Backup.php b/app/Models/Backup.php index 3f2a931d1e..d6e4be65a2 100644 --- a/app/Models/Backup.php +++ b/app/Models/Backup.php @@ -32,6 +32,7 @@ class Backup extends Model public const RESOURCE_NAME = 'backup'; public const ADAPTER_DAEMON = 'wings'; + public const ADAPTER_AWS_S3 = 's3'; protected $table = 'backups'; diff --git a/app/Models/Egg.php b/app/Models/Egg.php index e8c672064f..001be85e3f 100644 --- a/app/Models/Egg.php +++ b/app/Models/Egg.php @@ -70,6 +70,7 @@ class Egg extends Model * than leaving it null. */ public const FEATURE_EULA_POPUP = 'eula'; + public const FEATURE_FASTDL = 'fastdl'; /** diff --git a/app/Models/Filters/AdminServerFilter.php b/app/Models/Filters/AdminServerFilter.php index d3c1eea606..e90f17e081 100644 --- a/app/Models/Filters/AdminServerFilter.php +++ b/app/Models/Filters/AdminServerFilter.php @@ -11,9 +11,9 @@ class AdminServerFilter implements Filter * A multi-column filter for the servers table that allows an administrative user to search * across UUID, name, owner username, and owner email. * - * @param string $value + * @param string $value */ - public function __invoke(Builder $query, $value, string $property) + public function __invoke(Builder $query, $value, string $property): void { if ($query->getQuery()->from !== 'servers') { throw new \BadMethodCallException('Cannot use the AdminServerFilter against a non-server model.'); diff --git a/app/Models/Filters/MultiFieldServerFilter.php b/app/Models/Filters/MultiFieldServerFilter.php index da3f91f6d5..89bb15f49c 100644 --- a/app/Models/Filters/MultiFieldServerFilter.php +++ b/app/Models/Filters/MultiFieldServerFilter.php @@ -19,9 +19,9 @@ class MultiFieldServerFilter implements Filter * search across multiple columns. This allows us to provide a very generic search ability for * the frontend. * - * @param string $value + * @param string $value */ - public function __invoke(Builder $query, $value, string $property) + public function __invoke(Builder $query, $value, string $property): void { if ($query->getQuery()->from !== 'servers') { throw new \BadMethodCallException('Cannot use the MultiFieldServerFilter against a non-server model.'); diff --git a/app/Models/Model.php b/app/Models/Model.php index 536edf9b57..20bc2bf334 100644 --- a/app/Models/Model.php +++ b/app/Models/Model.php @@ -38,7 +38,7 @@ abstract class Model extends IlluminateModel * * @throws \Illuminate\Contracts\Container\BindingResolutionException */ - protected static function boot() + protected static function boot(): void { parent::boot(); @@ -69,7 +69,7 @@ public function getRouteKeyName(): string return 'uuid'; } - protected function asDateTime($value) + protected function asDateTime($value): Carbon { $timezone = auth()->user()?->timezone ?? config('app.timezone', 'UTC'); @@ -135,11 +135,9 @@ public static function getRulesForField(string $field): array * Returns the rules associated with the model, specifically for updating the given model * rather than just creating it. */ - public static function getRulesForUpdate($model, string $column = 'id'): array + public static function getRulesForUpdate(self $model): array { - if ($model instanceof Model) { - [$id, $column] = [$model->getKey(), $model->getKeyName()]; - } + [$id, $column] = [$model->getKey(), $model->getKeyName()]; $rules = static::getRules(); foreach ($rules as $key => &$data) { diff --git a/app/Models/Mount.php b/app/Models/Mount.php index 2236038059..60cb2b436c 100644 --- a/app/Models/Mount.php +++ b/app/Models/Mount.php @@ -70,7 +70,7 @@ public static function getRules(): array /** * Blacklisted source paths. */ - public static $invalidSourcePaths = [ + public static array $invalidSourcePaths = [ '/etc/pelican', '/var/lib/pelican/volumes', '/srv/daemon-data', @@ -79,7 +79,7 @@ public static function getRules(): array /** * Blacklisted target paths. */ - public static $invalidTargetPaths = [ + public static array $invalidTargetPaths = [ '/home/container', ]; diff --git a/app/Models/Node.php b/app/Models/Node.php index 0a9613196d..64f44e3af0 100644 --- a/app/Models/Node.php +++ b/app/Models/Node.php @@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Notifications\Notifiable; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Http; use Illuminate\Support\Str; use Symfony\Component\Yaml\Yaml; @@ -53,6 +54,7 @@ class Node extends Model public const RESOURCE_NAME = 'node'; public const DAEMON_TOKEN_ID_LENGTH = 16; + public const DAEMON_TOKEN_LENGTH = 64; /** @@ -135,7 +137,9 @@ protected function casts(): array } public int $servers_sum_memory = 0; + public int $servers_sum_disk = 0; + public int $servers_sum_cpu = 0; public function getRouteKeyName(): string @@ -268,7 +272,7 @@ public function isViable(int $memory, int $disk, int $cpu): bool return true; } - public static function getForServerCreation() + public static function getForServerCreation(): Collection { return self::with('allocations')->get()->map(function (Node $item) { $filtered = $item->getRelation('allocations')->where('server_id', null)->map(function ($map) { @@ -294,6 +298,7 @@ public function systemInformation(): array { return once(function () { try { + // @phpstan-ignore-next-line return resolve(DaemonConfigurationRepository::class) ->setNode($this) ->getSystemInformation(connectTimeout: 3); @@ -330,7 +335,7 @@ public function serverStatuses(): array return $statuses; } - public function statistics() + public function statistics(): array { $default = [ 'memory_total' => 0, diff --git a/app/Models/Objects/DeploymentObject.php b/app/Models/Objects/DeploymentObject.php index b7a4ebaf3f..692ca74873 100644 --- a/app/Models/Objects/DeploymentObject.php +++ b/app/Models/Objects/DeploymentObject.php @@ -45,5 +45,4 @@ public function setTags(array $tags): self return $this; } - } diff --git a/app/Models/Permission.php b/app/Models/Permission.php index ab9fef2607..29cbd826d3 100644 --- a/app/Models/Permission.php +++ b/app/Models/Permission.php @@ -16,51 +16,81 @@ class Permission extends Model * Constants defining different permissions available. */ public const ACTION_WEBSOCKET_CONNECT = 'websocket.connect'; + public const ACTION_CONTROL_CONSOLE = 'control.console'; + public const ACTION_CONTROL_START = 'control.start'; + public const ACTION_CONTROL_STOP = 'control.stop'; + public const ACTION_CONTROL_RESTART = 'control.restart'; public const ACTION_DATABASE_READ = 'database.read'; + public const ACTION_DATABASE_CREATE = 'database.create'; + public const ACTION_DATABASE_UPDATE = 'database.update'; + public const ACTION_DATABASE_DELETE = 'database.delete'; + public const ACTION_DATABASE_VIEW_PASSWORD = 'database.view_password'; public const ACTION_SCHEDULE_READ = 'schedule.read'; + public const ACTION_SCHEDULE_CREATE = 'schedule.create'; + public const ACTION_SCHEDULE_UPDATE = 'schedule.update'; + public const ACTION_SCHEDULE_DELETE = 'schedule.delete'; public const ACTION_USER_READ = 'user.read'; + public const ACTION_USER_CREATE = 'user.create'; + public const ACTION_USER_UPDATE = 'user.update'; + public const ACTION_USER_DELETE = 'user.delete'; public const ACTION_BACKUP_READ = 'backup.read'; + public const ACTION_BACKUP_CREATE = 'backup.create'; + public const ACTION_BACKUP_DELETE = 'backup.delete'; + public const ACTION_BACKUP_DOWNLOAD = 'backup.download'; + public const ACTION_BACKUP_RESTORE = 'backup.restore'; public const ACTION_ALLOCATION_READ = 'allocation.read'; + public const ACTION_ALLOCATION_CREATE = 'allocation.create'; + public const ACTION_ALLOCATION_UPDATE = 'allocation.update'; + public const ACTION_ALLOCATION_DELETE = 'allocation.delete'; public const ACTION_FILE_READ = 'file.read'; + public const ACTION_FILE_READ_CONTENT = 'file.read-content'; + public const ACTION_FILE_CREATE = 'file.create'; + public const ACTION_FILE_UPDATE = 'file.update'; + public const ACTION_FILE_DELETE = 'file.delete'; + public const ACTION_FILE_ARCHIVE = 'file.archive'; + public const ACTION_FILE_SFTP = 'file.sftp'; public const ACTION_STARTUP_READ = 'startup.read'; + public const ACTION_STARTUP_UPDATE = 'startup.update'; + public const ACTION_STARTUP_DOCKER_IMAGE = 'startup.docker-image'; public const ACTION_SETTINGS_RENAME = 'settings.rename'; + public const ACTION_SETTINGS_REINSTALL = 'settings.reinstall'; public const ACTION_ACTIVITY_READ = 'activity.read'; diff --git a/app/Models/Schedule.php b/app/Models/Schedule.php index ae7b9baecf..d6d19a8e14 100644 --- a/app/Models/Schedule.php +++ b/app/Models/Schedule.php @@ -2,8 +2,7 @@ namespace App\Models; -use Cron\CronExpression; -use Carbon\CarbonImmutable; +use App\Helpers\Utilities; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -112,13 +111,9 @@ public function getRouteKeyName(): string * * @throws \Exception */ - public function getNextRunDate(): CarbonImmutable + public function getNextRunDate(): string { - $formatted = sprintf('%s %s %s %s %s', $this->cron_minute, $this->cron_hour, $this->cron_day_of_month, $this->cron_month, $this->cron_day_of_week); - - return CarbonImmutable::createFromTimestamp( - (new CronExpression($formatted))->getNextRunDate()->getTimestamp() - ); + return Utilities::getScheduleNextRunDate($this->cron_minute, $this->cron_hour, $this->cron_day_of_month, $this->cron_month, $this->cron_day_of_week)->toDateTimeString(); } /** diff --git a/app/Models/Server.php b/app/Models/Server.php index a0c341c6d4..67b7190324 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -71,6 +71,7 @@ * @property \App\Models\User $user * @property \Illuminate\Database\Eloquent\Collection|\App\Models\EggVariable[] $variables * @property int|null $variables_count + * * @method static \Database\Factories\ServerFactory factory(...$parameters) * @method static \Illuminate\Database\Eloquent\Builder|Server newModelQuery() * @method static \Illuminate\Database\Eloquent\Builder|Server newQuery() @@ -101,6 +102,7 @@ * @method static \Illuminate\Database\Eloquent\Builder|Server whereUpdatedAt($value) * @method static \Illuminate\Database\Eloquent\Builder|Server whereUuid($value) * @method static \Illuminate\Database\Eloquent\Builder|Server whereuuid_short($value) + * * @property array|null $docker_labels * @property string|null $ports * @property-read mixed $condition @@ -108,10 +110,12 @@ * @property-read int|null $egg_variables_count * @property-read \Illuminate\Database\Eloquent\Collection $serverVariables * @property-read int|null $server_variables_count + * * @method static \Illuminate\Database\Eloquent\Builder|Server whereDockerLabels($value) * @method static \Illuminate\Database\Eloquent\Builder|Server whereInstalledAt($value) * @method static \Illuminate\Database\Eloquent\Builder|Server wherePorts($value) * @method static \Illuminate\Database\Eloquent\Builder|Server whereUuidShort($value) + * * @mixin \Eloquent */ class Server extends Model @@ -363,7 +367,7 @@ public function resolveChildRouteBinding($childType, $value, $field) * * @throws ServerStateConflictException */ - public function validateCurrentState() + public function validateCurrentState(): void { if ( $this->isSuspended() || @@ -382,7 +386,7 @@ public function validateCurrentState() * sure the server can be transferred and is not currently being transferred * or installed. */ - public function validateTransferState() + public function validateTransferState(): void { if ( !$this->isInstalled() || diff --git a/app/Models/Task.php b/app/Models/Task.php index 254d38ece8..202a3165ab 100644 --- a/app/Models/Task.php +++ b/app/Models/Task.php @@ -31,8 +31,11 @@ class Task extends Model * The default actions that can exist for a task */ public const ACTION_POWER = 'power'; + public const ACTION_COMMAND = 'command'; + public const ACTION_BACKUP = 'backup'; + public const ACTION_DELETE_FILES = 'delete_files'; /** diff --git a/app/Models/User.php b/app/Models/User.php index 395f4994fa..45545db348 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -99,6 +99,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac use Notifiable; public const USER_LEVEL_USER = 0; + public const USER_LEVEL_ADMIN = 1; /** @@ -233,9 +234,9 @@ public function toReactObject(): array /** * Send the password reset notification. * - * @param string $token + * @param string $token */ - public function sendPasswordResetNotification($token) + public function sendPasswordResetNotification($token): void { Activity::event('auth:reset-password') ->withRequestMetadata() @@ -248,7 +249,7 @@ public function sendPasswordResetNotification($token) /** * Store the username as a lowercase string. */ - public function setUsernameAttribute(string $value) + public function setUsernameAttribute(string $value): void { $this->attributes['username'] = mb_strtolower($value); } diff --git a/app/PHPStan/ForbiddenGlobalFunctionsRule.php b/app/PHPStan/ForbiddenGlobalFunctionsRule.php new file mode 100644 index 0000000000..545cc60fb7 --- /dev/null +++ b/app/PHPStan/ForbiddenGlobalFunctionsRule.php @@ -0,0 +1,38 @@ +forbiddenFunctions = $forbiddenFunctions; + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + /** @var FuncCall $node */ + if ($node->name instanceof Node\Name) { + $functionName = (string) $node->name; + if (in_array($functionName, $this->forbiddenFunctions, true)) { + return [ + sprintf('Usage of global function "%s" is forbidden.', $functionName), + ]; + } + } + + return []; + } +} diff --git a/app/Policies/ServerPolicy.php b/app/Policies/ServerPolicy.php index dbc2ae4028..dfa72c9b98 100644 --- a/app/Policies/ServerPolicy.php +++ b/app/Policies/ServerPolicy.php @@ -41,7 +41,7 @@ public function before(User $user, string $ability, string|Server $server): ?boo * not call the before() function if there isn't a function matching the * policy permission. */ - public function __call(string $name, mixed $arguments) + public function __call(string $name, mixed $arguments): void { // do nothing } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 20a6b0dd51..4ead65cbbd 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -7,13 +7,13 @@ use App\Models\ApiKey; use App\Models\Node; use App\Models\User; -use App\Services\Helpers\SoftwareVersionService; use Dedoc\Scramble\Scramble; use Dedoc\Scramble\Support\Generator\OpenApi; use Dedoc\Scramble\Support\Generator\SecurityScheme; use Filament\Support\Colors\Color; use Filament\Support\Facades\FilamentColor; use Illuminate\Database\Eloquent\Relations\Relation; +use Illuminate\Foundation\Application; use Illuminate\Pagination\Paginator; use Illuminate\Support\Facades\Broadcast; use Illuminate\Support\Facades\Event; @@ -30,11 +30,11 @@ class AppServiceProvider extends ServiceProvider /** * Bootstrap any application services. */ - public function boot(): void + public function boot(Application $app): void { - $versionData = app(SoftwareVersionService::class)->versionData(); - View::share('appVersion', $versionData['version'] ?? 'undefined'); - View::share('appIsGit', $versionData['is_git'] ?? false); + // TODO: remove when old admin area gets yeeted + View::share('appVersion', config('app.version')); + View::share('appIsGit', false); Paginator::useBootstrap(); @@ -65,7 +65,7 @@ public function boot(): void ->asJson() ->withToken($node->daemon_token) ->withHeaders($headers) - ->withOptions(['verify' => (bool) app()->environment('production')]) + ->withOptions(['verify' => (bool) $app->environment('production')]) ->timeout(config('panel.guzzle.timeout')) ->connectTimeout(config('panel.guzzle.connect_timeout')) ->baseUrl($node->getConnectionAddress()) diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 0aef2f030a..f6b7c568b0 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -9,6 +9,7 @@ use Filament\Http\Middleware\DispatchServingFilamentEvent; use Filament\Panel; use Filament\PanelProvider; +use Filament\Support\Enums\MaxWidth; use Filament\Support\Facades\FilamentAsset; use Filament\Widgets; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; @@ -21,7 +22,7 @@ class AdminPanelProvider extends PanelProvider { - public function boot() + public function boot(): void { FilamentAsset::registerCssVariables([ 'sidebar-width' => '16rem !important', @@ -43,6 +44,8 @@ public function panel(Panel $panel): Panel ->brandLogo(config('app.logo')) ->brandLogoHeight('2rem') ->profile(EditProfile::class, false) + ->maxContentWidth(MaxWidth::ScreenTwoExtraLarge) + ->spa() ->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources') ->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages') ->discoverClusters(in: app_path('Filament/Clusters'), for: 'App\\Filament\\Clusters') diff --git a/app/Repositories/Daemon/DaemonBackupRepository.php b/app/Repositories/Daemon/DaemonBackupRepository.php index bbe852e809..ff34c2acd2 100644 --- a/app/Repositories/Daemon/DaemonBackupRepository.php +++ b/app/Repositories/Daemon/DaemonBackupRepository.php @@ -2,6 +2,7 @@ namespace App\Repositories\Daemon; +use Illuminate\Http\Client\Response; use Webmozart\Assert\Assert; use App\Models\Backup; use App\Models\Server; @@ -27,7 +28,7 @@ public function setBackupAdapter(string $adapter): self * * @throws \App\Exceptions\Http\Connection\DaemonConnectionException */ - public function backup(Backup $backup) + public function backup(Backup $backup): Response { Assert::isInstanceOf($this->server, Server::class); @@ -50,7 +51,7 @@ public function backup(Backup $backup) * * @throws \App\Exceptions\Http\Connection\DaemonConnectionException */ - public function restore(Backup $backup, string $url = null, bool $truncate = false) + public function restore(Backup $backup, ?string $url = null, bool $truncate = false): Response { Assert::isInstanceOf($this->server, Server::class); @@ -73,7 +74,7 @@ public function restore(Backup $backup, string $url = null, bool $truncate = fal * * @throws \App\Exceptions\Http\Connection\DaemonConnectionException */ - public function delete(Backup $backup) + public function delete(Backup $backup): Response { Assert::isInstanceOf($this->server, Server::class); diff --git a/app/Repositories/Daemon/DaemonConfigurationRepository.php b/app/Repositories/Daemon/DaemonConfigurationRepository.php index 52cda56dd4..910e95e58b 100644 --- a/app/Repositories/Daemon/DaemonConfigurationRepository.php +++ b/app/Repositories/Daemon/DaemonConfigurationRepository.php @@ -5,6 +5,7 @@ use App\Models\Node; use GuzzleHttp\Exception\TransferException; use App\Exceptions\Http\Connection\DaemonConnectionException; +use Illuminate\Http\Client\Response; class DaemonConfigurationRepository extends DaemonRepository { @@ -13,7 +14,7 @@ class DaemonConfigurationRepository extends DaemonRepository * * @throws \App\Exceptions\Http\Connection\DaemonConnectionException */ - public function getSystemInformation(?int $version = null, $connectTimeout = 5): array + public function getSystemInformation(?int $version = null, int $connectTimeout = 5): array { try { $response = $this @@ -34,7 +35,7 @@ public function getSystemInformation(?int $version = null, $connectTimeout = 5): * * @throws \App\Exceptions\Http\Connection\DaemonConnectionException */ - public function update(Node $node) + public function update(Node $node): Response { try { return $this->getHttpClient()->post( diff --git a/app/Repositories/Daemon/DaemonFileRepository.php b/app/Repositories/Daemon/DaemonFileRepository.php index 7667f482e8..a65013fb30 100644 --- a/app/Repositories/Daemon/DaemonFileRepository.php +++ b/app/Repositories/Daemon/DaemonFileRepository.php @@ -3,6 +3,7 @@ namespace App\Repositories\Daemon; use Carbon\CarbonInterval; +use Illuminate\Http\Client\Response; use Webmozart\Assert\Assert; use App\Models\Server; use GuzzleHttp\Exception\ClientException; @@ -15,13 +16,13 @@ class DaemonFileRepository extends DaemonRepository /** * Return the contents of a given file. * - * @param int|null $notLargerThan the maximum content length in bytes + * @param int|null $notLargerThan the maximum content length in bytes * * @throws \GuzzleHttp\Exception\TransferException * @throws \App\Exceptions\Http\Server\FileSizeTooLargeException * @throws \App\Exceptions\Http\Connection\DaemonConnectionException */ - public function getContent(string $path, int $notLargerThan = null): string + public function getContent(string $path, ?int $notLargerThan = null): string { Assert::isInstanceOf($this->server, Server::class); @@ -48,7 +49,7 @@ public function getContent(string $path, int $notLargerThan = null): string * * @throws \App\Exceptions\Http\Connection\DaemonConnectionException */ - public function putContent(string $path, string $content) + public function putContent(string $path, string $content): Response { Assert::isInstanceOf($this->server, Server::class); @@ -88,7 +89,7 @@ public function getDirectory(string $path): array * * @throws \App\Exceptions\Http\Connection\DaemonConnectionException */ - public function createDirectory(string $name, string $path) + public function createDirectory(string $name, string $path): Response { Assert::isInstanceOf($this->server, Server::class); @@ -110,7 +111,7 @@ public function createDirectory(string $name, string $path) * * @throws \App\Exceptions\Http\Connection\DaemonConnectionException */ - public function renameFiles(?string $root, array $files) + public function renameFiles(?string $root, array $files): Response { Assert::isInstanceOf($this->server, Server::class); @@ -132,7 +133,7 @@ public function renameFiles(?string $root, array $files) * * @throws \App\Exceptions\Http\Connection\DaemonConnectionException */ - public function copyFile(string $location) + public function copyFile(string $location): Response { Assert::isInstanceOf($this->server, Server::class); @@ -153,7 +154,7 @@ public function copyFile(string $location) * * @throws \App\Exceptions\Http\Connection\DaemonConnectionException */ - public function deleteFiles(?string $root, array $files) + public function deleteFiles(?string $root, array $files): Response { Assert::isInstanceOf($this->server, Server::class); @@ -203,7 +204,7 @@ public function compressFiles(?string $root, array $files): array * * @throws \App\Exceptions\Http\Connection\DaemonConnectionException */ - public function decompressFile(?string $root, string $file) + public function decompressFile(?string $root, string $file): Response { Assert::isInstanceOf($this->server, Server::class); @@ -229,7 +230,7 @@ public function decompressFile(?string $root, string $file) * * @throws \App\Exceptions\Http\Connection\DaemonConnectionException */ - public function chmodFiles(?string $root, array $files) + public function chmodFiles(?string $root, array $files): Response { Assert::isInstanceOf($this->server, Server::class); @@ -251,7 +252,7 @@ public function chmodFiles(?string $root, array $files) * * @throws \App\Exceptions\Http\Connection\DaemonConnectionException */ - public function pull(string $url, ?string $directory, array $params = []) + public function pull(string $url, ?string $directory, array $params = []): Response { Assert::isInstanceOf($this->server, Server::class); diff --git a/app/Repositories/Daemon/DaemonPowerRepository.php b/app/Repositories/Daemon/DaemonPowerRepository.php index ca3e03c442..3b701be342 100644 --- a/app/Repositories/Daemon/DaemonPowerRepository.php +++ b/app/Repositories/Daemon/DaemonPowerRepository.php @@ -2,6 +2,7 @@ namespace App\Repositories\Daemon; +use Illuminate\Http\Client\Response; use Webmozart\Assert\Assert; use App\Models\Server; use GuzzleHttp\Exception\TransferException; @@ -14,7 +15,7 @@ class DaemonPowerRepository extends DaemonRepository * * @throws \App\Exceptions\Http\Connection\DaemonConnectionException */ - public function send(string $action) + public function send(string $action): Response { Assert::isInstanceOf($this->server, Server::class); diff --git a/app/Rules/Username.php b/app/Rules/Username.php index 591df389c2..f888da1037 100644 --- a/app/Rules/Username.php +++ b/app/Rules/Username.php @@ -17,8 +17,8 @@ class Username implements Rule * * Allowed characters: a-z0-9_-. * - * @param string $attribute - * @param mixed $value + * @param string $attribute + * @param mixed $value */ public function passes($attribute, $value): bool { diff --git a/app/Services/Acl/Api/AdminAcl.php b/app/Services/Acl/Api/AdminAcl.php index dd58e317c6..90ec6480ae 100644 --- a/app/Services/Acl/Api/AdminAcl.php +++ b/app/Services/Acl/Api/AdminAcl.php @@ -17,7 +17,9 @@ class AdminAcl * implements a read/write/none permissions scheme for all endpoints. */ public const NONE = 0; + public const READ = 1; + public const WRITE = 2; /** @@ -25,13 +27,21 @@ class AdminAcl * set for each key. These are stored in the database as r_{resource}. */ public const RESOURCE_SERVERS = 'servers'; + public const RESOURCE_NODES = 'nodes'; + public const RESOURCE_ALLOCATIONS = 'allocations'; + public const RESOURCE_USERS = 'users'; + public const RESOURCE_EGGS = 'eggs'; + public const RESOURCE_DATABASE_HOSTS = 'database_hosts'; + public const RESOURCE_SERVER_DATABASES = 'server_databases'; + public const RESOURCE_MOUNTS = 'mounts'; + public const RESOURCE_ROLES = 'roles'; /** diff --git a/app/Services/Activity/ActivityLogBatchService.php b/app/Services/Activity/ActivityLogBatchService.php index cb2e91b5c4..df237286fb 100644 --- a/app/Services/Activity/ActivityLogBatchService.php +++ b/app/Services/Activity/ActivityLogBatchService.php @@ -7,6 +7,7 @@ class ActivityLogBatchService { protected int $transaction = 0; + protected ?string $uuid = null; /** diff --git a/app/Services/Activity/ActivityLogService.php b/app/Services/Activity/ActivityLogService.php index 37b7ae43b5..7de267a3e7 100644 --- a/app/Services/Activity/ActivityLogService.php +++ b/app/Services/Activity/ActivityLogService.php @@ -64,7 +64,7 @@ public function description(?string $description): self * * @template T extends \Illuminate\Database\Eloquent\Model|\Illuminate\Contracts\Auth\Authenticatable * - * @param T|T[]|null $subjects + * @param T|T[]|null $subjects */ public function subject(...$subjects): self { @@ -100,8 +100,8 @@ public function actor(Model $actor): self /** * Sets a custom property on the activity log instance. * - * @param string|array $key - * @param mixed $value + * @param string|array $key + * @param mixed $value */ public function property($key, $value = null): self { @@ -130,7 +130,7 @@ public function withRequestMetadata(): self * performing this action it will be logged to the disk but will not interrupt * the code flow. */ - public function log(string $description = null): ActivityLog + public function log(?string $description = null): ActivityLog { $activity = $this->getActivity(); @@ -168,7 +168,7 @@ public function clone(): self * * @throws \Throwable */ - public function transaction(\Closure $callback) + public function transaction(\Closure $callback): mixed { return $this->connection->transaction(function () use ($callback) { $response = $callback($this); diff --git a/app/Services/Allocations/AssignmentService.php b/app/Services/Allocations/AssignmentService.php index 33ed4aa748..23f28e1224 100644 --- a/app/Services/Allocations/AssignmentService.php +++ b/app/Services/Allocations/AssignmentService.php @@ -16,10 +16,15 @@ class AssignmentService { public const CIDR_MAX_BITS = 25; + public const CIDR_MIN_BITS = 32; + public const PORT_FLOOR = 1024; + public const PORT_CEIL = 65535; + public const PORT_RANGE_LIMIT = 1000; + public const PORT_RANGE_REGEX = '/^(\d{4,5})-(\d{4,5})$/'; /** @@ -38,7 +43,7 @@ public function __construct(protected ConnectionInterface $connection) * @throws \App\Exceptions\Service\Allocation\PortOutOfRangeException * @throws \App\Exceptions\Service\Allocation\TooManyPortsInRangeException */ - public function handle(Node $node, array $data, Server $server = null): array + public function handle(Node $node, array $data, ?Server $server = null): array { $explode = explode('/', $data['allocation_ip']); if (count($explode) !== 1) { diff --git a/app/Services/Backups/InitiateBackupService.php b/app/Services/Backups/InitiateBackupService.php index 447c575359..fa15db88ac 100644 --- a/app/Services/Backups/InitiateBackupService.php +++ b/app/Services/Backups/InitiateBackupService.php @@ -43,7 +43,7 @@ public function setIsLocked(bool $isLocked): self /** * Sets the files to be ignored by this backup. * - * @param string[]|null $ignored + * @param string[]|null $ignored */ public function setIgnoredFiles(?array $ignored): self { @@ -70,7 +70,7 @@ public function setIgnoredFiles(?array $ignored): self * @throws \App\Exceptions\Service\Backup\TooManyBackupsException * @throws \Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException */ - public function handle(Server $server, string $name = null, bool $override = false): Backup + public function handle(Server $server, ?string $name = null, bool $override = false): Backup { $limit = config('backups.throttles.limit'); $period = config('backups.throttles.period'); diff --git a/app/Services/Deployment/FindViableNodesService.php b/app/Services/Deployment/FindViableNodesService.php index 4cffc71d28..d95c08c640 100644 --- a/app/Services/Deployment/FindViableNodesService.php +++ b/app/Services/Deployment/FindViableNodesService.php @@ -17,7 +17,7 @@ class FindViableNodesService * are tossed out, as are any nodes marked as non-public, meaning automatic * deployments should not be done against them. */ - public function handle(int $memory = 0, int $disk = 0, int $cpu = 0, $tags = []): Collection + public function handle(int $memory = 0, int $disk = 0, int $cpu = 0, array $tags = []): Collection { $nodes = Node::query() ->withSum('servers', 'memory') diff --git a/app/Services/Eggs/Sharing/EggImporterService.php b/app/Services/Eggs/Sharing/EggImporterService.php index dc54525a6e..e539a7f282 100644 --- a/app/Services/Eggs/Sharing/EggImporterService.php +++ b/app/Services/Eggs/Sharing/EggImporterService.php @@ -34,7 +34,7 @@ public function __construct(protected ConnectionInterface $connection) * * @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable */ - public function fromFile(UploadedFile $file, Egg $egg = null): Egg + public function fromFile(UploadedFile $file, ?Egg $egg = null): Egg { $parsed = $this->parseFile($file); @@ -75,7 +75,7 @@ public function fromFile(UploadedFile $file, Egg $egg = null): Egg * * @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable */ - public function fromUrl(string $url, Egg $egg = null): Egg + public function fromUrl(string $url, ?Egg $egg = null): Egg { $info = pathinfo($url); $tmpDir = TemporaryDirectory::make()->deleteWhenDestroyed(); diff --git a/app/Services/Files/DeleteFilesService.php b/app/Services/Files/DeleteFilesService.php index 2b1be5fda1..1692923edd 100644 --- a/app/Services/Files/DeleteFilesService.php +++ b/app/Services/Files/DeleteFilesService.php @@ -19,6 +19,7 @@ public function __construct( /** * Deletes the given files. + * * @throws DaemonConnectionException */ public function handle(Server $server, array $files): void diff --git a/app/Services/Servers/ServerConfigurationStructureService.php b/app/Services/Servers/ServerConfigurationStructureService.php index 43b8c97364..7ac714ed51 100644 --- a/app/Services/Servers/ServerConfigurationStructureService.php +++ b/app/Services/Servers/ServerConfigurationStructureService.php @@ -93,5 +93,4 @@ protected function returnFormat(Server $server): array return $response; } - } diff --git a/app/Services/Servers/ServerCreationService.php b/app/Services/Servers/ServerCreationService.php index 7fd3e8b67a..18eb790e53 100644 --- a/app/Services/Servers/ServerCreationService.php +++ b/app/Services/Servers/ServerCreationService.php @@ -45,7 +45,7 @@ public function __construct( * @throws \Illuminate\Validation\ValidationException * @throws \App\Exceptions\Service\Deployment\NoViableAllocationException */ - public function handle(array $data, DeploymentObject $deployment = null): Server + public function handle(array $data, ?DeploymentObject $deployment = null): Server { if (!isset($data['oom_killer']) && isset($data['oom_disabled'])) { $data['oom_killer'] = !$data['oom_disabled']; diff --git a/app/Services/Servers/SuspensionService.php b/app/Services/Servers/SuspensionService.php index aef02b7b2e..0209db3fc5 100644 --- a/app/Services/Servers/SuspensionService.php +++ b/app/Services/Servers/SuspensionService.php @@ -12,6 +12,7 @@ class SuspensionService { public const ACTION_SUSPEND = 'suspend'; + public const ACTION_UNSUSPEND = 'unsuspend'; /** @@ -27,7 +28,7 @@ public function __construct( * * @throws \Throwable */ - public function toggle(Server $server, string $action = self::ACTION_SUSPEND) + public function toggle(Server $server, string $action = self::ACTION_SUSPEND): void { Assert::oneOf($action, [self::ACTION_SUSPEND, self::ACTION_UNSUSPEND]); @@ -36,7 +37,9 @@ public function toggle(Server $server, string $action = self::ACTION_SUSPEND) // suspended in the database. Additionally, nothing needs to happen if the server // is not suspended, and we try to un-suspend the instance. if ($isSuspending === $server->isSuspended()) { - return Notification::make()->danger()->title('Failed!')->body('Server is already suspended!')->send(); + Notification::make()->danger()->title('Failed!')->body('Server is already suspended!')->send(); + + return; } // Check if the server is currently being transferred. diff --git a/app/Services/Servers/TransferServerService.php b/app/Services/Servers/TransferServerService.php index 4312369dd7..8a7bcd3ea1 100644 --- a/app/Services/Servers/TransferServerService.php +++ b/app/Services/Servers/TransferServerService.php @@ -106,7 +106,7 @@ public function handle(Server $server, array $data): bool /** * Assigns the specified allocations to the specified server. */ - private function assignAllocationsToServer(Server $server, int $node_id, int $allocation_id, array $additional_allocations) + private function assignAllocationsToServer(Server $server, int $node_id, int $allocation_id, array $additional_allocations): void { $allocations = $additional_allocations; $allocations[] = $allocation_id; diff --git a/app/Services/Users/ToggleTwoFactorService.php b/app/Services/Users/ToggleTwoFactorService.php index 64518e39bf..ca5df2dcbd 100644 --- a/app/Services/Users/ToggleTwoFactorService.php +++ b/app/Services/Users/ToggleTwoFactorService.php @@ -30,7 +30,7 @@ public function __construct( * @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException * @throws \App\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid */ - public function handle(User $user, string $token, bool $toggleState = null): array + public function handle(User $user, string $token, ?bool $toggleState = null): array { $isValidToken = $this->google2FA->verifyKey($user->totp_secret, $token, config()->get('panel.auth.2fa.window')); diff --git a/app/Traits/CheckMigrationsTrait.php b/app/Traits/CheckMigrationsTrait.php index ee2de8f3a8..1f849e8aa9 100644 --- a/app/Traits/CheckMigrationsTrait.php +++ b/app/Traits/CheckMigrationsTrait.php @@ -12,7 +12,7 @@ trait CheckMigrationsTrait protected function hasCompletedMigrations(): bool { /** @var Migrator $migrator */ - $migrator = app()->make('migrator'); + $migrator = app()->make('migrator'); // @phpstan-ignore-line $files = $migrator->getMigrationFiles(database_path('migrations')); diff --git a/app/Traits/Controllers/PlainJavascriptInjection.php b/app/Traits/Controllers/PlainJavascriptInjection.php index 4b5169296b..e77686776c 100644 --- a/app/Traits/Controllers/PlainJavascriptInjection.php +++ b/app/Traits/Controllers/PlainJavascriptInjection.php @@ -9,7 +9,7 @@ trait PlainJavascriptInjection /** * Injects statistics into javascript. */ - public function injectJavascript($data) + public function injectJavascript($data): void { \JavaScript::put($data); } diff --git a/app/Traits/EnvironmentWriterTrait.php b/app/Traits/EnvironmentWriterTrait.php index 460d8000e1..cb5bef0476 100644 --- a/app/Traits/EnvironmentWriterTrait.php +++ b/app/Traits/EnvironmentWriterTrait.php @@ -22,6 +22,7 @@ public function escapeEnvironmentValue(string $value): string /** * Update the .env file for the application using the passed in values. + * * @throws Exception */ public function writeToEnvironment(array $values = []): void diff --git a/app/Traits/Helpers/AvailableLanguages.php b/app/Traits/Helpers/AvailableLanguages.php index 53b45c5a60..aafffbbab1 100644 --- a/app/Traits/Helpers/AvailableLanguages.php +++ b/app/Traits/Helpers/AvailableLanguages.php @@ -51,6 +51,7 @@ public function isLanguageTranslated(string $countryCode = 'en'): bool */ private function getFilesystemInstance(): Filesystem { + // @phpstan-ignore-next-line return $this->filesystem = $this->filesystem ?: app()->make(Filesystem::class); } } diff --git a/app/Transformers/Api/Application/BaseTransformer.php b/app/Transformers/Api/Application/BaseTransformer.php index 62ee4ceeb6..ec4bb9d3cd 100644 --- a/app/Transformers/Api/Application/BaseTransformer.php +++ b/app/Transformers/Api/Application/BaseTransformer.php @@ -54,6 +54,7 @@ public function setRequest(Request $request): self */ public static function fromRequest(Request $request): self { + // @phpstan-ignore-next-line return app(static::class)->setRequest($request); } @@ -89,8 +90,7 @@ protected function authorize(string $resource): bool * * @template T of \App\Transformers\Api\Application\BaseTransformer * - * @param class-string $abstract - * + * @param class-string $abstract * @return T * * @throws \App\Exceptions\Transformer\InvalidTransformerLevelException diff --git a/app/Transformers/Api/Application/EggVariableTransformer.php b/app/Transformers/Api/Application/EggVariableTransformer.php index 381b3102e1..d70299787b 100644 --- a/app/Transformers/Api/Application/EggVariableTransformer.php +++ b/app/Transformers/Api/Application/EggVariableTransformer.php @@ -15,7 +15,7 @@ public function getResourceName(): string return Egg::RESOURCE_NAME; } - public function transform(EggVariable $model) + public function transform(EggVariable $model): array { return $model->toArray(); } diff --git a/app/Transformers/Api/Application/MountTransformer.php b/app/Transformers/Api/Application/MountTransformer.php index c658f30fa2..18dbe36dcd 100644 --- a/app/Transformers/Api/Application/MountTransformer.php +++ b/app/Transformers/Api/Application/MountTransformer.php @@ -22,7 +22,7 @@ public function getResourceName(): string return Mount::RESOURCE_NAME; } - public function transform(Mount $model) + public function transform(Mount $model): array { return $model->toArray(); } diff --git a/app/Transformers/Api/Application/ServerTransformer.php b/app/Transformers/Api/Application/ServerTransformer.php index 90836cc248..b81baf2794 100644 --- a/app/Transformers/Api/Application/ServerTransformer.php +++ b/app/Transformers/Api/Application/ServerTransformer.php @@ -30,7 +30,7 @@ class ServerTransformer extends BaseTransformer /** * Perform dependency injection. */ - public function handle(EnvironmentService $environmentService) + public function handle(EnvironmentService $environmentService): void { $this->environmentService = $environmentService; } diff --git a/app/Transformers/Api/Client/ActivityLogTransformer.php b/app/Transformers/Api/Client/ActivityLogTransformer.php index 58d5f13ae5..a54cb1f304 100644 --- a/app/Transformers/Api/Client/ActivityLogTransformer.php +++ b/app/Transformers/Api/Client/ActivityLogTransformer.php @@ -6,6 +6,7 @@ use App\Models\User; use App\Models\ActivityLog; use Illuminate\Database\Eloquent\Model; +use League\Fractal\Resource\ResourceAbstract; class ActivityLogTransformer extends BaseClientTransformer { @@ -34,7 +35,7 @@ public function transform(ActivityLog $model): array ]; } - public function includeActor(ActivityLog $model) + public function includeActor(ActivityLog $model): ResourceAbstract { if (!$model->actor instanceof User) { return $this->null(); @@ -111,7 +112,7 @@ protected function hasAdditionalMetadata(ActivityLog $model): bool * Determines if the user can view the IP address in the output either because they are the * actor that performed the action, or because they are an administrator on the Panel. */ - protected function canViewIP(Model $actor = null): bool + protected function canViewIP(?Model $actor = null): bool { return $actor?->is($this->request->user()) || $this->request->user()->isRootAdmin(); } diff --git a/app/Transformers/Api/Client/BaseClientTransformer.php b/app/Transformers/Api/Client/BaseClientTransformer.php index c490d1bfdc..9ee56cfaf1 100644 --- a/app/Transformers/Api/Client/BaseClientTransformer.php +++ b/app/Transformers/Api/Client/BaseClientTransformer.php @@ -24,7 +24,7 @@ public function getUser(): User * * @noinspection PhpParameterNameChangedDuringInheritanceInspection */ - protected function authorize(string $ability, Server $server = null): bool + protected function authorize(string $ability, ?Server $server = null): bool { Assert::isInstanceOf($server, Server::class); diff --git a/compose.yml b/compose.yml index 23d2f2a397..6dab7145e3 100644 --- a/compose.yml +++ b/compose.yml @@ -5,9 +5,7 @@ x-common: ADMIN_EMAIL: "USEYOUROWNEMAILHERE@example.com" APP_DEBUG: "false" - APP_ENVIRONMENT_ONLY: "false" APP_ENV: "production" - SESSION_DRIVER: "file" mail: &mail-environment diff --git a/config/logging.php b/config/logging.php new file mode 100644 index 0000000000..492c0a890c --- /dev/null +++ b/config/logging.php @@ -0,0 +1,7 @@ + env('LOG_CHANNEL', 'daily'), + +]; diff --git a/config/mail.php b/config/mail.php index c6a0657b31..22b1b4e763 100644 --- a/config/mail.php +++ b/config/mail.php @@ -2,6 +2,13 @@ return [ + 'default' => env('MAIL_MAILER', 'log'), + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'no-reply@example.com'), + 'name' => env('MAIL_FROM_NAME', 'Pelican Admin'), + ], + 'mailers' => [ 'mailgun' => [ 'transport' => 'mailgun', diff --git a/config/session.php b/config/session.php new file mode 100644 index 0000000000..cf4a46ff83 --- /dev/null +++ b/config/session.php @@ -0,0 +1,9 @@ + env('SESSION_DRIVER', 'file'), + + 'cookie' => env('SESSION_COOKIE', 'pelican_session'), + +]; diff --git a/config/trustedproxy.php b/config/trustedproxy.php new file mode 100644 index 0000000000..7e0166af8e --- /dev/null +++ b/config/trustedproxy.php @@ -0,0 +1,28 @@ +getClientIp() + * always gets the originating client IP, no matter + * how many proxies that client's request has + * subsequently passed through. + */ + 'proxies' => in_array(env('TRUSTED_PROXIES', []), ['*', '**']) ? + env('TRUSTED_PROXIES') : explode(',', env('TRUSTED_PROXIES') ?? ''), +]; diff --git a/database/Seeders/DatabaseSeeder.php b/database/Seeders/DatabaseSeeder.php index 2f7f6694e1..b2e7c20e41 100644 --- a/database/Seeders/DatabaseSeeder.php +++ b/database/Seeders/DatabaseSeeder.php @@ -10,7 +10,7 @@ class DatabaseSeeder extends Seeder /** * Run the database seeds. */ - public function run() + public function run(): void { $this->call(EggSeeder::class); diff --git a/database/Seeders/EggSeeder.php b/database/Seeders/EggSeeder.php index dcbc4f25c1..d6a3743b7b 100644 --- a/database/Seeders/EggSeeder.php +++ b/database/Seeders/EggSeeder.php @@ -34,7 +34,7 @@ public function __construct( /** * Run the egg seeder. */ - public function run() + public function run(): void { foreach (static::$imports as $import) { /* @noinspection PhpParamsInspection */ @@ -45,7 +45,7 @@ public function run() /** * Loop through the list of egg files and import them. */ - protected function parseEggFiles($name) + protected function parseEggFiles($name): void { $files = new \DirectoryIterator(database_path('Seeders/eggs/' . kebab_case($name))); diff --git a/phpstan.neon b/phpstan.neon index 58b6c8e913..e5c462230c 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,20 +1,20 @@ includes: - vendor/larastan/larastan/extension.neon +rules: + - App\PHPStan\ForbiddenGlobalFunctionsRule + parameters: paths: - - app/ + - app - # Level 9 is the highest level - level: 5 + level: 6 ignoreErrors: # Prologue\Alerts defines its methods from its configuration file dynamically - '#^Call to an undefined method Prologue\\Alerts\\AlertsMessageBag::(danger|success|info|warning)\(\)\.$#' -# excludePaths: -# - ./*/*/FileToBeExcluded.php -# -# checkMissingIterableValueType: false - + - '#no value type specified in iterable#' + - '#Unable to resolve the template type#' + - '#does not specify its types#' diff --git a/pint.json b/pint.json index 4d7ccf1135..99fa80d3a1 100644 --- a/pint.json +++ b/pint.json @@ -1,12 +1,8 @@ { "preset": "laravel", "rules": { - "class_attributes_separation": false, "concat_space": false, "not_operator_with_successor_space": false, - "nullable_type_declaration_for_default_null_value": false, - "ordered_imports": false, - "phpdoc_align": false, - "phpdoc_separation": false + "ordered_imports": false } } diff --git a/tests/Assertions/AssertsActivityLogged.php b/tests/Assertions/AssertsActivityLogged.php index db352bc975..23a3461b1f 100644 --- a/tests/Assertions/AssertsActivityLogged.php +++ b/tests/Assertions/AssertsActivityLogged.php @@ -11,7 +11,7 @@ trait AssertsActivityLogged { /** - * @param \Illuminate\Database\Eloquent\Model|array $subjects + * @param \Illuminate\Database\Eloquent\Model|array $subjects */ public function assertActivityFor(string $event, ?Model $actor, ...$subjects): void { diff --git a/tests/Integration/Api/Client/ClientApiIntegrationTestCase.php b/tests/Integration/Api/Client/ClientApiIntegrationTestCase.php index e909bb41ec..d6dee00da0 100644 --- a/tests/Integration/Api/Client/ClientApiIntegrationTestCase.php +++ b/tests/Integration/Api/Client/ClientApiIntegrationTestCase.php @@ -47,7 +47,7 @@ protected function createTestResponse($response, $request): \Illuminate\Testing\ /** * Returns a link to the specific resource using the client API. */ - protected function link(mixed $model, string $append = null): string + protected function link(mixed $model, ?string $append = null): string { switch (get_class($model)) { case Server::class: @@ -76,7 +76,7 @@ protected function link(mixed $model, string $append = null): string * Asserts that the data passed through matches the output of the data from the transformer. This * will remove the "relationships" key when performing the comparison. */ - protected function assertJsonTransformedWith(array $data, Model|EloquentModel $model) + protected function assertJsonTransformedWith(array $data, Model|EloquentModel $model): void { $reflect = new \ReflectionClass($model); $transformer = sprintf('\\App\\Transformers\\Api\\Client\\%sTransformer', $reflect->getShortName()); diff --git a/tests/Integration/Api/Client/Server/PowerControllerTest.php b/tests/Integration/Api/Client/Server/PowerControllerTest.php index 5cadb37729..94d3e7326c 100644 --- a/tests/Integration/Api/Client/Server/PowerControllerTest.php +++ b/tests/Integration/Api/Client/Server/PowerControllerTest.php @@ -14,7 +14,7 @@ class PowerControllerTest extends ClientApiIntegrationTestCase * an error in response. This checks against the specific permission needed to send * the command to the server. * - * @param string[] $permissions + * @param string[] $permissions * * @dataProvider invalidPermissionDataProvider */ diff --git a/tests/Integration/Api/Remote/SftpAuthenticationControllerTest.php b/tests/Integration/Api/Remote/SftpAuthenticationControllerTest.php index 031eedce70..3236266dcd 100644 --- a/tests/Integration/Api/Remote/SftpAuthenticationControllerTest.php +++ b/tests/Integration/Api/Remote/SftpAuthenticationControllerTest.php @@ -226,7 +226,7 @@ protected function getUsername(bool $long = false): string /** * Sets the authorization header for the rest of the test. */ - protected function setAuthorization(Node $node = null): void + protected function setAuthorization(?Node $node = null): void { $node = $node ?? $this->server->node; diff --git a/tests/Traits/Http/RequestMockHelpers.php b/tests/Traits/Http/RequestMockHelpers.php index 22f60e74f6..46a71e26a7 100644 --- a/tests/Traits/Http/RequestMockHelpers.php +++ b/tests/Traits/Http/RequestMockHelpers.php @@ -27,7 +27,7 @@ public function setRequestMockClass(string $class): void /** * Configure the user model that the request mock should return with. */ - public function setRequestUserModel(User $user = null): void + public function setRequestUserModel(?User $user = null): void { $this->request->shouldReceive('user')->andReturn($user); } @@ -80,7 +80,7 @@ protected function buildRequestMock(): void * * @deprecated */ - protected function setRequestUser(User $user = null): User + protected function setRequestUser(?User $user = null): User { $user = $user instanceof User ? $user : User::factory()->make(); $this->request->shouldReceive('user')->withNoArgs()->andReturn($user); diff --git a/tests/Traits/Integration/CreatesTestModels.php b/tests/Traits/Integration/CreatesTestModels.php index 12d6459347..0e28cb40e7 100644 --- a/tests/Traits/Integration/CreatesTestModels.php +++ b/tests/Traits/Integration/CreatesTestModels.php @@ -65,8 +65,7 @@ public function createServerModel(array $attributes = []): Server * Generates a user and a server for that user. If an array of permissions is passed it * is assumed that the user is actually a subuser of the server. * - * @param string[] $permissions - * + * @param string[] $permissions * @return array{\App\Models\User, \App\Models\Server} */ public function generateTestAccount(array $permissions = []): array