Compare commits

...

20 Commits

Author SHA1 Message Date
Boy132
670a9c1a97 replace hardcoded hex strings 2026-05-04 11:15:52 +02:00
Boy132
2bbfb0eef9 Fix "undefined key" error for TagsFilter (#2312) 2026-05-04 10:48:34 +02:00
Boy132
2a64ea8536 Cleanup node fqdn/ip validation (#2307) 2026-05-04 09:01:03 +02:00
Boy132
4fdbbff74b Correctly store falsy startup variable values (#2305) 2026-04-28 09:35:19 +02:00
Boy132
2ed891633c Add subuser permission for changing server icon (#2304) 2026-04-28 09:34:38 +02:00
Michael (Parker) Parker
58dcbeac0b install plugin packages on start (#2298) 2026-04-23 08:41:44 -05:00
Boy132
91c5ddb2bd Catch icon exception when importing egg (#2299) 2026-04-22 13:52:31 +02:00
Lance Pioch
562be98b20 Add user_mountable functionality for Mounts (#2077) (#2181)
Co-authored-by: Boy132 <mail@boy132.de>
2026-04-21 22:03:10 -04:00
Lance Pioch
fd3b8a7ab3 Laravel 12.56.0 Shift (#2283)
Co-authored-by: Shift <shift@laravelshift.com>
2026-04-21 22:02:48 -04:00
Boy132
1817383bf5 Add changes from upstream (#2293)
Co-authored-by: DaneEveritt <dane@daneeveritt.com>
Co-authored-by: danny6167 <danielb@purpleflaghosting.com>
Co-authored-by: MrSoulPenguin <28676680+MrSoulPenguin@users.noreply.github.com>
2026-04-20 17:25:54 +02:00
Boy132
d39a0c4464 Allow to add translation prefix to custom subuser permissions (#2265) 2026-04-20 17:24:24 +02:00
Boy132
2e48095379 Refactor egg & server icon uploading (#2281) 2026-04-20 17:23:41 +02:00
Jørgen Vatle
06c662988a Feature: Add support for user-configurable egg/nests index file. (#2287)
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2026-04-20 05:06:39 -04:00
Neil Hanlon
98d7158dfc Fix PATCH /api/application/nodes returning 500 due to null timestamps (#2286) 2026-04-20 05:06:01 -04:00
JUBE
e01d9f2cf3 Add sortable feature to name column in ListServers (#2282) 2026-04-08 19:50:01 -04:00
Boy132
b693d0e728 Do not allow all file extensions for egg image in egg importer (#2279) 2026-03-22 19:18:46 +01:00
Boy132
612041e1f8 Run composer update and update model phpdocs (#2275) 2026-03-17 09:09:01 +01:00
Boy132
64bcdb514b Add jobs for plugin install, update and uninstall (#2267) 2026-03-17 09:08:28 +01:00
Lance Pioch
c1105db702 Fix encodings with new tested helper (#2238) 2026-03-16 16:48:13 -04:00
Lance Pioch
761492d09f Laravel 12.53.0 Shift (#2266)
Co-authored-by: Shift <shift@laravelshift.com>
2026-02-26 23:56:15 -05:00
172 changed files with 3350 additions and 2358 deletions

View File

@@ -13,7 +13,7 @@ class UpdateEggIndexCommand extends Command
public function handle(): int public function handle(): int
{ {
try { try {
$data = Http::timeout(5)->connectTimeout(1)->get('https://raw.githubusercontent.com/pelican-eggs/pelican-eggs.github.io/refs/heads/main/content/pelican.json')->throw()->json(); $data = Http::timeout(5)->connectTimeout(1)->get(config('panel.cdn.egg_index_url'))->throw()->json();
} catch (Exception $exception) { } catch (Exception $exception) {
$this->error($exception->getMessage()); $this->error($exception->getMessage());

View File

@@ -41,17 +41,8 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
}; };
} }
public function getColor(bool $hex = false): string public function getColor(): string
{ {
if ($hex) {
return match ($this) {
self::Created, self::Restarting => '#2563EB',
self::Starting, self::Paused, self::Removing, self::Stopping => '#D97706',
self::Running => '#22C55E',
self::Exited, self::Missing, self::Dead, self::Offline => '#EF4444',
};
}
return match ($this) { return match ($this) {
self::Created => 'primary', self::Created => 'primary',
self::Starting => 'warning', self::Starting => 'warning',

View File

@@ -25,16 +25,8 @@ enum ServerState: string implements HasColor, HasIcon, HasLabel
}; };
} }
public function getColor(bool $hex = false): string public function getColor(): string
{ {
if ($hex) {
return match ($this) {
self::Installing, self::RestoringBackup => '#2563EB',
self::Suspended => '#D97706',
self::InstallFailed, self::ReinstallFailed => '#EF4444',
};
}
return match ($this) { return match ($this) {
self::Installing => 'primary', self::Installing => 'primary',
self::InstallFailed => 'danger', self::InstallFailed => 'danger',

View File

@@ -50,6 +50,9 @@ enum SubuserPermission: string
case ActivityRead = 'activity.read'; case ActivityRead = 'activity.read';
case MountRead = 'mount.read';
case MountUpdate = 'mount.update';
case StartupRead = 'startup.read'; case StartupRead = 'startup.read';
case StartupUpdate = 'startup.update'; case StartupUpdate = 'startup.update';
case StartupDockerImage = 'startup.docker-image'; case StartupDockerImage = 'startup.docker-image';
@@ -57,6 +60,7 @@ enum SubuserPermission: string
case SettingsRename = 'settings.rename'; case SettingsRename = 'settings.rename';
case SettingsDescription = 'settings.description'; case SettingsDescription = 'settings.description';
case SettingsReinstall = 'settings.reinstall'; case SettingsReinstall = 'settings.reinstall';
case SettingsChangeIcon = 'settings.change-icon';
/** @return string[] */ /** @return string[] */
public function split(): array public function split(): array
@@ -84,6 +88,7 @@ enum SubuserPermission: string
'schedule' => TablerIcon::Clock, 'schedule' => TablerIcon::Clock,
'settings' => TablerIcon::Settings, 'settings' => TablerIcon::Settings,
'activity' => TablerIcon::Stack, 'activity' => TablerIcon::Stack,
'mount' => TablerIcon::LayersLinked,
default => null, default => null,
}; };
} }

View File

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

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Events\User;
use App\Models\User;
use Illuminate\Foundation\Events\Dispatchable;
final class PasswordChanged
{
use Dispatchable;
public function __construct(public readonly User $user) {}
}

View File

@@ -14,13 +14,14 @@ use Boquizo\FilamentLogViewer\Utils\Level;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
class ListLogs extends BaseListLogs class ListLogs extends BaseListLogs
{ {
protected string $view = 'filament.components.list-logs'; protected string $view = 'filament.components.list-logs';
public function getHeading(): string|null|\Illuminate\Contracts\Support\Htmlable public function getHeading(): string|null|Htmlable
{ {
return trans('admin/log.navigation.panel_logs'); return trans('admin/log.navigation.panel_logs');
} }

View File

@@ -5,8 +5,10 @@ namespace App\Filament\Admin\Resources\Eggs\Pages;
use App\Enums\EditorLanguages; use App\Enums\EditorLanguages;
use App\Enums\TablerIcon; use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Eggs\EggResource; use App\Filament\Admin\Resources\Eggs\EggResource;
use App\Filament\Components\Actions\DeleteIcon;
use App\Filament\Components\Actions\ExportEggAction; use App\Filament\Components\Actions\ExportEggAction;
use App\Filament\Components\Actions\ImportEggAction; use App\Filament\Components\Actions\ImportEggAction;
use App\Filament\Components\Actions\UploadIcon;
use App\Filament\Components\Forms\Fields\CopyFrom; use App\Filament\Components\Forms\Fields\CopyFrom;
use App\Filament\Components\Forms\Fields\MonacoEditor; use App\Filament\Components\Forms\Fields\MonacoEditor;
use App\Models\Egg; use App\Models\Egg;
@@ -14,12 +16,10 @@ use App\Models\EggVariable;
use App\Traits\Filament\CanCustomizeHeaderActions; use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets; use App\Traits\Filament\CanCustomizeHeaderWidgets;
use App\Traits\Filament\CanCustomizeTabs; use App\Traits\Filament\CanCustomizeTabs;
use Exception;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Actions\ActionGroup; use Filament\Actions\ActionGroup;
use Filament\Actions\DeleteAction; use Filament\Actions\DeleteAction;
use Filament\Forms\Components\Checkbox; use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\KeyValue; use Filament\Forms\Components\KeyValue;
use Filament\Forms\Components\Repeater; use Filament\Forms\Components\Repeater;
@@ -28,11 +28,8 @@ use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea; use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle; use Filament\Forms\Components\Toggle;
use Filament\Infolists\Components\TextEntry;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
use Filament\Schemas\Components\Fieldset; use Filament\Schemas\Components\Fieldset;
use Filament\Schemas\Components\Flex;
use Filament\Schemas\Components\Grid; use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Image; use Filament\Schemas\Components\Image;
use Filament\Schemas\Components\Tabs; use Filament\Schemas\Components\Tabs;
@@ -40,10 +37,7 @@ use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\Utilities\Get; use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set; use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
use Filament\Support\Enums\IconSize;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rules\Unique; use Illuminate\Validation\Rules\Unique;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
class EditEgg extends EditRecord class EditEgg extends EditRecord
{ {
@@ -74,163 +68,17 @@ class EditEgg extends EditRecord
->icon(TablerIcon::Egg) ->icon(TablerIcon::Egg)
->schema([ ->schema([
Grid::make(2) Grid::make(2)
->columnSpan(1) ->columnStart(1)
->schema([ ->schema([
Image::make('', '') Image::make('', 'icon')
->hidden(fn ($record) => !$record->image) ->hidden(fn ($record) => !$record->icon)
->url(fn ($record) => $record->image) ->url(fn ($record) => $record->icon)
->alt('')
->alignJustify()
->imageSize(150) ->imageSize(150)
->columnSpanFull(), ->columnSpanFull()
Flex::make([ ->alignJustify(),
Action::make('uploadImage') UploadIcon::make(),
->hiddenLabel() DeleteIcon::make()
->tooltip(trans('admin/egg.import.import_image')) ->iconStoragePath(Egg::getIconStoragePath()),
->iconSize(IconSize::Large)
->icon(TablerIcon::PhotoUp)
->modal()
->modalHeading('')
->modalSubmitActionLabel(trans('admin/egg.import.import_image'))
->schema([
Tabs::make()
->contained(false)
->tabs([
Tab::make(trans('admin/egg.import.url'))
->schema([
Hidden::make('imageUrl'),
Hidden::make('imageExtension'),
TextInput::make('image_url')
->label(trans('admin/egg.import.image_url'))
->reactive()
->autocomplete(false)
->debounce(500)
->afterStateUpdated(function ($state, Set $set) {
if (!$state) {
$set('image_url_error', null);
$set('imageUrl', null);
$set('imageExtension', null);
return;
}
try {
if (!filter_var($state, FILTER_VALIDATE_URL)) {
throw new Exception(trans('admin/egg.import.invalid_url'));
}
$extension = strtolower(pathinfo(parse_url($state, PHP_URL_PATH), PATHINFO_EXTENSION));
if (!array_key_exists($extension, Egg::IMAGE_FORMATS)) {
throw new Exception(trans('admin/egg.import.unsupported_format', ['format' => implode(', ', array_keys(Egg::IMAGE_FORMATS))]));
}
$host = parse_url($state, PHP_URL_HOST);
$ip = gethostbyname($host);
if (
filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false
) {
throw new Exception(trans('admin/egg.import.no_local_ip'));
}
$set('imageUrl', $state);
$set('imageExtension', $extension);
$set('image_url_error', null);
} catch (Exception $e) {
$set('image_url_error', $e->getMessage());
$set('imageUrl', null);
$set('imageExtension', null);
}
}),
TextEntry::make('image_url_error')
->hiddenLabel()
->visible(fn ($get) => $get('image_url_error') !== null)
->afterStateHydrated(fn ($set, $get) => $get('image_url_error')),
Image::make(fn (Get $get) => $get('image_url'), '')
->imageSize(150)
->visible(fn ($get) => $get('image_url') && !$get('image_url_error'))
->alignCenter(),
]),
Tab::make(trans('admin/egg.import.file'))
->schema([
FileUpload::make('image')
->hiddenLabel()
->previewable()
->openable(false)
->downloadable(false)
->maxSize(256)
->maxFiles(1)
->columnSpanFull()
->alignCenter()
->imageEditor()
->image()
->disk('public')
->directory(Egg::ICON_STORAGE_PATH)
->acceptedFileTypes([
'image/png',
'image/jpeg',
'image/webp',
'image/svg+xml',
])
->getUploadedFileNameForStorageUsing(function (TemporaryUploadedFile $file, $record) {
return $record->uuid . '.' . $file->getClientOriginalExtension();
}),
]),
]),
])
->action(function (array $data, $record): void {
if (!empty($data['imageUrl']) && !empty($data['imageExtension'])) {
$this->saveImageFromUrl($data['imageUrl'], $data['imageExtension'], $record);
Notification::make()
->title(trans('admin/egg.import.image_updated'))
->success()
->send();
return;
}
if (!empty($data['image'])) {
Notification::make()
->title(trans('admin/egg.import.image_updated'))
->success()
->send();
return;
}
if (empty($data['imageUrl']) && empty($data['image'])) {
Notification::make()
->title(trans('admin/egg.import.no_image'))
->warning()
->send();
}
}),
Action::make('delete_image')
->visible(fn ($record) => $record->image)
->hiddenLabel()
->tooltip(trans('admin/egg.import.delete_image'))
->icon(TablerIcon::Trash)
->iconSize(IconSize::Large)
->color('danger')
->action(function ($record) {
foreach (array_keys(Egg::IMAGE_FORMATS) as $ext) {
$path = Egg::ICON_STORAGE_PATH . "/$record->uuid.$ext";
if (Storage::disk('public')->exists($path)) {
Storage::disk('public')->delete($path);
}
}
Notification::make()
->title(trans('admin/egg.import.image_deleted'))
->success()
->send();
$record->refresh();
}),
]),
]), ]),
TextInput::make('name') TextInput::make('name')
->label(trans('admin/egg.name')) ->label(trans('admin/egg.name'))
@@ -469,39 +317,6 @@ class EditEgg extends EditRecord
$this->fillForm(); $this->fillForm();
} }
/**
* Save an image from URL download to a file.
*
* @throws Exception
*/
private function saveImageFromUrl(string $imageUrl, string $extension, Egg $egg): void
{
$context = stream_context_create([
'http' => ['timeout' => 3],
'https' => [
'timeout' => 3,
'verify_peer' => true,
'verify_peer_name' => true,
],
]);
$normalizedExtension = match ($extension) {
'svg+xml', 'svg' => 'svg',
'jpeg', 'jpg' => 'jpg',
'png' => 'png',
'webp' => 'webp',
default => throw new Exception(trans('admin/egg.import.unknown_extension')),
};
$data = @file_get_contents($imageUrl, false, $context, 0, 1048576); // 1024KB
if (empty($data)) {
throw new Exception(trans('admin/egg.import.invalid_url'));
}
Storage::disk('public')->put(Egg::ICON_STORAGE_PATH . "/$egg->uuid.$normalizedExtension", $data);
}
protected function getFormActions(): array protected function getFormActions(): array
{ {
return []; return [];

View File

@@ -38,6 +38,8 @@ class ListEggs extends ListRecords
*/ */
public function table(Table $table): Table public function table(Table $table): Table
{ {
$defaultEggIcon = 'data:image/svg+xml;base64,' . base64_encode(file_get_contents(public_path('pelican.svg')));
return $table return $table
->searchable(true) ->searchable(true)
->defaultPaginationPageOption(25) ->defaultPaginationPageOption(25)
@@ -45,13 +47,11 @@ class ListEggs extends ListRecords
TextColumn::make('id') TextColumn::make('id')
->label('Id') ->label('Id')
->hidden(), ->hidden(),
ImageColumn::make('image') ImageColumn::make('icon')
->label('') ->label('')
->alignCenter() ->alignCenter()
->circular() ->circular()
->getStateUsing(fn ($record) => $record->image ->getStateUsing(fn (Egg $record) => $record->icon ?: $defaultEggIcon),
? $record->image
: 'data:image/svg+xml;base64,' . base64_encode(file_get_contents(public_path('pelican.svg')))),
TextColumn::make('name') TextColumn::make('name')
->label(trans('admin/egg.name')) ->label(trans('admin/egg.name'))
->description(fn ($record): ?string => (strlen($record->description) > 120) ? substr($record->description, 0, 120).'...' : $record->description) ->description(fn ($record): ?string => (strlen($record->description) > 120) ? substr($record->description, 0, 120).'...' : $record->description)

View File

@@ -95,6 +95,12 @@ class MountResource extends Resource
->icon(fn ($state) => $state ? TablerIcon::WritingOff : TablerIcon::Writing) ->icon(fn ($state) => $state ? TablerIcon::WritingOff : TablerIcon::Writing)
->color(fn ($state) => $state ? 'success' : 'warning') ->color(fn ($state) => $state ? 'success' : 'warning')
->formatStateUsing(fn ($state) => $state ? trans('admin/mount.toggles.read_only') : trans('admin/mount.toggles.writable')), ->formatStateUsing(fn ($state) => $state ? trans('admin/mount.toggles.read_only') : trans('admin/mount.toggles.writable')),
TextColumn::make('user_mountable')
->label(trans('admin/mount.table.user_mountable'))
->badge()
->icon(fn ($state) => $state ? TablerIcon::User : TablerIcon::UserOff)
->color(fn ($state) => $state ? 'success' : 'warning')
->formatStateUsing(fn ($state) => $state ? trans('admin/mount.toggles.user_mountable') : trans('admin/mount.toggles.not_user_mountable')),
]) ])
->recordActions([ ->recordActions([
ViewAction::make() ViewAction::make()
@@ -124,7 +130,8 @@ class MountResource extends Resource
->label(trans('admin/mount.name')) ->label(trans('admin/mount.name'))
->required() ->required()
->helperText(trans('admin/mount.name_help')) ->helperText(trans('admin/mount.name_help'))
->maxLength(64), ->maxLength(64)
->columnSpanFull(),
ToggleButtons::make('read_only') ToggleButtons::make('read_only')
->label(trans('admin/mount.read_only')) ->label(trans('admin/mount.read_only'))
->helperText(trans('admin/mount.read_only_help')) ->helperText(trans('admin/mount.read_only_help'))
@@ -143,6 +150,24 @@ class MountResource extends Resource
]) ])
->inline() ->inline()
->default(false), ->default(false),
ToggleButtons::make('user_mountable')
->label(trans('admin/mount.user_mountable'))
->helperText(trans('admin/mount.user_mountable_help'))
->stateCast(new BooleanStateCast(false, true))
->options([
false => trans('admin/mount.toggles.not_user_mountable'),
true => trans('admin/mount.toggles.user_mountable'),
])
->icons([
false => TablerIcon::UserOff,
true => TablerIcon::User,
])
->colors([
false => 'warning',
true => 'success',
])
->inline()
->default(true),
TextInput::make('source') TextInput::make('source')
->label(trans('admin/mount.source')) ->label(trans('admin/mount.source'))
->required() ->required()

View File

@@ -42,7 +42,6 @@ class CreateMount extends CreateRecord
protected function handleRecordCreation(array $data): Model protected function handleRecordCreation(array $data): Model
{ {
$data['uuid'] ??= Str::uuid()->toString(); $data['uuid'] ??= Str::uuid()->toString();
$data['user_mountable'] = 1;
return parent::handleRecordCreation($data); return parent::handleRecordCreation($data);
} }

View File

@@ -64,9 +64,8 @@ class CreateNode extends CreateRecord
->icon(TablerIcon::Server) ->icon(TablerIcon::Server)
->columnSpanFull() ->columnSpanFull()
->columns([ ->columns([
'default' => 2, 'default' => 1,
'sm' => 3, 'md' => 2,
'md' => 3,
'lg' => 4, 'lg' => 4,
]) ])
->schema([ ->schema([
@@ -76,81 +75,83 @@ class CreateNode extends CreateRecord
->autofocus() ->autofocus()
->live(debounce: 1500) ->live(debounce: 1500)
->rules(Node::getRulesForField('fqdn')) ->rules(Node::getRulesForField('fqdn'))
->prohibited(fn ($state) => is_ip($state) && request()->isSecure())
->label(fn ($state) => is_ip($state) ? trans('admin/node.ip_address') : trans('admin/node.domain')) ->label(fn ($state) => is_ip($state) ? trans('admin/node.ip_address') : trans('admin/node.domain'))
->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com') ->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
->helperText(function ($state) { ->helperText(fn () => request()->isSecure() ? trans('admin/node.fqdn_ssl') : null)
->validationMessages([
'prohibited' => trans('admin/node.dns_error'),
])
->prohibited(function ($state, Get $get) {
if (!$state) {
return true;
}
if (is_ip($state)) {
return false;
}
$ip = $get('ip');
return !is_ip($ip);
})
->hintColor(function ($state, Get $get) {
if (!$state) {
return null;
}
if (is_ip($state)) { if (is_ip($state)) {
if (request()->isSecure()) { if (request()->isSecure()) {
return trans('admin/node.fqdn_help'); return 'warning';
} }
} else {
$ip = $get('ip');
return ''; return is_ip($ip) ? 'success' : 'danger';
} }
return trans('admin/node.error'); return null;
}) })
->hintColor('danger') ->hint(function ($state, Get $get) {
->hint(function ($state) { if (!$state) {
if (is_ip($state) && request()->isSecure()) { return null;
return trans('admin/node.ssl_ip');
} }
return ''; if (is_ip($state)) {
if (request()->isSecure()) {
return trans('admin/node.ssl_ip');
}
} else {
$ip = $get('ip');
return is_ip($ip) ? trans('admin/node.valid') . ': ' . $ip : trans('admin/node.invalid');
}
return null;
}) })
->afterStateUpdated(function (Set $set, ?string $state) { ->afterStateUpdated(function (Set $set, ?string $state) {
$set('dns', null);
$set('ip', null); $set('ip', null);
if (!$state) {
return;
}
[$subdomain] = str($state)->explode('.', 2); [$subdomain] = str($state)->explode('.', 2);
if (!is_numeric($subdomain)) { if (!is_numeric($subdomain)) {
$set('name', $subdomain); $set('name', $subdomain);
} }
if (!$state || is_ip($state)) { if (!is_ip($state)) {
$set('dns', null); $ip = get_ip_from_hostname($state);
if (is_ip($ip)) {
return; $set('ip', $ip);
} } else {
$set('ip', null);
$ip = get_ip_from_hostname($state); }
if ($ip) {
$set('dns', true);
$set('ip', $ip);
} else {
$set('dns', false);
} }
}) })
->maxLength(255), ->maxLength(255),
Hidden::make('ip')
TextInput::make('ip') ->saved(false),
->disabled()
->hidden(),
ToggleButtons::make('dns')
->label(trans('admin/node.dns'))
->helperText(trans('admin/node.dns_help'))
->disabled()
->inline()
->default(null)
->hint(fn (Get $get) => $get('ip'))
->hintColor('success')
->options([
true => trans('admin/node.valid'),
false => trans('admin/node.invalid'),
])
->colors([
true => 'success',
false => 'danger',
])
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 1,
]),
TextInput::make('daemon_connect') TextInput::make('daemon_connect')
->columnSpan(1) ->columnSpan(1)
->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port')) ->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port'))
@@ -160,7 +161,16 @@ class CreateNode extends CreateRecord
->default(8080) ->default(8080)
->required() ->required()
->integer(), ->integer(),
TextInput::make('daemon_listen')
->columnSpan(1)
->label(trans('admin/node.listen_port'))
->helperText(trans('admin/node.listen_port_help'))
->minValue(1)
->maxValue(65535)
->default(8080)
->required()
->integer()
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
TextInput::make('name') TextInput::make('name')
->label(trans('admin/node.display_name')) ->label(trans('admin/node.display_name'))
->columnSpan([ ->columnSpan([
@@ -171,27 +181,16 @@ class CreateNode extends CreateRecord
]) ])
->required() ->required()
->maxLength(100), ->maxLength(100),
Hidden::make('scheme')
->default(fn () => request()->isSecure() ? 'https' : 'http'),
Hidden::make('behind_proxy')
->default(false),
ToggleButtons::make('connection') ToggleButtons::make('connection')
->label(trans('admin/node.ssl')) ->label(trans('admin/node.ssl'))
->columnSpan(1) ->columnSpan(2)
->inline() ->inline()
->helperText(function (Get $get) { ->helperText(function () {
if (request()->isSecure()) { if (request()->isSecure()) {
return new HtmlString(trans('admin/node.panel_on_ssl')); return trans('admin/node.panel_on_ssl');
} }
if (is_ip($get('fqdn'))) { return null;
return trans('admin/node.ssl_help');
}
return '';
}) })
->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure()) ->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure())
->options([ ->options([
@@ -219,17 +218,10 @@ class CreateNode extends CreateRecord
$set('daemon_connect', $state === 'https_proxy' ? 443 : 8080); $set('daemon_connect', $state === 'https_proxy' ? 443 : 8080);
$set('daemon_listen', 8080); $set('daemon_listen', 8080);
}), }),
Hidden::make('scheme')
TextInput::make('daemon_listen') ->default(fn () => request()->isSecure() ? 'https' : 'http'),
->columnSpan(1) Hidden::make('behind_proxy')
->label(trans('admin/node.listen_port')) ->default(false),
->helperText(trans('admin/node.listen_port_help'))
->minValue(1)
->maxValue(65535)
->default(8080)
->required()
->integer()
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
]), ]),
Step::make('advanced') Step::make('advanced')
->label(trans('admin/node.tabs.advanced_settings')) ->label(trans('admin/node.tabs.advanced_settings'))

View File

@@ -137,74 +137,83 @@ class EditNode extends EditRecord
->autofocus() ->autofocus()
->live(debounce: 1500) ->live(debounce: 1500)
->rules(Node::getRulesForField('fqdn')) ->rules(Node::getRulesForField('fqdn'))
->prohibited(fn ($state) => is_ip($state) && request()->isSecure())
->label(fn ($state) => is_ip($state) ? trans('admin/node.ip_address') : trans('admin/node.domain')) ->label(fn ($state) => is_ip($state) ? trans('admin/node.ip_address') : trans('admin/node.domain'))
->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com') ->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
->helperText(function ($state) { ->helperText(fn () => request()->isSecure() ? trans('admin/node.fqdn_ssl') : null)
->validationMessages([
'prohibited' => trans('admin/node.dns_error'),
])
->prohibited(function ($state, Get $get) {
if (!$state) {
return true;
}
if (is_ip($state)) {
return false;
}
$ip = $get('ip');
return !is_ip($ip);
})
->hintColor(function ($state, Get $get) {
if (!$state) {
return null;
}
if (is_ip($state)) { if (is_ip($state)) {
if (request()->isSecure()) { if (request()->isSecure()) {
return trans('admin/node.fqdn_help'); return 'warning';
} }
} else {
$ip = $get('ip');
return ''; return is_ip($ip) ? 'success' : 'danger';
} }
return trans('admin/node.error'); return null;
}) })
->hintColor('danger') ->hint(function ($state, Get $get) {
->hint(function ($state) { if (!$state) {
if (is_ip($state) && request()->isSecure()) { return null;
return trans('admin/node.ssl_ip');
} }
return ''; if (is_ip($state)) {
if (request()->isSecure()) {
return trans('admin/node.ssl_ip');
}
} else {
$ip = $get('ip');
return is_ip($ip) ? trans('admin/node.valid') . ': ' . $ip : trans('admin/node.invalid');
}
return null;
}) })
->afterStateUpdated(function (Set $set, ?string $state) { ->afterStateUpdated(function (Set $set, ?string $state) {
$set('dns', null);
$set('ip', null); $set('ip', null);
if (!$state) {
return;
}
[$subdomain] = str($state)->explode('.', 2); [$subdomain] = str($state)->explode('.', 2);
if (!is_numeric($subdomain)) { if (!is_numeric($subdomain)) {
$set('name', $subdomain); $set('name', $subdomain);
} }
if (!$state || is_ip($state)) { if (!is_ip($state)) {
$set('dns', null); $ip = get_ip_from_hostname($state);
if (is_ip($ip)) {
return; $set('ip', $ip);
} } else {
$set('ip', null);
$ip = get_ip_from_hostname($state); }
if ($ip) {
$set('dns', true);
$set('ip', $ip);
} else {
$set('dns', false);
} }
}) })
->maxLength(255), ->maxLength(255),
TextInput::make('ip') Hidden::make('ip')
->disabled() ->saved(false),
->hidden(),
ToggleButtons::make('dns')
->label(trans('admin/node.dns'))
->helperText(trans('admin/node.dns_help'))
->disabled()
->inline()
->default(null)
->hint(fn (Get $get) => $get('ip'))
->hintColor('success')
->stateCast(new BooleanStateCast(false, true))
->options([
1 => trans('admin/node.valid'),
0 => trans('admin/node.invalid'),
])
->colors([
1 => 'success',
0 => 'danger',
])
->columnSpan(1),
TextInput::make('daemon_connect') TextInput::make('daemon_connect')
->columnSpan(1) ->columnSpan(1)
->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port')) ->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port'))
@@ -214,6 +223,16 @@ class EditNode extends EditRecord
->default(8080) ->default(8080)
->required() ->required()
->integer(), ->integer(),
TextInput::make('daemon_listen')
->columnSpan(1)
->label(trans('admin/node.listen_port'))
->helperText(trans('admin/node.listen_port_help'))
->minValue(1)
->maxValue(65535)
->default(8080)
->required()
->integer()
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
TextInput::make('name') TextInput::make('name')
->label(trans('admin/node.display_name')) ->label(trans('admin/node.display_name'))
->columnSpan([ ->columnSpan([
@@ -224,22 +243,16 @@ class EditNode extends EditRecord
]) ])
->required() ->required()
->maxLength(100), ->maxLength(100),
Hidden::make('scheme'),
Hidden::make('behind_proxy'),
ToggleButtons::make('connection') ToggleButtons::make('connection')
->label(trans('admin/node.ssl')) ->label(trans('admin/node.ssl'))
->columnSpan(1) ->columnSpan(2)
->inline() ->inline()
->helperText(function (Get $get) { ->helperText(function () {
if (request()->isSecure()) { if (request()->isSecure()) {
return new HtmlString(trans('admin/node.panel_on_ssl')); return trans('admin/node.panel_on_ssl');
} }
if (is_ip($get('fqdn'))) { return null;
return trans('admin/node.ssl_help');
}
return '';
}) })
->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure()) ->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure())
->options([ ->options([
@@ -267,16 +280,10 @@ class EditNode extends EditRecord
$set('daemon_connect', $state === 'https_proxy' ? 443 : 8080); $set('daemon_connect', $state === 'https_proxy' ? 443 : 8080);
$set('daemon_listen', 8080); $set('daemon_listen', 8080);
}), }),
TextInput::make('daemon_listen') Hidden::make('scheme')
->columnSpan(1) ->default(fn () => request()->isSecure() ? 'https' : 'http'),
->label(trans('admin/node.listen_port')) Hidden::make('behind_proxy')
->helperText(trans('admin/node.listen_port_help')) ->default(false),
->minValue(1)
->maxValue(65535)
->default(8080)
->required()
->integer()
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
]), ]),
Tab::make('advanced_settings') Tab::make('advanced_settings')
->label(trans('admin/node.tabs.advanced_settings')) ->label(trans('admin/node.tabs.advanced_settings'))
@@ -735,7 +742,7 @@ class EditNode extends EditRecord
$set('pulled', false); $set('pulled', false);
$set('uploaded', true); $set('uploaded', true);
} catch (\Exception $e) { } catch (Exception $e) {
Notification::make() Notification::make()
->title(trans('admin/node.diagnostics.upload_failed')) ->title(trans('admin/node.diagnostics.upload_failed'))
->body($e->getMessage()) ->body($e->getMessage())

View File

@@ -5,6 +5,9 @@ namespace App\Filament\Admin\Resources\Plugins;
use App\Enums\PluginStatus; use App\Enums\PluginStatus;
use App\Enums\TablerIcon; use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Plugins\Pages\ListPlugins; use App\Filament\Admin\Resources\Plugins\Pages\ListPlugins;
use App\Jobs\Plugin\InstallPlugin;
use App\Jobs\Plugin\UninstallPlugin;
use App\Jobs\Plugin\UpdatePlugin;
use App\Models\Plugin; use App\Models\Plugin;
use App\Services\Helpers\PluginService; use App\Services\Helpers\PluginService;
use BackedEnum; use BackedEnum;
@@ -119,15 +122,14 @@ class PluginResource extends Resource
->icon(TablerIcon::Terminal) ->icon(TablerIcon::Terminal)
->color('success') ->color('success')
->hidden(fn (Plugin $plugin) => $plugin->status !== PluginStatus::NotInstalled) ->hidden(fn (Plugin $plugin) => $plugin->status !== PluginStatus::NotInstalled)
->action(function (Plugin $plugin, $livewire, PluginService $pluginService) { ->action(function (Plugin $plugin) {
try { try {
$pluginService->installPlugin($plugin, !$plugin->isTheme() || !$pluginService->hasThemePluginEnabled()); InstallPlugin::dispatch(user(), $plugin);
redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab]));
Notification::make() Notification::make()
->success() ->success()
->title(trans('admin/plugin.notifications.installed')) ->title(trans('admin/plugin.notifications.install_started'))
->body(trans('admin/plugin.notifications.background_info'))
->send(); ->send();
} catch (Exception $exception) { } catch (Exception $exception) {
Notification::make() Notification::make()
@@ -143,15 +145,14 @@ class PluginResource extends Resource
->icon(TablerIcon::Download) ->icon(TablerIcon::Download)
->color('success') ->color('success')
->visible(fn (Plugin $plugin) => $plugin->status !== PluginStatus::NotInstalled && $plugin->isUpdateAvailable()) ->visible(fn (Plugin $plugin) => $plugin->status !== PluginStatus::NotInstalled && $plugin->isUpdateAvailable())
->action(function (Plugin $plugin, $livewire, PluginService $pluginService) { ->action(function (Plugin $plugin) {
try { try {
$pluginService->updatePlugin($plugin); UpdatePlugin::dispatch(user(), $plugin);
redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab]));
Notification::make() Notification::make()
->success() ->success()
->title(trans('admin/plugin.notifications.updated')) ->title(trans('admin/plugin.notifications.update_started'))
->body(trans('admin/plugin.notifications.background_info'))
->send(); ->send();
} catch (Exception $exception) { } catch (Exception $exception) {
Notification::make() Notification::make()
@@ -220,15 +221,14 @@ class PluginResource extends Resource
->color('danger') ->color('danger')
->requiresConfirmation() ->requiresConfirmation()
->hidden(fn (Plugin $plugin) => $plugin->status === PluginStatus::NotInstalled || $plugin->status === PluginStatus::Errored) ->hidden(fn (Plugin $plugin) => $plugin->status === PluginStatus::NotInstalled || $plugin->status === PluginStatus::Errored)
->action(function (Plugin $plugin, $livewire, PluginService $pluginService) { ->action(function (Plugin $plugin) {
try { try {
$pluginService->uninstallPlugin($plugin); UninstallPlugin::dispatch(user(), $plugin);
redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab]));
Notification::make() Notification::make()
->success() ->success()
->title(trans('admin/plugin.notifications.uninstalled')) ->title(trans('admin/plugin.notifications.uninstall_started'))
->body(trans('admin/plugin.notifications.background_info'))
->send(); ->send();
} catch (Exception $exception) { } catch (Exception $exception) {
Notification::make() Notification::make()

View File

@@ -5,8 +5,9 @@ namespace App\Filament\Admin\Resources\Servers\Pages;
use App\Enums\SuspendAction; use App\Enums\SuspendAction;
use App\Enums\TablerIcon; use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Servers\ServerResource; use App\Filament\Admin\Resources\Servers\ServerResource;
use App\Filament\Components\Actions\DeleteServerIcon; use App\Filament\Components\Actions\DeleteIcon;
use App\Filament\Components\Actions\PreviewStartupAction; use App\Filament\Components\Actions\PreviewStartupAction;
use App\Filament\Components\Actions\UploadIcon;
use App\Filament\Components\Forms\Fields\MonacoEditor; use App\Filament\Components\Forms\Fields\MonacoEditor;
use App\Filament\Components\Forms\Fields\StartupVariable; use App\Filament\Components\Forms\Fields\StartupVariable;
use App\Filament\Components\StateCasts\ServerConditionStateCast; use App\Filament\Components\StateCasts\ServerConditionStateCast;
@@ -31,7 +32,6 @@ use Exception;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Actions\ActionGroup; use Filament\Actions\ActionGroup;
use Filament\Forms\Components\CheckboxList; use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\KeyValue; use Filament\Forms\Components\KeyValue;
use Filament\Forms\Components\Repeater; use Filament\Forms\Components\Repeater;
@@ -41,7 +41,6 @@ use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle; use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\ToggleButtons; use Filament\Forms\Components\ToggleButtons;
use Filament\Infolists\Components\TextEntry;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
use Filament\Schemas\Components\Actions; use Filament\Schemas\Components\Actions;
@@ -60,9 +59,7 @@ use Filament\Support\Enums\Alignment;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Client\ConnectionException; use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\HtmlString; use Illuminate\Support\HtmlString;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
use LogicException; use LogicException;
use Random\RandomException; use Random\RandomException;
@@ -111,141 +108,18 @@ class EditServer extends EditRecord
->icon(TablerIcon::InfoCircle) ->icon(TablerIcon::InfoCircle)
->schema([ ->schema([
Grid::make() Grid::make()
->columns(2)
->columnStart(1) ->columnStart(1)
->schema([ ->schema([
Image::make('', 'icon') Image::make('', 'icon')
->hidden(fn ($record) => !$record->icon && !$record->egg->image) ->hidden(fn ($record) => !$record->icon && !$record->egg->icon)
->url(fn ($record) => $record->icon ?: $record->egg->image) ->url(fn ($record) => $record->icon ?: $record->egg->icon)
->tooltip(fn ($record) => $record->icon ? '' : trans('server/setting.server_info.icon.tooltip')) ->tooltip(fn ($record) => $record->icon ? '' : trans('server/setting.server_info.icon.tooltip'))
->columnSpan(2) ->imageSize(150)
->columnSpanFull()
->alignJustify(), ->alignJustify(),
Action::make('uploadIcon') UploadIcon::make(),
->hiddenLabel() DeleteIcon::make()
->icon(TablerIcon::PhotoUp) ->iconStoragePath(Server::getIconStoragePath()),
->tooltip(trans('admin/server.import_image'))
->modal()
->modalSubmitActionLabel(trans('server/setting.server_info.icon.upload'))
->schema([
Tabs::make()
->contained(false)
->tabs([
Tab::make(trans('admin/egg.import.url'))
->schema([
Hidden::make('imageUrl'),
Hidden::make('imageExtension'),
TextInput::make('image_url')
->label(trans('admin/egg.import.image_url'))
->reactive()
->autocomplete(false)
->debounce(500)
->afterStateUpdated(function ($state, Set $set) {
if (!$state) {
$set('image_url_error', null);
$set('imageUrl', null);
$set('imageExtension', null);
return;
}
try {
if (!in_array(parse_url($state, PHP_URL_SCHEME), ['http', 'https'], true)) {
throw new \Exception(trans('admin/egg.import.invalid_url'));
}
if (!filter_var($state, FILTER_VALIDATE_URL)) {
throw new \Exception(trans('admin/egg.import.invalid_url'));
}
$extension = strtolower(pathinfo(parse_url($state, PHP_URL_PATH), PATHINFO_EXTENSION));
if (!array_key_exists($extension, Server::IMAGE_FORMATS)) {
throw new \Exception(trans('admin/egg.import.unsupported_format', ['format' => implode(', ', array_keys(Server::IMAGE_FORMATS))]));
}
$host = parse_url($state, PHP_URL_HOST);
$ip = gethostbyname($host);
if (
filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false
) {
throw new \Exception(trans('admin/egg.import.no_local_ip'));
}
$set('imageUrl', $state);
$set('imageExtension', $extension);
$set('image_url_error', null);
} catch (\Exception $e) {
$set('image_url_error', $e->getMessage());
$set('imageUrl', null);
$set('imageExtension', null);
}
}),
TextEntry::make('image_url_error')
->hiddenLabel()
->visible(fn (Get $get) => $get('image_url_error') !== null)
->afterStateHydrated(fn (Get $get) => $get('image_url_error')),
Image::make(fn (Get $get) => $get('image_url'), '')
->imageSize(150)
->visible(fn (Get $get) => $get('image_url') && !$get('image_url_error'))
->alignCenter(),
]),
Tab::make(trans('admin/egg.import.file'))
->schema([
FileUpload::make('image')
->hiddenLabel()
->previewable()
->openable(false)
->downloadable(false)
->maxSize(256)
->maxFiles(1)
->columnSpanFull()
->alignCenter()
->imageEditor()
->image()
->disk('public')
->directory(Server::ICON_STORAGE_PATH)
->acceptedFileTypes([
'image/png',
'image/jpeg',
'image/webp',
'image/svg+xml',
])
->getUploadedFileNameForStorageUsing(function (TemporaryUploadedFile $file, $record) {
return $record->uuid . '.' . $file->getClientOriginalExtension();
}),
]),
]),
])
->action(function (array $data, $record): void {
if (!empty($data['imageUrl']) && !empty($data['imageExtension'])) {
$this->saveIconFromUrl($data['imageUrl'], $data['imageExtension'], $record);
Notification::make()
->title(trans('server/setting.server_info.icon.updated'))
->success()
->send();
return;
}
if (!empty($data['image'])) {
Notification::make()
->title(trans('server/setting.server_info.icon.updated'))
->success()
->send();
return;
}
if (empty($data['imageUrl']) && empty($data['image'])) {
Notification::make()
->title(trans('admin/egg.import.no_image'))
->warning()
->send();
}
}),
DeleteServerIcon::make(),
]), ]),
Grid::make() Grid::make()
->columns(3) ->columns(3)
@@ -321,7 +195,7 @@ class EditServer extends EditRecord
try { try {
$logs = $serverRepository->setServer($server)->getInstallLogs(); $logs = $serverRepository->setServer($server)->getInstallLogs();
return mb_convert_encoding($logs, 'UTF-8', ['UTF-8', 'UTF-16', 'ISO-8859-1', 'ASCII']); return convert_to_utf8($logs);
} catch (ConnectionException) { } catch (ConnectionException) {
Notification::make() Notification::make()
->title(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name])) ->title(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
@@ -1202,37 +1076,4 @@ class EditServer extends EditRecord
{ {
return null; return null;
} }
/**
* Save an icon from URL download to a file.
*
* @throws Exception
*/
private function saveIconFromUrl(string $imageUrl, string $extension, Server $server): void
{
$context = stream_context_create([
'http' => ['timeout' => 3],
'https' => [
'timeout' => 3,
'verify_peer' => true,
'verify_peer_name' => true,
],
]);
$normalizedExtension = match ($extension) {
'svg+xml', 'svg' => 'svg',
'jpeg', 'jpg' => 'jpg',
'png' => 'png',
'webp' => 'webp',
default => throw new Exception(trans('admin/egg.import.unknown_extension')),
};
$data = @file_get_contents($imageUrl, false, $context, 0, 262144); //256KB
if (empty($data)) {
throw new Exception(trans('admin/egg.import.invalid_url'));
}
Storage::disk('public')->put(Server::ICON_STORAGE_PATH . "/$server->uuid.$normalizedExtension", $data);
}
} }

View File

@@ -70,7 +70,7 @@ class ListServers extends ListRecords
ImageColumn::make('icon') ImageColumn::make('icon')
->label('') ->label('')
->imageSize(46) ->imageSize(46)
->state(fn (Server $server) => $server->icon ?: $server->egg->image), ->state(fn (Server $server) => $server->icon ?: $server->egg->icon),
TextColumn::make('condition') TextColumn::make('condition')
->label(trans('server/dashboard.status')) ->label(trans('server/dashboard.status'))
->badge() ->badge()
@@ -81,7 +81,8 @@ class ListServers extends ListRecords
->label(trans('server/dashboard.title')) ->label(trans('server/dashboard.title'))
->description(fn (Server $server) => $server->description) ->description(fn (Server $server) => $server->description)
->grow() ->grow()
->searchable(), ->searchable()
->sortable(),
TextColumn::make('allocation.address') TextColumn::make('allocation.address')
->label('') ->label('')
->badge() ->badge()

View File

@@ -0,0 +1,79 @@
<?php
namespace App\Filament\Components\Actions;
use App\Enums\TablerIcon;
use App\Models\Traits\HasIcon;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Illuminate\Support\Facades\Storage;
class DeleteIcon extends Action
{
/** @var string[] */
protected ?array $iconFormats = null;
protected ?string $iconStoragePath = null;
public static function getDefaultName(): ?string
{
return 'delete_icon';
}
protected function setUp(): void
{
parent::setUp();
$this->visible(fn ($record) => $record->icon);
$this->hiddenLabel();
$this->tooltip(trans('admin/egg.import.delete_icon'));
$this->icon(TablerIcon::Trash);
$this->color('danger');
$this->action(function ($record) {
foreach ($this->getIconFormats() as $ext) {
$path = $this->getIconStoragePath() . "/$record->uuid.$ext";
if (Storage::disk('public')->exists($path)) {
Storage::disk('public')->delete($path);
}
}
Notification::make()
->title(trans('admin/egg.import.icon_deleted'))
->success()
->send();
$record->refresh();
});
}
/** @param string[] $iconFormats */
public function iconFormats(?array $iconFormats): static
{
$this->iconFormats = $iconFormats;
return $this;
}
public function iconStoragePath(?string $iconStoragePath): static
{
$this->iconStoragePath = $iconStoragePath;
return $this;
}
/** @return string[] */
public function getIconFormats(): array
{
return $this->iconFormats ?? array_keys(HasIcon::$iconFormats);
}
public function getIconStoragePath(): ?string
{
return $this->iconStoragePath;
}
}

View File

@@ -1,48 +0,0 @@
<?php
namespace App\Filament\Components\Actions;
use App\Enums\TablerIcon;
use App\Models\Server;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Illuminate\Support\Facades\Storage;
class DeleteServerIcon extends Action
{
public static function getDefaultName(): ?string
{
return 'delete_icon';
}
protected function setUp(): void
{
parent::setUp();
$this->visible(fn ($record) => $record->icon);
$this->hiddenLabel();
$this->tooltip(trans('admin/server.import_image'));
$this->icon(TablerIcon::Trash);
$this->color('danger');
$this->action(function ($record) {
foreach (array_keys(Server::IMAGE_FORMATS) as $ext) {
$path = Server::ICON_STORAGE_PATH . "/$record->uuid.$ext";
if (Storage::disk('public')->exists($path)) {
Storage::disk('public')->delete($path);
}
}
Notification::make()
->title(trans('server/setting.server_info.icon.deleted'))
->success()
->send();
$record->refresh();
});
}
}

View File

@@ -0,0 +1,164 @@
<?php
namespace App\Filament\Components\Actions;
use App\Enums\TablerIcon;
use App\Models\Traits\HasIcon;
use Exception;
use Filament\Actions\Action;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Notifications\Notification;
use Filament\Schemas\Components\Image;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Illuminate\Support\Facades\Http;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
class UploadIcon extends Action
{
/** @var string[] */
protected ?array $iconFormats = null;
public static function getDefaultName(): ?string
{
return 'upload_icon';
}
protected function setUp(): void
{
parent::setUp();
$this->hiddenLabel();
$this->tooltip(trans('admin/egg.import.import_icon'));
$this->icon(TablerIcon::PhotoUp);
$this->modal();
$this->modalHeading('');
$this->modalSubmitActionLabel(trans('admin/egg.import.import_icon'));
$this->schema([
Tabs::make()
->contained(false)
->tabs([
Tab::make(trans('admin/egg.import.url'))
->schema([
TextInput::make('icon_url')
->label(trans('admin/egg.import.icon_url'))
->reactive()
->autocomplete(false)
->debounce(500)
->afterStateUpdated(function ($state, Set $set) {
if (!$state) {
$set('icon_url_error', null);
return;
}
try {
$this->validateIconUrl($state);
$set('icon_url_error', null);
} catch (Exception $exception) {
$set('icon_url_error', $exception->getMessage());
}
}),
TextEntry::make('icon_url_error')
->hiddenLabel()
->visible(fn (Get $get) => $get('icon_url_error') !== null)
->afterStateHydrated(fn (Get $get) => $get('icon_url_error')),
Image::make(fn (Get $get) => $get('icon_url'), '')
->imageSize(150)
->visible(fn (Get $get) => $get('icon_url') && !$get('icon_url_error'))
->alignCenter(),
]),
Tab::make(trans('admin/egg.import.file'))
->schema([
FileUpload::make('icon')
->hiddenLabel()
->previewable()
->openable(false)
->downloadable(false)
->maxSize(256)
->maxFiles(1)
->columnSpanFull()
->alignCenter()
->imageEditor()
->image()
->acceptedFileTypes(fn () => $this->getIconFormats())
->saveUploadedFileUsing(fn (TemporaryUploadedFile $file, $record) => $record->writeIcon($file->getClientOriginalExtension(), $file->getContent())),
]),
]),
]);
$this->action(function (array $data, $record) {
if (!empty($data['icon_url'])) {
$this->validateIconUrl($data['icon_url']);
$content = Http::timeout(5)->connectTimeout(1)->withoutRedirecting()->get($data['icon_url'])->body();
if (empty($content)) {
throw new Exception(trans('admin/egg.import.invalid_url'));
}
$extension = strtolower(pathinfo(parse_url($data['icon_url'], PHP_URL_PATH), PATHINFO_EXTENSION));
$record->writeIcon($extension, $content);
Notification::make()
->title(trans('admin/egg.import.icon_updated'))
->success()
->send();
} elseif (!empty($data['icon'])) {
Notification::make()
->title(trans('admin/egg.import.icon_updated'))
->success()
->send();
} else {
Notification::make()
->title(trans('admin/egg.import.no_icon'))
->warning()
->send();
}
});
}
protected function validateIconUrl(string $url): void
{
if (!in_array(parse_url($url, PHP_URL_SCHEME), ['http', 'https'], true)) {
throw new Exception(trans('admin/egg.import.invalid_url'));
}
if (!filter_var($url, FILTER_VALIDATE_URL)) {
throw new Exception(trans('admin/egg.import.invalid_url'));
}
$host = parse_url($url, PHP_URL_HOST);
$ip = gethostbyname($host);
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
throw new Exception(trans('admin/egg.import.no_local_ip'));
}
}
/** @param string[] $iconFormats */
public function iconFormats(?array $iconFormats): static
{
$this->iconFormats = $iconFormats;
return $this;
}
/** @return string[] */
public function getIconFormats(): array
{
return $this->iconFormats ?? array_values(HasIcon::$iconFormats);
}
}

View File

@@ -49,7 +49,7 @@ class StartupVariable extends Field
$this->hintIcon(TablerIcon::Code, fn (StartupVariable $component) => implode('|', $component->getVariableRules())); $this->hintIcon(TablerIcon::Code, fn (StartupVariable $component) => implode('|', $component->getVariableRules()));
$this->helperText(fn (StartupVariable $component) => !$component->getVariableDesc() ? '—' : $component->getVariableDesc()); $this->helperText(fn (StartupVariable $component) => $component->getVariableDesc());
$this->rules(fn (StartupVariable $component) => $component->getVariableRules()); $this->rules(fn (StartupVariable $component) => $component->getVariableRules());
@@ -70,7 +70,7 @@ class StartupVariable extends Field
], ],
StartupVariableType::Toggle => [ StartupVariableType::Toggle => [
...parent::getDefaultStateCasts(), ...parent::getDefaultStateCasts(),
new BooleanStateCast(false), new BooleanStateCast(false, true),
], ],
default => parent::getDefaultStateCasts() default => parent::getDefaultStateCasts()
}; };

View File

@@ -21,9 +21,9 @@ class TagsFilter extends BaseFilter
{ {
parent::setUp(); parent::setUp();
$this->query(fn (Builder $query, array $data) => $query->when($data['tag'], fn (Builder $query, $tag) => $query->whereJsonContains('tags', $tag))); $this->query(fn (Builder $query, array $data) => $query->when($data['tag'] ?? null, fn (Builder $query, $tag) => $query->whereJsonContains('tags', $tag)));
$this->indicateUsing(fn (array $data) => $data['tag'] ? 'Tag: ' . $data['tag'] : null); $this->indicateUsing(fn (array $data) => ($data['tag'] ?? null) ? 'Tag: ' . $data['tag'] : null);
$this->resetState(['tag' => null]); $this->resetState(['tag' => null]);

View File

@@ -0,0 +1,114 @@
<?php
namespace App\Filament\Server\Pages;
use App\Enums\SubuserPermission;
use App\Enums\TablerIcon;
use App\Facades\Activity;
use App\Models\Mount;
use App\Models\Server;
use BackedEnum;
use Exception;
use Filament\Facades\Filament;
use Filament\Forms\Components\CheckboxList;
use Filament\Notifications\Notification;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Illuminate\Support\HtmlString;
class Mounts extends ServerFormPage
{
protected static string|BackedEnum|null $navigationIcon = TablerIcon::LayersLinked;
protected static ?int $navigationSort = 9;
public static function canAccess(): bool
{
return parent::canAccess() && user()?->can(SubuserPermission::MountRead, Filament::getTenant());
}
protected function authorizeAccess(): void
{
abort_unless(user()?->can(SubuserPermission::MountRead, Filament::getTenant()), 403);
}
protected function fillForm(): void
{
$this->form->fill([
'mounts' => $this->getRecord()->mounts->pluck('id')->toArray(),
]);
}
public function form(Schema $schema): Schema
{
$server = $this->getRecord();
$allowedMounts = Mount::query()
->where('user_mountable', true)
->where(function ($query) use ($server) {
$query->whereDoesntHave('nodes')
->orWhereHas('nodes', fn ($q) => $q->where('nodes.id', $server->node_id));
})
->where(function ($query) use ($server) {
$query->whereDoesntHave('eggs')
->orWhereHas('eggs', fn ($q) => $q->where('eggs.id', $server->egg_id));
})
->get();
return parent::form($schema)
->components([
Section::make([
CheckboxList::make('mounts')
->label(trans('server/mount.description'))
->relationship('mounts')
->options(fn () => $allowedMounts->mapWithKeys(fn (Mount $mount) => [$mount->id => $mount->name]))
->descriptions(fn () => $allowedMounts->mapWithKeys(fn (Mount $mount) => [$mount->id => new HtmlString(str("$mount->source -> $mount->target")->stripTags() . ($mount->description ? '<br>' . str($mount->description)->stripTags() : ''))]))
->helperText(fn () => $allowedMounts->isEmpty() ? trans('server/mount.no_mounts') : null)
->disabled(fn (Server $server) => !user()?->can(SubuserPermission::MountUpdate, $server))
->bulkToggleable()
->live()
->afterStateUpdated(function ($state) {
$this->save();
})
->columnSpanFull(),
]),
]);
}
public function save(): void
{
abort_unless(user()?->can(SubuserPermission::MountUpdate, $this->getRecord()), 403);
try {
$this->form->getState();
$this->form->saveRelationships();
Activity::event('server:mount.update')
->log();
Notification::make()
->title(trans('server/mount.notification_updated'))
->body(trans('server/mount.notification_updated_body'))
->success()
->send();
} catch (Exception $exception) {
report($exception);
Notification::make()
->title(trans('server/mount.notification_failed'))
->body($exception->getMessage())
->danger()
->send();
}
}
public function getTitle(): string
{
return trans('server/mount.title');
}
public static function getNavigationLabel(): string
{
return trans('server/mount.title');
}
}

View File

@@ -5,14 +5,13 @@ namespace App\Filament\Server\Pages;
use App\Enums\SubuserPermission; use App\Enums\SubuserPermission;
use App\Enums\TablerIcon; use App\Enums\TablerIcon;
use App\Facades\Activity; use App\Facades\Activity;
use App\Filament\Components\Actions\DeleteServerIcon; use App\Filament\Components\Actions\DeleteIcon;
use App\Filament\Components\Actions\UploadIcon;
use App\Models\Server; use App\Models\Server;
use App\Services\Servers\ReinstallServerService; use App\Services\Servers\ReinstallServerService;
use BackedEnum; use BackedEnum;
use Exception; use Exception;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Textarea; use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry; use Filament\Infolists\Components\TextEntry;
@@ -21,20 +20,14 @@ use Filament\Schemas\Components\Fieldset;
use Filament\Schemas\Components\Grid; use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Image; use Filament\Schemas\Components\Image;
use Filament\Schemas\Components\Section; use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
use Filament\Support\Enums\Alignment; use Filament\Support\Enums\Alignment;
use Illuminate\Support\Facades\Storage;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
class Settings extends ServerFormPage class Settings extends ServerFormPage
{ {
protected static string|BackedEnum|null $navigationIcon = TablerIcon::Settings; protected static string|BackedEnum|null $navigationIcon = TablerIcon::Settings;
protected static ?int $navigationSort = 10; protected static ?int $navigationSort = 11;
/** /**
* @throws Exception * @throws Exception
@@ -79,140 +72,20 @@ class Settings extends ServerFormPage
->afterStateUpdated(fn ($state, Server $server) => $this->updateDescription($state ?? '', $server)), ->afterStateUpdated(fn ($state, Server $server) => $this->updateDescription($state ?? '', $server)),
]), ]),
Grid::make() Grid::make()
->columns(2)
->columnStart(6) ->columnStart(6)
->schema([ ->schema([
Image::make('', 'icon') Image::make('', 'icon')
->hidden(fn ($record) => !$record->icon && !$record->egg->image) ->hidden(fn ($record) => !$record->icon && !$record->egg->icon)
->url(fn ($record) => $record->icon ?: $record->egg->image) ->url(fn ($record) => $record->icon ?: $record->egg->icon)
->tooltip(fn ($record) => $record->icon ? '' : trans('server/setting.server_info.icon.tooltip')) ->tooltip(fn ($record) => $record->icon ? '' : trans('server/setting.server_info.icon.tooltip'))
->columnSpan(2) ->imageSize(150)
->columnSpanFull()
->alignJustify(), ->alignJustify(),
Action::make('uploadIcon') UploadIcon::make()
->hiddenLabel() ->authorize(fn (Server $server) => user()?->can(SubuserPermission::SettingsChangeIcon, $server)),
->tooltip(trans('admin/server.import_image')) DeleteIcon::make()
->icon(TablerIcon::PhotoUp) ->iconStoragePath(Server::getIconStoragePath())
->modal() ->authorize(fn (Server $server) => user()?->can(SubuserPermission::SettingsChangeIcon, $server)),
->modalSubmitActionLabel(trans('server/setting.server_info.icon.upload'))
->schema([
Tabs::make()
->contained(false)
->tabs([
Tab::make(trans('admin/egg.import.url'))
->schema([
Hidden::make('imageUrl'),
Hidden::make('imageExtension'),
TextInput::make('image_url')
->label(trans('admin/egg.import.image_url'))
->reactive()
->autocomplete(false)
->debounce(500)
->afterStateUpdated(function ($state, Set $set) {
if (!$state) {
$set('image_url_error', null);
$set('imageUrl', null);
$set('imageExtension', null);
return;
}
try {
if (!in_array(parse_url($state, PHP_URL_SCHEME), ['http', 'https'], true)) {
throw new \Exception(trans('admin/egg.import.invalid_url'));
}
if (!filter_var($state, FILTER_VALIDATE_URL)) {
throw new \Exception(trans('admin/egg.import.invalid_url'));
}
$extension = strtolower(pathinfo(parse_url($state, PHP_URL_PATH), PATHINFO_EXTENSION));
if (!array_key_exists($extension, Server::IMAGE_FORMATS)) {
throw new \Exception(trans('admin/egg.import.unsupported_format', ['format' => implode(', ', array_keys(Server::IMAGE_FORMATS))]));
}
$host = parse_url($state, PHP_URL_HOST);
$ip = gethostbyname($host);
if (
filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false
) {
throw new \Exception(trans('admin/egg.import.no_local_ip'));
}
$set('imageUrl', $state);
$set('imageExtension', $extension);
$set('image_url_error', null);
} catch (\Exception $e) {
$set('image_url_error', $e->getMessage());
$set('imageUrl', null);
$set('imageExtension', null);
}
}),
TextEntry::make('image_url_error')
->hiddenLabel()
->visible(fn (Get $get) => $get('image_url_error') !== null)
->afterStateHydrated(fn (Get $get) => $get('image_url_error')),
Image::make(fn (Get $get) => $get('image_url'), '')
->imageSize(150)
->visible(fn (Get $get) => $get('image_url') && !$get('image_url_error'))
->alignCenter(),
]),
Tab::make(trans('admin/egg.import.file'))
->schema([
FileUpload::make('image')
->hiddenLabel()
->previewable()
->openable(false)
->downloadable(false)
->maxSize(256)
->maxFiles(1)
->columnSpanFull()
->alignCenter()
->imageEditor()
->image()
->disk('public')
->directory(Server::ICON_STORAGE_PATH)
->acceptedFileTypes([
'image/png',
'image/jpeg',
'image/webp',
'image/svg+xml',
])
->getUploadedFileNameForStorageUsing(function (TemporaryUploadedFile $file, $record) {
return $record->uuid . '.' . $file->getClientOriginalExtension();
}),
]),
]),
])
->action(function (array $data, $record): void {
if (!empty($data['imageUrl']) && !empty($data['imageExtension'])) {
$this->saveIconFromUrl($data['imageUrl'], $data['imageExtension'], $record);
Notification::make()
->title(trans('server/setting.server_info.icon.updated'))
->success()
->send();
return;
}
if (!empty($data['image'])) {
Notification::make()
->title(trans('server/setting.server_info.icon.updated'))
->success()
->send();
}
if (empty($data['imageUrl']) && empty($data['image'])) {
Notification::make()
->title(trans('admin/egg.import.no_image'))
->warning()
->send();
}
}),
DeleteServerIcon::make(),
]), ]),
TextInput::make('uuid') TextInput::make('uuid')
->label(trans('server/setting.server_info.uuid')) ->label(trans('server/setting.server_info.uuid'))
@@ -446,39 +319,6 @@ class Settings extends ServerFormPage
} }
} }
/**
* Save an icon from URL download to a file.
*
* @throws Exception
*/
private function saveIconFromUrl(string $imageUrl, string $extension, Server $server): void
{
$context = stream_context_create([
'http' => ['timeout' => 3],
'https' => [
'timeout' => 3,
'verify_peer' => true,
'verify_peer_name' => true,
],
]);
$normalizedExtension = match ($extension) {
'svg+xml', 'svg' => 'svg',
'jpeg', 'jpg' => 'jpg',
'png' => 'png',
'webp' => 'webp',
default => throw new Exception(trans('admin/egg.import.unknown_extension')),
};
$data = @file_get_contents($imageUrl, false, $context, 0, 262144); //256KB
if (empty($data)) {
throw new Exception(trans('admin/egg.import.invalid_url'));
}
Storage::disk('public')->put(Server::ICON_STORAGE_PATH . "/$server->uuid.$normalizedExtension", $data);
}
public function getTitle(): string public function getTitle(): string
{ {
return trans('server/setting.title'); return trans('server/setting.title');

View File

@@ -28,7 +28,7 @@ class Startup extends ServerFormPage
{ {
protected static string|BackedEnum|null $navigationIcon = TablerIcon::PlayerPlay; protected static string|BackedEnum|null $navigationIcon = TablerIcon::PlayerPlay;
protected static ?int $navigationSort = 9; protected static ?int $navigationSort = 10;
/** /**
* @throws Exception * @throws Exception
@@ -149,12 +149,16 @@ class Startup extends ServerFormPage
return parent::canAccess() && user()?->can(SubuserPermission::StartupRead, Filament::getTenant()); return parent::canAccess() && user()?->can(SubuserPermission::StartupRead, Filament::getTenant());
} }
public function update(?string $state, ServerVariable $serverVariable): void public function update(null|string|bool $state, ServerVariable $serverVariable): void
{ {
if (!$serverVariable->variable->user_editable) { if (!$serverVariable->variable->user_editable) {
return; return;
} }
if (is_bool($state)) {
$state = $state ? '1' : '0';
}
$original = $serverVariable->variable_value; $original = $serverVariable->variable_value;
try { try {

View File

@@ -139,6 +139,7 @@ class ActivityResource extends Resource
]); ]);
} }
/** @return Builder<ActivityLog> */
public static function getEloquentQuery(): Builder public static function getEloquentQuery(): Builder
{ {
/** @var Server $server */ /** @var Server $server */

View File

@@ -149,7 +149,7 @@ class EditFiles extends Page
try { try {
$contents = $this->getDaemonFileRepository()->getContent($this->path, config('panel.files.max_edit_size')); $contents = $this->getDaemonFileRepository()->getContent($this->path, config('panel.files.max_edit_size'));
return mb_convert_encoding($contents, 'UTF-8', ['UTF-8', 'UTF-16', 'ISO-8859-1', 'ASCII']); return convert_to_utf8($contents);
} catch (FileSizeTooLargeException) { } catch (FileSizeTooLargeException) {
AlertBanner::make('file_too_large') AlertBanner::make('file_too_large')
->title(trans('server/file.alerts.file_too_large.title', ['name' => basename($this->path)])) ->title(trans('server/file.alerts.file_too_large.title', ['name' => basename($this->path)]))
@@ -259,9 +259,9 @@ class EditFiles extends Page
return $this->fileRepository; return $this->fileRepository;
} }
public static function getUrl(array $parameters = [], bool $isAbsolute = true, ?string $panel = null, ?Model $tenant = null, bool $shouldGuessMissingParameters = false): string public static function getUrl(array $parameters = [], bool $isAbsolute = true, ?string $panel = null, ?Model $tenant = null, bool $shouldGuessMissingParameters = false, ?string $configuration = null): string
{ {
return parent::getUrl($parameters, $isAbsolute, $panel, $tenant) . '/'; return parent::getUrl($parameters, $isAbsolute, $panel, $tenant, $shouldGuessMissingParameters, $configuration) . '/';
} }
public static function route(string $path): PageRegistration public static function route(string $path): PageRegistration

View File

@@ -79,15 +79,15 @@ class SubuserResource extends Resource
foreach ($data['permissions'] as $permission) { foreach ($data['permissions'] as $permission) {
$options[$permission] = str($permission)->headline(); $options[$permission] = str($permission)->headline();
$descriptions[$permission] = trans('server/user.permissions.' . $data['name'] . '_' . str($permission)->replace('-', '_')); $descriptions[$permission] = trans($data['translation_prefix']. '.' . $data['name'] . '_' . str($permission)->replace('-', '_'));
$permissionsArray[$data['name']][] = $permission; $permissionsArray[$data['name']][] = $permission;
} }
$tabs[] = Tab::make($data['name']) $tabs[] = Tab::make($data['name'])
->label(str($data['name'])->headline()) ->label(trans($data['translation_prefix']. '.' . $data['name'] . '_title'))
->schema([ ->schema([
Section::make() Section::make()
->description(trans('server/user.permissions.' . $data['name'] . '_desc')) ->description(trans($data['translation_prefix']. '.' . $data['name'] . '_desc'))
->icon($data['icon']) ->icon($data['icon'])
->contained(false) ->contained(false)
->schema([ ->schema([

View File

@@ -13,10 +13,17 @@ use Illuminate\Auth\SessionGuard;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Support\Facades\RateLimiter;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
use Throwable; use Throwable;
class AccountController extends ClientApiController class AccountController extends ClientApiController
{ {
/**
* The number of seconds that must elapse before the email change throttle resets.
*/
private const EMAIL_UPDATE_THROTTLE = 60 * 60 * 24;
/** /**
* AccountController constructor. * AccountController constructor.
*/ */
@@ -63,10 +70,22 @@ class AccountController extends ClientApiController
*/ */
public function updateEmail(UpdateEmailRequest $request): JsonResponse public function updateEmail(UpdateEmailRequest $request): JsonResponse
{ {
$original = $request->user()->email; $user = $request->user();
$this->updateService->handle($request->user(), $request->validated());
// Only allow a user to change their email three times in the span
// of 24 hours. This prevents malicious users from trying to find
// existing accounts in the system by constantly changing their email.
if (RateLimiter::tooManyAttempts($key = "user:update-email:{$user->uuid}", 3)) {
throw new TooManyRequestsHttpException(message: 'Your email address has been changed too many times today. Please try again later.');
}
$original = $user->email;
if (mb_strtolower($original) !== mb_strtolower($request->validated('email'))) {
RateLimiter::hit($key, self::EMAIL_UPDATE_THROTTLE);
$this->updateService->handle($user, $request->validated());
if ($original !== $request->input('email')) {
Activity::event('user:account.email-changed') Activity::event('user:account.email-changed')
->property(['old' => $original, 'new' => $request->input('email')]) ->property(['old' => $original, 'new' => $request->input('email')])
->log(); ->log();
@@ -85,7 +104,9 @@ class AccountController extends ClientApiController
*/ */
public function updatePassword(UpdatePasswordRequest $request): JsonResponse public function updatePassword(UpdatePasswordRequest $request): JsonResponse
{ {
$user = $this->updateService->handle($request->user(), $request->validated()); $user = Activity::event('user:account.password-changed')->transaction(function () use ($request) {
return $this->updateService->handle($request->user(), $request->validated());
});
$guard = $this->manager->guard(); $guard = $this->manager->guard();
// If you do not update the user in the session you'll end up working with a // If you do not update the user in the session you'll end up working with a
@@ -98,8 +119,6 @@ class AccountController extends ClientApiController
$guard->logoutOtherDevices($request->input('password')); $guard->logoutOtherDevices($request->input('password'));
} }
Activity::event('user:account.password-changed')->log();
return new JsonResponse([], Response::HTTP_NO_CONTENT); return new JsonResponse([], Response::HTTP_NO_CONTENT);
} }
} }

View File

@@ -87,7 +87,7 @@ class BackupController extends ClientApiController
} }
$backup = Activity::event('server:backup.start')->transaction(function ($log) use ($action, $server, $request) { $backup = Activity::event('server:backup.start')->transaction(function ($log) use ($action, $server, $request) {
$server->backups()->lockForUpdate(); $server->backups()->lockForUpdate()->count();
$backup = $action->handle($server, $request->input('name')); $backup = $action->handle($server, $request->input('name'));

View File

@@ -60,7 +60,7 @@ class DatabaseController extends ClientApiController
public function store(StoreDatabaseRequest $request, Server $server): array public function store(StoreDatabaseRequest $request, Server $server): array
{ {
$database = Activity::event('server:database.create')->transaction(function ($log) use ($request, $server) { $database = Activity::event('server:database.create')->transaction(function ($log) use ($request, $server) {
$server->databases()->lockForUpdate(); $server->databases()->lockForUpdate()->count();
$database = $this->deployDatabaseService->handle($server, $request->validated()); $database = $this->deployDatabaseService->handle($server, $request->validated());
@@ -87,15 +87,12 @@ class DatabaseController extends ClientApiController
*/ */
public function rotatePassword(RotatePasswordRequest $request, Server $server, Database $database): array public function rotatePassword(RotatePasswordRequest $request, Server $server, Database $database): array
{ {
$this->managementService->rotatePassword($database);
$database->refresh();
Activity::event('server:database.rotate-password') Activity::event('server:database.rotate-password')
->subject($database) ->subject($database)
->property('name', $database->database) ->property('name', $database->database)
->log(); ->transaction(fn () => $this->managementService->rotatePassword($database));
return $this->fractal->item($database) return $this->fractal->item($database->refresh())
->parseIncludes(['password']) ->parseIncludes(['password'])
->transformWith($this->getTransformer(DatabaseTransformer::class)) ->transformWith($this->getTransformer(DatabaseTransformer::class))
->toArray(); ->toArray();

View File

@@ -77,7 +77,7 @@ class FileController extends ClientApiController
->property('file', $request->get('file')) ->property('file', $request->get('file'))
->log(); ->log();
return new Response($response, Response::HTTP_OK, ['Content-Type' => 'text/plain']); return new Response(convert_to_utf8($response), Response::HTTP_OK, ['Content-Type' => 'text/plain; charset=utf-8']);
} }
/** /**

View File

@@ -87,13 +87,13 @@ class StartupController extends ClientApiController
$startup = $this->startupCommandService->handle($server); $startup = $this->startupCommandService->handle($server);
if ($variable->env_variable !== $request->input('value')) { if ($original !== $request->input('value')) {
Activity::event('server:startup.edit') Activity::event('server:startup.edit')
->subject($variable) ->subject($variable)
->property([ ->property([
'variable' => $variable->env_variable, 'variable' => $variable->env_variable,
'old' => $original, 'old' => $original,
'new' => $request->input('value'), 'new' => $request->input('value') ?? '',
]) ])
->log(); ->log();
} }

View File

@@ -56,7 +56,7 @@ class BackupRemoteUploadController extends Controller
/** @var Server $server */ /** @var Server $server */
$server = $model->server; $server = $model->server;
if ($server->node_id !== $node->id) { if ($server->node_id !== $node->id) {
throw new HttpForbiddenException('You do not have permission to access that backup.'); throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
} }
// Prevent backups that have already been completed from trying to // Prevent backups that have already been completed from trying to

View File

@@ -47,7 +47,7 @@ class BackupStatusController extends Controller
/** @var Server $server */ /** @var Server $server */
$server = $model->server; $server = $model->server;
if ($server->node_id !== $node->id) { if ($server->node_id !== $node->id) {
throw new HttpForbiddenException('You do not have permission to access that backup.'); throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
} }
if ($model->is_successful) { if ($model->is_successful) {
@@ -97,6 +97,11 @@ class BackupStatusController extends Controller
/** @var Backup $model */ /** @var Backup $model */
$model = Backup::query()->where('uuid', $backup)->firstOrFail(); $model = Backup::query()->where('uuid', $backup)->firstOrFail();
$node = $request->attributes->get('node');
if (!$model->server->node->is($node)) {
throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
}
$model->server->update(['status' => null]); $model->server->update(['status' => null]);
Activity::event($request->boolean('successful') ? 'server:backup.restore-complete' : 'server.backup.restore-failed') Activity::event($request->boolean('successful') ? 'server:backup.restore-complete' : 'server.backup.restore-failed')

View File

@@ -3,18 +3,23 @@
namespace App\Http\Controllers\Api\Remote\Servers; namespace App\Http\Controllers\Api\Remote\Servers;
use App\Enums\ContainerStatus; use App\Enums\ContainerStatus;
use App\Exceptions\Http\HttpForbiddenException;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Api\Remote\ServerRequest;
use App\Models\Server; use App\Models\Server;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ServerContainersController extends Controller class ServerContainersController extends Controller
{ {
/** /**
* Updates the server container's status on the Panel * Updates the server container's status on the Panel
*/ */
public function status(ServerRequest $request, Server $server): JsonResponse public function status(Request $request, Server $server): JsonResponse
{ {
if (!$server->node->is($request->attributes->get('node'))) {
throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
}
$status = ContainerStatus::tryFrom($request->json('data.new_state')) ?? ContainerStatus::Missing; $status = ContainerStatus::tryFrom($request->json('data.new_state')) ?? ContainerStatus::Missing;
cache()->put("servers.$server->uuid.status", $status, now()->addHour()); cache()->put("servers.$server->uuid.status", $status, now()->addHour());

View File

@@ -3,9 +3,9 @@
namespace App\Http\Controllers\Api\Remote\Servers; namespace App\Http\Controllers\Api\Remote\Servers;
use App\Enums\ServerState; use App\Enums\ServerState;
use App\Exceptions\Http\HttpForbiddenException;
use App\Facades\Activity; use App\Facades\Activity;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Api\Remote\ServerRequest;
use App\Http\Resources\Daemon\ServerConfigurationCollection; use App\Http\Resources\Daemon\ServerConfigurationCollection;
use App\Models\ActivityLog; use App\Models\ActivityLog;
use App\Models\Backup; use App\Models\Backup;
@@ -17,6 +17,7 @@ use Illuminate\Database\ConnectionInterface;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Throwable; use Throwable;
use Webmozart\Assert\Assert;
class ServerDetailsController extends Controller class ServerDetailsController extends Controller
{ {
@@ -33,8 +34,21 @@ class ServerDetailsController extends Controller
* Returns details about the server that allows daemon to self-recover and ensure * Returns details about the server that allows daemon to self-recover and ensure
* that the state of the server matches the Panel at all times. * that the state of the server matches the Panel at all times.
*/ */
public function __invoke(ServerRequest $request, Server $server): JsonResponse public function __invoke(Request $request, Server $server): JsonResponse
{ {
Assert::isInstanceOf($node = $request->attributes->get('node'), Node::class);
$transfer = $server->transfer;
// If the server is being transferred allow either node to request information about
// the server. If the server is not being transferred only the target node is allowed
// to fetch these details.
$valid = $transfer ? $node->id === $transfer->old_node || $node->id === $transfer->new_node : $node->id === $server->node_id;
if (!$valid) {
throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
}
return new JsonResponse([ return new JsonResponse([
'settings' => $this->configurationStructureService->handle($server), 'settings' => $this->configurationStructureService->handle($server),
'process_configuration' => $this->eggConfigurationService->handle($server), 'process_configuration' => $this->eggConfigurationService->handle($server),

View File

@@ -4,12 +4,13 @@ namespace App\Http\Controllers\Api\Remote\Servers;
use App\Enums\ServerState; use App\Enums\ServerState;
use App\Events\Server\Installed as ServerInstalled; use App\Events\Server\Installed as ServerInstalled;
use App\Exceptions\Http\HttpForbiddenException;
use App\Exceptions\Model\DataValidationException; use App\Exceptions\Model\DataValidationException;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Api\Remote\InstallationDataRequest; use App\Http\Requests\Api\Remote\InstallationDataRequest;
use App\Http\Requests\Api\Remote\ServerRequest;
use App\Models\Server; use App\Models\Server;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
class ServerInstallController extends Controller class ServerInstallController extends Controller
@@ -17,12 +18,18 @@ class ServerInstallController extends Controller
/** /**
* Returns installation information for a server. * Returns installation information for a server.
*/ */
public function index(ServerRequest $request, Server $server): JsonResponse public function index(Request $request, Server $server): JsonResponse
{ {
if (!$server->node->is($request->attributes->get('node'))) {
throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
}
$egg = $server->egg;
return new JsonResponse([ return new JsonResponse([
'container_image' => $server->egg->copy_script_container, 'container_image' => $egg->copy_script_container,
'entrypoint' => $server->egg->copy_script_entry, 'entrypoint' => $egg->copy_script_entry,
'script' => $server->egg->copy_script_install, 'script' => $egg->copy_script_install,
]); ]);
} }
@@ -35,6 +42,10 @@ class ServerInstallController extends Controller
{ {
$status = null; $status = null;
if (!$server->node->is($request->attributes->get('node'))) {
throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
}
$successful = $request->boolean('successful'); $successful = $request->boolean('successful');
// Make sure the type of failure is accurate // Make sure the type of failure is accurate

View File

@@ -2,17 +2,20 @@
namespace App\Http\Controllers\Api\Remote\Servers; namespace App\Http\Controllers\Api\Remote\Servers;
use App\Exceptions\Http\HttpForbiddenException;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Api\Remote\ServerRequest;
use App\Models\Allocation; use App\Models\Allocation;
use App\Models\Node;
use App\Models\Server; use App\Models\Server;
use App\Repositories\Daemon\DaemonServerRepository; use App\Repositories\Daemon\DaemonServerRepository;
use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionInterface;
use Illuminate\Http\Client\ConnectionException; use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException; use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
use Throwable; use Throwable;
use Webmozart\Assert\Assert;
class ServerTransferController extends Controller class ServerTransferController extends Controller
{ {
@@ -29,13 +32,22 @@ class ServerTransferController extends Controller
* *
* @throws Throwable * @throws Throwable
*/ */
public function failure(ServerRequest $request, Server $server): JsonResponse public function failure(Request $request, Server $server): JsonResponse
{ {
$transfer = $server->transfer; $transfer = $server->transfer;
if (is_null($transfer)) { if (is_null($transfer)) {
throw new ConflictHttpException('Server is not being transferred.'); throw new ConflictHttpException('Server is not being transferred.');
} }
/* @var Node $node */
Assert::isInstanceOf($node = $request->attributes->get('node'), Node::class);
// Either node can tell the panel that the transfer has failed. Only the new node
// can tell the panel that it was successful.
if (!$node->is($transfer->newNode) && !$node->is($transfer->oldNode)) {
throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
}
$this->connection->transaction(function () use ($transfer) { $this->connection->transaction(function () use ($transfer) {
$transfer->forceFill(['successful' => false])->saveOrFail(); $transfer->forceFill(['successful' => false])->saveOrFail();
@@ -53,13 +65,22 @@ class ServerTransferController extends Controller
* *
* @throws Throwable * @throws Throwable
*/ */
public function success(ServerRequest $request, Server $server): JsonResponse public function success(Request $request, Server $server): JsonResponse
{ {
$transfer = $server->transfer; $transfer = $server->transfer;
if (is_null($transfer)) { if (is_null($transfer)) {
throw new ConflictHttpException('Server is not being transferred.'); throw new ConflictHttpException('Server is not being transferred.');
} }
/* @var Node $node */
Assert::isInstanceOf($node = $request->attributes->get('node'), Node::class);
// Only the new node communicates a successful state to the panel, so we should
// not allow the old node to hit this endpoint.
if (!$node->is($transfer->newNode)) {
throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
}
/** @var Server $server */ /** @var Server $server */
$server = $this->connection->transaction(function () use ($server, $transfer) { $server = $this->connection->transaction(function () use ($server, $transfer) {
$data = []; $data = [];

View File

@@ -23,7 +23,7 @@ class RequireTwoFactorAuthentication
* order to perform actions. If so, we check the level at which it is required (all users * order to perform actions. If so, we check the level at which it is required (all users
* or just admins) and then check if the user has enabled it for their account. * or just admins) and then check if the user has enabled it for their account.
* *
* @throws \App\Exceptions\Http\TwoFactorAuthRequiredException * @throws TwoFactorAuthRequiredException
*/ */
public function handle(Request $request, \Closure $next): mixed public function handle(Request $request, \Closure $next): mixed
{ {

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class SetSecurityHeaders
{
/**
* Ideally we move away from X-Frame-Options/X-XSS-Protection and implement a
* proper standard CSP, but I can guarantee that will break for a lot of folks
* using custom plugins and who knows what image embeds.
*
* We'll circle back to that at a later date when it can be more fully controlled
* by the admin to support those cases without too much trouble.
*
* @var array<string, string>
*/
protected static array $headers = [
'X-Frame-Options' => 'DENY',
'X-Content-Type-Options' => 'nosniff',
'X-XSS-Protection' => '1; mode=block',
'Referrer-Policy' => 'no-referrer-when-downgrade',
];
/**
* Enforces some basic security headers on all responses returned by the software.
* If a header has already been set in another location within the code it will be
* skipped over here.
*
* @param (\Closure(mixed): Response) $next
*/
public function handle(Request $request, \Closure $next): mixed
{
$response = $next($request);
foreach (static::$headers as $key => $value) {
if (!$response->headers->has($key)) {
$response->headers->set($key, $value);
}
}
return $response;
}
}

View File

@@ -87,7 +87,7 @@ abstract class ApplicationApiRequest extends FormRequest
$value = $this->route()->parameter($key); $value = $this->route()->parameter($key);
Assert::isInstanceOf($value, $expect); Assert::isInstanceOf($value, $expect);
Assert::isInstanceOf($value, Model::class); Assert::isInstanceOf($value, Model::class); // @phpstan-ignore staticMethod.alreadyNarrowedType
Assert::true($value->exists); Assert::true($value->exists);
/* @var T $value */ /* @var T $value */

View File

@@ -2,8 +2,15 @@
namespace App\Http\Requests\Api\Remote; namespace App\Http\Requests\Api\Remote;
class InstallationDataRequest extends ServerRequest use Illuminate\Foundation\Http\FormRequest;
class InstallationDataRequest extends FormRequest
{ {
public function authorize(): bool
{
return true;
}
/** /**
* @return array<string, string|string[]> * @return array<string, string|string[]>
*/ */

View File

@@ -1,29 +0,0 @@
<?php
namespace App\Http\Requests\Api\Remote;
use App\Models\Node;
use App\Models\Server;
use Illuminate\Foundation\Http\FormRequest;
class ServerRequest extends FormRequest
{
public function authorize(): bool
{
/** @var Node $node */
$node = $this->attributes->get('node');
/** @var ?Server $server */
$server = $this->route()->parameter('server');
if ($server) {
if ($server->transfer) {
return $server->transfer->old_node === $node->id || $server->transfer->new_node === $node->id;
}
return $server->node_id === $node->id;
}
return false;
}
}

View File

@@ -1,21 +0,0 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
abstract class Job
{
/*
|--------------------------------------------------------------------------
| Queueable Jobs
|--------------------------------------------------------------------------
|
| This job base class provides a central location to place any logic that
| is shared across all of your jobs. The trait included with the class
| provides access to the "onQueue" and "delay" queue helper methods.
|
*/
use Queueable;
}

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Jobs\Plugin;
use App\Filament\Admin\Resources\Plugins\Pages\ListPlugins;
use App\Models\Plugin;
use App\Models\User;
use App\Services\Helpers\PluginService;
use Exception;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class InstallPlugin implements ShouldBeUnique, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public User $user, public Plugin $plugin) {}
public function handle(PluginService $pluginService): void
{
try {
$pluginService->installPlugin($this->plugin, !$this->plugin->isTheme() || !$pluginService->hasThemePluginEnabled());
Notification::make()
->success()
->title(trans('admin/plugin.notifications.installed'))
->body($this->plugin->name)
->actions([
Action::make('goto_plugins')
->label(trans('admin/plugin.notifications.goto_plugins'))
->url(ListPlugins::getUrl(panel: 'admin')),
])
->sendToDatabase($this->user);
} catch (Exception $exception) {
report($exception);
Notification::make()
->danger()
->title(trans('admin/plugin.notifications.install_error'))
->body($exception->getMessage())
->sendToDatabase($this->user);
}
}
public function uniqueId(): string
{
return 'plugin:install:' . $this->plugin->id;
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Jobs\Plugin;
use App\Filament\Admin\Resources\Plugins\Pages\ListPlugins;
use App\Models\Plugin;
use App\Models\User;
use App\Services\Helpers\PluginService;
use Exception;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class UninstallPlugin implements ShouldBeUnique, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public User $user, public Plugin $plugin) {}
public function handle(PluginService $pluginService): void
{
try {
$pluginService->uninstallPlugin($this->plugin);
Notification::make()
->success()
->title(trans('admin/plugin.notifications.uninstalled'))
->body($this->plugin->name)
->actions([
Action::make('goto_plugins')
->label(trans('admin/plugin.notifications.goto_plugins'))
->url(ListPlugins::getUrl(panel: 'admin')),
])
->sendToDatabase($this->user);
} catch (Exception $exception) {
report($exception);
Notification::make()
->danger()
->title(trans('admin/plugin.notifications.uninstall_error'))
->body($exception->getMessage())
->sendToDatabase($this->user);
}
}
public function uniqueId(): string
{
return 'plugin:uninstall:' . $this->plugin->id;
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Jobs\Plugin;
use App\Filament\Admin\Resources\Plugins\Pages\ListPlugins;
use App\Models\Plugin;
use App\Models\User;
use App\Services\Helpers\PluginService;
use Exception;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class UpdatePlugin implements ShouldBeUnique, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public User $user, public Plugin $plugin) {}
public function handle(PluginService $pluginService): void
{
try {
$pluginService->updatePlugin($this->plugin);
Notification::make()
->success()
->title(trans('admin/plugin.notifications.updated'))
->body($this->plugin->name)
->actions([
Action::make('goto_plugins')
->label(trans('admin/plugin.notifications.goto_plugins'))
->url(ListPlugins::getUrl(panel: 'admin')),
])
->sendToDatabase($this->user);
} catch (Exception $exception) {
report($exception);
Notification::make()
->danger()
->title(trans('admin/plugin.notifications.update_error'))
->body($exception->getMessage())
->sendToDatabase($this->user);
}
}
public function uniqueId(): string
{
return 'plugin:update:' . $this->plugin->id;
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Jobs;
use App\Models\Node;
use App\Models\Server;
use App\Repositories\Daemon\DaemonServerRepository;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Queue\Attributes\DeleteWhenMissingModels;
use Illuminate\Queue\Attributes\WithoutRelations;
/**
* Revokes all SFTP access for a user on a given node or for a specific server.
*/
#[DeleteWhenMissingModels]
class RevokeSftpAccessJob implements ShouldBeUnique, ShouldQueue
{
use Queueable;
public int $tries = 3;
public int $maxExceptions = 1;
public function __construct(
public readonly string $user,
#[WithoutRelations]
public readonly Server|Node $target,
) {}
public function uniqueId(): string
{
$target = $this->target instanceof Node ? "node:{$this->target->uuid}" : "server:{$this->target->uuid}";
return "revoke-sftp:{$this->user}:{$target}";
}
public function handle(DaemonServerRepository $repository): void
{
try {
if ($this->target instanceof Server) {
$repository->setServer($this->target)->deauthorize($this->user);
} else {
$repository->setNode($this->target)->deauthorize($this->user);
}
} catch (ConnectionException) {
// Keep retrying this job with a longer and longer backoff until we hit three
// attempts at which point we stop and will assume the node is fully offline
// and we are just wasting time.
$this->release($this->attempts() * 10);
}
}
}

View File

@@ -7,6 +7,7 @@ use App\Jobs\Job;
use App\Models\Task; use App\Models\Task;
use Carbon\CarbonImmutable; use Carbon\CarbonImmutable;
use Exception; use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Http\Client\ConnectionException; use Illuminate\Http\Client\ConnectionException;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
@@ -14,9 +15,10 @@ use Illuminate\Queue\SerializesModels;
use InvalidArgumentException; use InvalidArgumentException;
use Throwable; use Throwable;
class RunTaskJob extends Job implements ShouldQueue class RunTaskJob implements ShouldQueue
{ {
use InteractsWithQueue; use InteractsWithQueue;
use Queueable;
use SerializesModels; use SerializesModels;
/** /**

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Listeners;
use App\Events\User\Deleting;
use App\Events\User\PasswordChanged;
use App\Jobs\RevokeSftpAccessJob;
use App\Models\Node;
use Illuminate\Database\Eloquent\Collection;
class RevocationListener
{
public function handle(Deleting|PasswordChanged $event): void
{
$user = $event->user;
// Look at all of the nodes that a user is associated with and trigger a job
// that disconnects them from websockets and SFTP.
Node::query()
->whereIn('nodes.id', $user->directAccessibleServers()->select('servers.node_id')->distinct())
->chunk(50, function (Collection $nodes) use ($user) {
$nodes->each(fn (Node $node) => RevokeSftpAccessJob::dispatch($user->uuid, $node));
});
}
}

View File

@@ -8,6 +8,7 @@ use App\Services\Nodes\NodeJWTService;
use App\Services\Servers\GetUserPermissionsService; use App\Services\Servers\GetUserPermissionsService;
use Filament\Support\Enums\IconSize; use Filament\Support\Enums\IconSize;
use Filament\Tables\View\Components\Columns\IconColumnComponent\IconComponent; use Filament\Tables\View\Components\Columns\IconColumnComponent\IconComponent;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\View\ComponentAttributeBag; use Illuminate\View\ComponentAttributeBag;
use Livewire\Attributes\Locked; use Livewire\Attributes\Locked;
@@ -30,7 +31,7 @@ class NodeClientConnectivity extends Component
$this->nodeJWTService = $nodeJWTService; $this->nodeJWTService = $nodeJWTService;
} }
public function render(): \Illuminate\Contracts\View\View public function render(): View
{ {
$httpUrl = $this->node->getConnectionAddress(); $httpUrl = $this->node->getConnectionAddress();

View File

@@ -6,7 +6,6 @@ use App\Enums\TablerIcon;
use App\Events\ActivityLogged; use App\Events\ActivityLogged;
use App\Traits\HasValidation; use App\Traits\HasValidation;
use BackedEnum; use BackedEnum;
use Carbon\Carbon;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Filament\Support\Contracts\HasIcon; use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel; use Filament\Support\Contracts\HasLabel;
@@ -17,6 +16,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Event;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@@ -31,28 +31,28 @@ use LogicException;
* @property string|null $description * @property string|null $description
* @property string|null $actor_type * @property string|null $actor_type
* @property int|null $actor_id * @property int|null $actor_id
* @property Collection<array-key, mixed> $properties
* @property Carbon $timestamp
* @property int|null $api_key_id * @property int|null $api_key_id
* @property Collection|null $properties * @property-read Model|\Eloquent|null $actor
* @property \Carbon\Carbon $timestamp * @property-read ApiKey|null $apiKey
* @property Model|\Eloquent $actor * @property-read \Illuminate\Database\Eloquent\Collection<int, ActivityLogSubject> $subjects
* @property \Illuminate\Database\Eloquent\Collection|ActivityLogSubject[] $subjects * @property-read int|null $subjects_count
* @property int|null $subjects_count
* @property ApiKey|null $apiKey
* *
* @method static Builder|ActivityLog forActor(Model $actor) * @method static Builder<static>|ActivityLog forActor(\Illuminate\Database\Eloquent\Model $actor)
* @method static Builder|ActivityLog forEvent(string $action) * @method static Builder<static>|ActivityLog forEvent(string $action)
* @method static Builder|ActivityLog newModelQuery() * @method static Builder<static>|ActivityLog newModelQuery()
* @method static Builder|ActivityLog newQuery() * @method static Builder<static>|ActivityLog newQuery()
* @method static Builder|ActivityLog query() * @method static Builder<static>|ActivityLog query()
* @method static Builder|ActivityLog whereActorId($value) * @method static Builder<static>|ActivityLog whereActorId($value)
* @method static Builder|ActivityLog whereActorType($value) * @method static Builder<static>|ActivityLog whereActorType($value)
* @method static Builder|ActivityLog whereApiKeyId($value) * @method static Builder<static>|ActivityLog whereApiKeyId($value)
* @method static Builder|ActivityLog whereDescription($value) * @method static Builder<static>|ActivityLog whereDescription($value)
* @method static Builder|ActivityLog whereEvent($value) * @method static Builder<static>|ActivityLog whereEvent($value)
* @method static Builder|ActivityLog whereId($value) * @method static Builder<static>|ActivityLog whereId($value)
* @method static Builder|ActivityLog whereIp($value) * @method static Builder<static>|ActivityLog whereIp($value)
* @method static Builder|ActivityLog whereProperties($value) * @method static Builder<static>|ActivityLog whereProperties($value)
* @method static Builder|ActivityLog whereTimestamp($value) * @method static Builder<static>|ActivityLog whereTimestamp($value)
*/ */
class ActivityLog extends Model implements HasIcon, HasLabel class ActivityLog extends Model implements HasIcon, HasLabel
{ {

View File

@@ -14,14 +14,18 @@ use Illuminate\Database\Eloquent\SoftDeletingScope;
* *
* @property int $id * @property int $id
* @property int $activity_log_id * @property int $activity_log_id
* @property int $subject_id
* @property string $subject_type * @property string $subject_type
* @property ActivityLog|null $activityLog * @property int $subject_id
* @property Model|\Eloquent $subject * @property-read ActivityLog $activityLog
* @property-read Model|\Eloquent $subject
* *
* @method static Builder|ActivityLogSubject newModelQuery() * @method static Builder<static>|ActivityLogSubject newModelQuery()
* @method static Builder|ActivityLogSubject newQuery() * @method static Builder<static>|ActivityLogSubject newQuery()
* @method static Builder|ActivityLogSubject query() * @method static Builder<static>|ActivityLogSubject query()
* @method static Builder<static>|ActivityLogSubject whereActivityLogId($value)
* @method static Builder<static>|ActivityLogSubject whereId($value)
* @method static Builder<static>|ActivityLogSubject whereSubjectId($value)
* @method static Builder<static>|ActivityLogSubject whereSubjectType($value)
*/ */
class ActivityLogSubject extends Pivot class ActivityLogSubject extends Pivot
{ {

View File

@@ -4,13 +4,12 @@ namespace App\Models;
use App\Exceptions\Service\Allocation\ServerUsingAllocationException; use App\Exceptions\Service\Allocation\ServerUsingAllocationException;
use App\Traits\HasValidation; use App\Traits\HasValidation;
use Carbon\Carbon;
use Database\Factories\AllocationFactory;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;
/** /**
* App\Models\Allocation. * App\Models\Allocation.
@@ -18,32 +17,33 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property int $id * @property int $id
* @property int $node_id * @property int $node_id
* @property string $ip * @property string $ip
* @property string|null $ip_alias
* @property int $port * @property int $port
* @property int|null $server_id * @property int|null $server_id
* @property string|null $notes
* @property Carbon|null $created_at * @property Carbon|null $created_at
* @property Carbon|null $updated_at * @property Carbon|null $updated_at
* @property string $alias * @property string|null $ip_alias
* @property bool $has_alias * @property string|null $notes
* @property string $address
* @property Server|null $server
* @property Node $node
* @property bool $is_locked * @property bool $is_locked
* @property-read string $address
* @property-read string $alias
* @property-read bool $has_alias
* @property-read Node $node
* @property-read Server|null $server
* *
* @method static AllocationFactory factory(...$parameters) * @method static \Database\Factories\AllocationFactory factory($count = null, $state = [])
* @method static Builder|Allocation newModelQuery() * @method static Builder<static>|Allocation newModelQuery()
* @method static Builder|Allocation newQuery() * @method static Builder<static>|Allocation newQuery()
* @method static Builder|Allocation query() * @method static Builder<static>|Allocation query()
* @method static Builder|Allocation whereCreatedAt($value) * @method static Builder<static>|Allocation whereCreatedAt($value)
* @method static Builder|Allocation whereId($value) * @method static Builder<static>|Allocation whereId($value)
* @method static Builder|Allocation whereIp($value) * @method static Builder<static>|Allocation whereIp($value)
* @method static Builder|Allocation whereIpAlias($value) * @method static Builder<static>|Allocation whereIpAlias($value)
* @method static Builder|Allocation whereNodeId($value) * @method static Builder<static>|Allocation whereIsLocked($value)
* @method static Builder|Allocation whereNotes($value) * @method static Builder<static>|Allocation whereNodeId($value)
* @method static Builder|Allocation wherePort($value) * @method static Builder<static>|Allocation whereNotes($value)
* @method static Builder|Allocation whereServerId($value) * @method static Builder<static>|Allocation wherePort($value)
* @method static Builder|Allocation whereUpdatedAt($value) * @method static Builder<static>|Allocation whereServerId($value)
* @method static Builder<static>|Allocation whereUpdatedAt($value)
*/ */
class Allocation extends Model class Allocation extends Model
{ {

View File

@@ -4,7 +4,6 @@ namespace App\Models;
use App\Services\Acl\Api\AdminAcl; use App\Services\Acl\Api\AdminAcl;
use App\Traits\HasValidation; use App\Traits\HasValidation;
use Database\Factories\ApiKeyFactory;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -17,41 +16,35 @@ use Webmozart\Assert\Assert;
* App\Models\ApiKey. * App\Models\ApiKey.
* *
* @property int $id * @property int $id
* @property int $user_id
* @property int $key_type
* @property string $identifier
* @property string $token * @property string $token
* @property string[]|null $permissions * @property string[] $allowed_ips
* @property string[]|null $allowed_ips
* @property string|null $memo
* @property Carbon|null $last_used_at
* @property Carbon|null $expires_at
* @property Carbon|null $created_at * @property Carbon|null $created_at
* @property Carbon|null $updated_at * @property Carbon|null $updated_at
* @property User $tokenable * @property int|null $user_id
* @property User $user * @property string|null $memo
* @property string|null $identifier
* @property int $key_type
* @property Carbon|null $last_used_at
* @property Carbon|null $expires_at
* @property array<string, int> $permissions
* @property-read User|null $user
* *
* @method static ApiKeyFactory factory(...$parameters) * @method static \Database\Factories\ApiKeyFactory factory($count = null, $state = [])
* @method static Builder|ApiKey newModelQuery() * @method static Builder<static>|ApiKey newModelQuery()
* @method static Builder|ApiKey newQuery() * @method static Builder<static>|ApiKey newQuery()
* @method static Builder|ApiKey query() * @method static Builder<static>|ApiKey query()
* @method static Builder|ApiKey whereAllowedIps($value) * @method static Builder<static>|ApiKey whereAllowedIps($value)
* @method static Builder|ApiKey whereCreatedAt($value) * @method static Builder<static>|ApiKey whereCreatedAt($value)
* @method static Builder|ApiKey whereId($value) * @method static Builder<static>|ApiKey whereExpiresAt($value)
* @method static Builder|ApiKey whereIdentifier($value) * @method static Builder<static>|ApiKey whereId($value)
* @method static Builder|ApiKey whereKeyType($value) * @method static Builder<static>|ApiKey whereIdentifier($value)
* @method static Builder|ApiKey whereLastUsedAt($value) * @method static Builder<static>|ApiKey whereKeyType($value)
* @method static Builder|ApiKey whereMemo($value) * @method static Builder<static>|ApiKey whereLastUsedAt($value)
* @method static Builder|ApiKey whereRAllocations($value) * @method static Builder<static>|ApiKey whereMemo($value)
* @method static Builder|ApiKey whereRDatabaseHosts($value) * @method static Builder<static>|ApiKey wherePermissions($value)
* @method static Builder|ApiKey whereREggs($value) * @method static Builder<static>|ApiKey whereToken($value)
* @method static Builder|ApiKey whereRNodes($value) * @method static Builder<static>|ApiKey whereUpdatedAt($value)
* @method static Builder|ApiKey whereRServerDatabases($value) * @method static Builder<static>|ApiKey whereUserId($value)
* @method static Builder|ApiKey whereRServers($value)
* @method static Builder|ApiKey whereRUsers($value)
* @method static Builder|ApiKey whereToken($value)
* @method static Builder|ApiKey whereUpdatedAt($value)
* @method static Builder|ApiKey whereUserId($value)
*/ */
class ApiKey extends PersonalAccessToken class ApiKey extends PersonalAccessToken
{ {

View File

@@ -18,20 +18,44 @@ use Illuminate\Database\Query\Builder;
* @property int $id * @property int $id
* @property int $server_id * @property int $server_id
* @property string $uuid * @property string $uuid
* @property bool $is_successful
* @property bool $is_locked
* @property string $name * @property string $name
* @property string[] $ignored_files * @property string[] $ignored_files
* @property string $disk * @property string $disk
* @property string|null $checksum * @property string|null $checksum
* @property int $bytes * @property int $bytes
* @property string|null $upload_id
* @property CarbonImmutable|null $completed_at * @property CarbonImmutable|null $completed_at
* @property BackupStatus $status * @property CarbonImmutable|null $created_at
* @property CarbonImmutable $created_at * @property CarbonImmutable|null $updated_at
* @property CarbonImmutable $updated_at
* @property CarbonImmutable|null $deleted_at * @property CarbonImmutable|null $deleted_at
* @property Server $server * @property bool $is_successful
* @property string|null $upload_id
* @property bool $is_locked
* @property-read Server $server
* @property-read BackupStatus $status
*
* @method static \Database\Factories\BackupFactory factory($count = null, $state = [])
* @method static BackupQueryBuilder<static>|Backup newModelQuery()
* @method static BackupQueryBuilder<static>|Backup newQuery()
* @method static BackupQueryBuilder<static>|Backup nonFailed()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Backup onlyTrashed()
* @method static BackupQueryBuilder<static>|Backup query()
* @method static BackupQueryBuilder<static>|Backup whereBytes($value)
* @method static BackupQueryBuilder<static>|Backup whereChecksum($value)
* @method static BackupQueryBuilder<static>|Backup whereCompletedAt($value)
* @method static BackupQueryBuilder<static>|Backup whereCreatedAt($value)
* @method static BackupQueryBuilder<static>|Backup whereDeletedAt($value)
* @method static BackupQueryBuilder<static>|Backup whereDisk($value)
* @method static BackupQueryBuilder<static>|Backup whereId($value)
* @method static BackupQueryBuilder<static>|Backup whereIgnoredFiles($value)
* @method static BackupQueryBuilder<static>|Backup whereIsLocked($value)
* @method static BackupQueryBuilder<static>|Backup whereIsSuccessful($value)
* @method static BackupQueryBuilder<static>|Backup whereName($value)
* @method static BackupQueryBuilder<static>|Backup whereServerId($value)
* @method static BackupQueryBuilder<static>|Backup whereUpdatedAt($value)
* @method static BackupQueryBuilder<static>|Backup whereUploadId($value)
* @method static BackupQueryBuilder<static>|Backup whereUuid($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Backup withTrashed(bool $withTrashed = true)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Backup withoutTrashed()
*/ */
class Backup extends Model implements Validatable class Backup extends Model implements Validatable
{ {

View File

@@ -4,11 +4,11 @@ namespace App\Models;
use App\Contracts\Validatable; use App\Contracts\Validatable;
use App\Traits\HasValidation; use App\Traits\HasValidation;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;
use PDOException; use PDOException;
/** /**
@@ -19,12 +19,27 @@ use PDOException;
* @property string $username * @property string $username
* @property string $remote * @property string $remote
* @property string $password * @property string $password
* @property ?int $max_connections * @property Carbon|null $created_at
* @property string $jdbc * @property Carbon|null $updated_at
* @property Carbon $created_at * @property int|null $max_connections
* @property Carbon $updated_at * @property-read DatabaseHost $host
* @property Server $server * @property-read string $jdbc
* @property DatabaseHost $host * @property-read Server $server
*
* @method static \Database\Factories\DatabaseFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder<static>|Database newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Database newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Database query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Database whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Database whereDatabase($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Database whereDatabaseHostId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Database whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Database whereMaxConnections($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Database wherePassword($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Database whereRemote($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Database whereServerId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Database whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Database whereUsername($value)
*/ */
class Database extends Model implements Validatable class Database extends Model implements Validatable
{ {

View File

@@ -21,13 +21,26 @@ use Illuminate\Support\Facades\DB;
* @property string $username * @property string $username
* @property string $password * @property string $password
* @property int|null $max_databases * @property int|null $max_databases
* @property int|null $node_id * @property CarbonImmutable|null $created_at
* @property CarbonImmutable $created_at * @property CarbonImmutable|null $updated_at
* @property CarbonImmutable $updated_at * @property-read Collection<int, Database> $databases
* @property Collection|Node[] $nodes * @property-read int|null $databases_count
* @property int|null $nodes_count * @property-read Collection<int, Node> $nodes
* @property Collection|Database[] $databases * @property-read int|null $nodes_count
* @property int|null $databases_count *
* @method static \Database\Factories\DatabaseHostFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder<static>|DatabaseHost newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|DatabaseHost newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|DatabaseHost query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|DatabaseHost whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DatabaseHost whereHost($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DatabaseHost whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DatabaseHost whereMaxDatabases($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DatabaseHost whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DatabaseHost wherePassword($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DatabaseHost wherePort($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DatabaseHost whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DatabaseHost whereUsername($value)
*/ */
class DatabaseHost extends Model implements Validatable class DatabaseHost extends Model implements Validatable
{ {

View File

@@ -5,62 +5,96 @@ namespace App\Models;
use App\Contracts\Validatable; use App\Contracts\Validatable;
use App\Exceptions\Service\Egg\HasChildrenException; use App\Exceptions\Service\Egg\HasChildrenException;
use App\Exceptions\Service\HasActiveServersException; use App\Exceptions\Service\HasActiveServersException;
use App\Models\Traits\HasIcon;
use App\Traits\HasValidation; use App\Traits\HasValidation;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphToMany; use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Carbon;
use Illuminate\Support\Str; use Illuminate\Support\Str;
/** /**
* @property int $id * @property int $id
* @property string $uuid
* @property string $author
* @property string $name * @property string $name
* @property string|null $description * @property string|null $description
* @property string|null $image * @property Carbon|null $created_at
* @property string[]|null $features * @property Carbon|null $updated_at
* @property array<string, string> $docker_images
* @property string|null $update_url
* @property bool $force_outgoing_ip
* @property string[]|null $file_denylist
* @property string|null $config_files
* @property string|null $config_startup
* @property string|null $config_logs
* @property string|null $config_stop
* @property int|null $config_from * @property int|null $config_from
* @property array<string, string> $startup_commands * @property string|null $config_stop
* @property bool $script_is_privileged * @property string|null $config_logs
* @property string|null $config_startup
* @property string|null $config_files
* @property string|null $script_install * @property string|null $script_install
* @property bool $script_is_privileged
* @property string $script_entry * @property string $script_entry
* @property string $script_container * @property string $script_container
* @property int|null $copy_script_from * @property int|null $copy_script_from
* @property Carbon $created_at * @property string|null $uuid
* @property Carbon $updated_at * @property string $author
* @property string|null $copy_script_install * @property string[]|null $features
* @property string $copy_script_entry * @property array<string, string> $docker_images
* @property string $copy_script_container * @property string|null $update_url
* @property string|null $inherit_config_files * @property string[]|null $file_denylist
* @property string|null $inherit_config_startup * @property bool $force_outgoing_ip
* @property string|null $inherit_config_logs
* @property string|null $inherit_config_stop
* @property string $inherit_file_denylist
* @property string[]|null $inherit_features
* @property string[] $tags * @property string[] $tags
* @property Collection|Server[] $servers * @property array<string, string> $startup_commands
* @property int|null $servers_count * @property-read Collection<int, Egg> $children
* @property Collection|EggVariable[] $variables * @property-read int|null $children_count
* @property int|null $variables_count * @property-read Egg|null $configFrom
* @property \App\Models\Egg|null $scriptFrom * @property-read string $copy_script_container
* @property \App\Models\Egg|null $configFrom * @property-read string $copy_script_entry
* @property-read string|null $copy_script_install
* @property-read string|null $icon
* @property-read string|null $inherit_config_files
* @property-read string|null $inherit_config_logs
* @property-read string|null $inherit_config_startup
* @property-read string|null $inherit_config_stop
* @property-read string[]|null $inherit_features
* @property-read string[]|null $inherit_file_denylist
* @property-read Collection<int, Mount> $mounts
* @property-read int|null $mounts_count
* @property-read Egg|null $scriptFrom
* @property-read Collection<int, Server> $servers
* @property-read int|null $servers_count
* @property-read Collection<int, EggVariable> $variables
* @property-read int|null $variables_count
*
* @method static \Database\Factories\EggFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereAuthor($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereConfigFiles($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereConfigFrom($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereConfigLogs($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereConfigStartup($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereConfigStop($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereCopyScriptFrom($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereDescription($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereDockerImages($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereFeatures($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereFileDenylist($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereForceOutgoingIp($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereScriptContainer($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereScriptEntry($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereScriptInstall($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereScriptIsPrivileged($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereStartupCommands($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereTags($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereUpdateUrl($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereUuid($value)
*/ */
class Egg extends Model implements Validatable class Egg extends Model implements Validatable
{ {
use HasFactory; use HasFactory;
use HasIcon;
use HasValidation; use HasValidation;
/** /**
@@ -74,22 +108,6 @@ class Egg extends Model implements Validatable
*/ */
public const EXPORT_VERSION = 'PLCN_v3'; public const EXPORT_VERSION = 'PLCN_v3';
/**
* Path to store egg icons relative to storage path.
*/
public const ICON_STORAGE_PATH = 'icons/egg';
/**
* Supported image formats: file extension => MIME type
*/
public const IMAGE_FORMATS = [
'png' => 'image/png',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'webp' => 'image/webp',
'svg' => 'image/svg+xml',
];
/** /**
* Fields that are not mass assignable. * Fields that are not mass assignable.
*/ */
@@ -344,16 +362,4 @@ class Egg extends Model implements Validatable
{ {
return str($this->name)->kebab()->lower()->trim()->split('/[^\w\-]/')->join(''); return str($this->name)->kebab()->lower()->trim()->split('/[^\w\-]/')->join('');
} }
public function getImageAttribute(): ?string
{
foreach (array_keys(static::IMAGE_FORMATS) as $ext) {
$path = static::ICON_STORAGE_PATH . "/$this->uuid.$ext";
if (Storage::disk('public')->exists($path)) {
return Storage::disk('public')->url($path);
}
}
return null;
}
} }

View File

@@ -5,6 +5,7 @@ namespace App\Models;
use App\Contracts\Validatable; use App\Contracts\Validatable;
use App\Traits\HasValidation; use App\Traits\HasValidation;
use Carbon\CarbonImmutable; use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -13,7 +14,6 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
/** /**
* @property int $id * @property int $id
* @property int $egg_id * @property int $egg_id
* @property null $sort
* @property string $name * @property string $name
* @property string $description * @property string $description
* @property string $env_variable * @property string $env_variable
@@ -21,15 +21,32 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
* @property bool $user_viewable * @property bool $user_viewable
* @property bool $user_editable * @property bool $user_editable
* @property string[] $rules * @property string[] $rules
* @property CarbonImmutable $created_at * @property CarbonImmutable|null $created_at
* @property CarbonImmutable $updated_at * @property CarbonImmutable|null $updated_at
* @property bool $required * @property int|null $sort
* @property Egg $egg * @property-read Egg|null $egg
* @property ServerVariable $serverVariable * @property-read bool $required
* @property-read Collection<int, ServerVariable> $serverVariable
* @property-read int|null $server_variable_count
* *
* The "server_value" variable is only present on the object if you've loaded this model * @method static \Database\Factories\EggVariableFactory factory($count = null, $state = [])
* using the server relationship. * @method static \Illuminate\Database\Eloquent\Builder<static>|EggVariable newModelQuery()
* @property string|null $server_value * @method static \Illuminate\Database\Eloquent\Builder<static>|EggVariable newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|EggVariable query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|EggVariable whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|EggVariable whereDefaultValue($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|EggVariable whereDescription($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|EggVariable whereEggId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|EggVariable whereEnvVariable($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|EggVariable whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|EggVariable whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|EggVariable whereRules($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|EggVariable whereSort($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|EggVariable whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|EggVariable whereUserEditable($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|EggVariable whereUserViewable($value)
*
* @property string|null $server_value This variable is only present on the object if you've loaded this model using the server relationship.
*/ */
class EggVariable extends Model implements Validatable class EggVariable extends Model implements Validatable
{ {

View File

@@ -6,12 +6,12 @@ use App\Enums\TablerIcon;
use App\Livewire\AlertBanner; use App\Livewire\AlertBanner;
use App\Repositories\Daemon\DaemonFileRepository; use App\Repositories\Daemon\DaemonFileRepository;
use BackedEnum; use BackedEnum;
use Carbon\Carbon;
use Closure; use Closure;
use Exception; use Exception;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Client\ConnectionException; use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Carbon;
use Sushi\Sushi; use Sushi\Sushi;
/** /**
@@ -28,6 +28,21 @@ use Sushi\Sushi;
* @property bool $is_file * @property bool $is_file
* @property bool $is_symlink * @property bool $is_symlink
* @property string $mime_type * @property string $mime_type
*
* @method static Builder<static>|File newModelQuery()
* @method static Builder<static>|File newQuery()
* @method static Builder<static>|File query()
* @method static Builder<static>|File whereCreatedAt($value)
* @method static Builder<static>|File whereId($value)
* @method static Builder<static>|File whereIsDirectory($value)
* @method static Builder<static>|File whereIsFile($value)
* @method static Builder<static>|File whereIsSymlink($value)
* @method static Builder<static>|File whereMimeType($value)
* @method static Builder<static>|File whereMode($value)
* @method static Builder<static>|File whereModeBits($value)
* @method static Builder<static>|File whereModifiedAt($value)
* @method static Builder<static>|File whereName($value)
* @method static Builder<static>|File whereSize($value)
*/ */
class File extends Model class File extends Model
{ {
@@ -138,6 +153,9 @@ class File extends Model
return [ return [
'created_at' => 'datetime', 'created_at' => 'datetime',
'modified_at' => 'datetime', 'modified_at' => 'datetime',
'is_directory' => 'boolean',
'is_file' => 'boolean',
'is_symlink' => 'boolean',
]; ];
} }

View File

@@ -12,14 +12,29 @@ use Illuminate\Database\Eloquent\Relations\MorphToMany;
* @property int $id * @property int $id
* @property string $uuid * @property string $uuid
* @property string $name * @property string $name
* @property string $description * @property string|null $description
* @property string $source * @property string $source
* @property string $target * @property string $target
* @property bool $read_only * @property bool $read_only
* @property bool $user_mountable * @property bool $user_mountable
* @property Egg[]|Collection $eggs * @property-read Collection<int, Egg> $eggs
* @property Node[]|Collection $nodes * @property-read int|null $eggs_count
* @property Server[]|Collection $servers * @property-read Collection<int, Node> $nodes
* @property-read int|null $nodes_count
* @property-read Collection<int, Server> $servers
* @property-read int|null $servers_count
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|Mount newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Mount newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Mount query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Mount whereDescription($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Mount whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Mount whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Mount whereReadOnly($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Mount whereSource($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Mount whereTarget($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Mount whereUserMountable($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Mount whereUuid($value)
*/ */
class Mount extends Model implements Validatable class Mount extends Model implements Validatable
{ {

View File

@@ -6,7 +6,6 @@ use App\Contracts\Validatable;
use App\Exceptions\Service\HasActiveServersException; use App\Exceptions\Service\HasActiveServersException;
use App\Repositories\Daemon\DaemonSystemRepository; use App\Repositories\Daemon\DaemonSystemRepository;
use App\Traits\HasValidation; use App\Traits\HasValidation;
use Carbon\Carbon;
use Exception; use Exception;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -15,46 +14,84 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\MorphToMany; use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Notifications\DatabaseNotificationCollection;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
/** /**
* @property int $id * @property int $id
* @property string $uuid
* @property bool $public * @property bool $public
* @property string $name * @property string $name
* @property string|null $description
* @property string $fqdn * @property string $fqdn
* @property string $scheme * @property string $scheme
* @property bool $behind_proxy
* @property bool $maintenance_mode
* @property int $memory * @property int $memory
* @property int $memory_overallocate * @property int $memory_overallocate
* @property int $disk * @property int $disk
* @property int $disk_overallocate * @property int $disk_overallocate
* @property int $cpu
* @property int $cpu_overallocate
* @property int $upload_size
* @property string $daemon_token_id
* @property string $daemon_token * @property string $daemon_token
* @property int $daemon_listen * @property int $daemon_listen
* @property int $daemon_connect
* @property int $daemon_sftp * @property int $daemon_sftp
* @property string|null $daemon_sftp_alias
* @property string $daemon_base * @property string $daemon_base
* @property string[] $tags * @property Carbon|null $created_at
* @property Carbon $created_at * @property Carbon|null $updated_at
* @property Carbon $updated_at * @property int $upload_size
* @property Mount[]|Collection $mounts * @property bool $behind_proxy
* @property int|null $mounts_count * @property string|null $description
* @property Server[]|Collection $servers * @property bool $maintenance_mode
* @property int|null $servers_count * @property string|null $uuid
* @property Allocation[]|Collection $allocations * @property string|null $daemon_token_id
* @property int|null $allocations_count * @property array<array-key, mixed>|null $tags
* @property Role[]|Collection $roles * @property int $cpu
* @property int|null $roles_count * @property int $cpu_overallocate
* @property string|null $daemon_sftp_alias
* @property int $daemon_connect
* @property-read Collection<int, Allocation> $allocations
* @property-read int|null $allocations_count
* @property-read Collection<int, DatabaseHost> $databaseHosts
* @property-read int|null $database_hosts_count
* @property-read Collection<int, Mount> $mounts
* @property-read int|null $mounts_count
* @property-read DatabaseNotificationCollection<int, DatabaseNotification> $notifications
* @property-read int|null $notifications_count
* @property-read Collection<int, Role> $roles
* @property-read int|null $roles_count
* @property-read Collection<int, Server> $servers
* @property-read int|null $servers_count
*
* @method static \Database\Factories\NodeFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereBehindProxy($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereCpu($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereCpuOverallocate($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereDaemonBase($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereDaemonConnect($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereDaemonListen($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereDaemonSftp($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereDaemonSftpAlias($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereDaemonToken($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereDaemonTokenId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereDescription($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereDisk($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereDiskOverallocate($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereFqdn($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereMaintenanceMode($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereMemory($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereMemoryOverallocate($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node wherePublic($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereScheme($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereTags($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereUploadSize($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereUuid($value)
*/ */
class Node extends Model implements Validatable class Node extends Model implements Validatable
{ {

View File

@@ -4,6 +4,16 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Relations\Pivot; use Illuminate\Database\Eloquent\Relations\Pivot;
/**
* @property int $node_id
* @property int $role_id
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|NodeRole newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|NodeRole newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|NodeRole query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|NodeRole whereNodeId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|NodeRole whereRoleId($value)
*/
class NodeRole extends Pivot class NodeRole extends Pivot
{ {
protected $table = 'node_role'; protected $table = 'node_role';

View File

@@ -34,6 +34,26 @@ use Sushi\Sushi;
* @property PluginStatus $status * @property PluginStatus $status
* @property string|null $status_message * @property string|null $status_message
* @property int $load_order * @property int $load_order
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin whereAuthor($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin whereCategory($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin whereClass($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin whereComposerPackages($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin whereDescription($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin whereLoadOrder($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin whereNamespace($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin wherePanelVersion($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin wherePanels($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin whereStatus($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin whereStatusMessage($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin whereUpdateUrl($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin whereUrl($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin whereVersion($value)
*/ */
class Plugin extends Model implements HasPluginSettings class Plugin extends Model implements HasPluginSettings
{ {

View File

@@ -9,6 +9,7 @@ use BackedEnum;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Support\Carbon;
use Spatie\Permission\Models\Permission; use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role as BaseRole; use Spatie\Permission\Models\Role as BaseRole;
@@ -16,12 +17,27 @@ use Spatie\Permission\Models\Role as BaseRole;
* @property int $id * @property int $id
* @property string $name * @property string $name
* @property string $guard_name * @property string $guard_name
* @property Collection|Permission[] $permissions * @property Carbon|null $created_at
* @property int|null $permissions_count * @property Carbon|null $updated_at
* @property Collection|User[] $users * @property-read NodeRole|null $pivot
* @property int|null $users_count * @property-read Collection<int, Node> $nodes
* @property Collection|Node[] $nodes * @property-read int|null $nodes_count
* @property int|null $nodes_count * @property-read Collection<int, Permission> $permissions
* @property-read int|null $permissions_count
* @property-read Collection<int, User> $users
* @property-read int|null $users_count
*
* @method static \Database\Factories\RoleFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder<static>|Role newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Role newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Role permission($permissions, $without = false)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Role query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Role whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Role whereGuardName($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Role whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Role whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Role whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Role withoutPermission($permissions)
*/ */
class Role extends BaseRole class Role extends BaseRole
{ {

View File

@@ -6,34 +6,55 @@ use App\Contracts\Validatable;
use App\Enums\ScheduleStatus; use App\Enums\ScheduleStatus;
use App\Helpers\Utilities; use App\Helpers\Utilities;
use App\Traits\HasValidation; use App\Traits\HasValidation;
use Carbon\Carbon;
use Exception; use Exception;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Collection; use Illuminate\Support\Carbon;
/** /**
* @property int $id * @property int $id
* @property int $server_id * @property int $server_id
* @property string $name * @property string $name
* @property string $cron_day_of_week * @property string $cron_day_of_week
* @property string $cron_month
* @property string $cron_day_of_month * @property string $cron_day_of_month
* @property string $cron_hour * @property string $cron_hour
* @property string $cron_minute * @property string $cron_minute
* @property bool $is_active * @property bool $is_active
* @property bool $is_processing * @property bool $is_processing
* @property bool $only_when_online
* @property Carbon|null $last_run_at * @property Carbon|null $last_run_at
* @property Carbon|null $next_run_at * @property Carbon|null $next_run_at
* @property ScheduleStatus $status * @property Carbon|null $created_at
* @property Carbon $created_at * @property Carbon|null $updated_at
* @property Carbon $updated_at * @property string $cron_month
* @property Server $server * @property bool $only_when_online
* @property Task[]|Collection $tasks * @property-read Server $server
* @property-read ScheduleStatus $status
* @property-read Collection<int, Task> $tasks
* @property-read int|null $tasks_count
*
* @method static \Database\Factories\ScheduleFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule whereCronDayOfMonth($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule whereCronDayOfWeek($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule whereCronHour($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule whereCronMinute($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule whereCronMonth($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule whereIsActive($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule whereIsProcessing($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule whereLastRunAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule whereNextRunAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule whereOnlyWhenOnline($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule whereServerId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule whereUpdatedAt($value)
*/ */
class Schedule extends Model implements Validatable class Schedule extends Model implements Validatable
{ {

View File

@@ -7,11 +7,12 @@ use App\Enums\ContainerStatus;
use App\Enums\ServerResourceType; use App\Enums\ServerResourceType;
use App\Enums\ServerState; use App\Enums\ServerState;
use App\Exceptions\Http\Server\ServerStateConflictException; use App\Exceptions\Http\Server\ServerStateConflictException;
use App\Models\Traits\HasIcon;
use App\Repositories\Daemon\DaemonServerRepository; use App\Repositories\Daemon\DaemonServerRepository;
use App\Services\Subusers\SubuserDeletionService; use App\Services\Subusers\SubuserDeletionService;
use App\Traits\HasValidation; use App\Traits\HasValidation;
use Carbon\CarbonInterface; use Carbon\CarbonInterface;
use Database\Factories\ServerFactory; use Exception;
use Filament\Models\Contracts\HasAvatar; use Filament\Models\Contracts\HasAvatar;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
@@ -30,111 +31,106 @@ use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
/** /**
* \App\Models\Server. * \App\Models\Server.
* *
* @property int $id * @property int $id
* @property string|null $external_id
* @property string $uuid * @property string $uuid
* @property string $uuid_short * @property string $uuid_short
* @property int $node_id * @property int $node_id
* @property string $name * @property string $name
* @property string $description
* @property ServerState|null $status
* @property bool $skip_scripts
* @property int $owner_id * @property int $owner_id
* @property int $memory * @property int $memory
* @property int $swap * @property int $swap
* @property int $disk * @property int $disk
* @property int $io * @property int $io
* @property int $cpu * @property int $cpu
* @property string|null $threads
* @property bool $oom_killer
* @property int|null $allocation_id
* @property int $egg_id * @property int $egg_id
* @property string $startup * @property string $startup
* @property string $image
* @property string|null $icon
* @property int|null $allocation_limit
* @property int|null $database_limit
* @property int|null $backup_limit
* @property Carbon|null $created_at * @property Carbon|null $created_at
* @property Carbon|null $updated_at * @property Carbon|null $updated_at
* @property int|null $allocation_id
* @property string $image
* @property string|null $description
* @property bool $skip_scripts
* @property string|null $external_id
* @property int|null $database_limit
* @property int|null $allocation_limit
* @property string|null $threads
* @property int $backup_limit
* @property ServerState|null $status
* @property Carbon|null $installed_at * @property Carbon|null $installed_at
* @property Collection|ActivityLog[] $activity * @property bool $oom_killer
* @property int|null $activity_count * @property array<array-key, mixed>|null $docker_labels
* @property Allocation|null $allocation * @property-read Collection<int, ActivityLog> $activity
* @property Collection|Allocation[] $allocations * @property-read int|null $activity_count
* @property int|null $allocations_count * @property-read Allocation|null $allocation
* @property Collection|Backup[] $backups * @property-read Collection<int, Allocation> $allocations
* @property int|null $backups_count * @property-read int|null $allocations_count
* @property Collection|Database[] $databases * @property-read Collection<int, Backup> $backups
* @property int|null $databases_count * @property-read int|null $backups_count
* @property Egg $egg * @property-read ServerState|ContainerStatus $condition
* @property Collection|Mount[] $mounts * @property-read Collection<int, Database> $databases
* @property int|null $mounts_count * @property-read int|null $databases_count
* @property Node $node * @property-read Egg $egg
* @property DatabaseNotificationCollection|DatabaseNotification[] $notifications
* @property int|null $notifications_count
* @property Collection|Schedule[] $schedules
* @property int|null $schedules_count
* @property Collection|Subuser[] $subusers
* @property int|null $subusers_count
* @property ServerTransfer|null $transfer
* @property User $user
* @property Collection|EggVariable[] $variables
* @property int|null $variables_count
*
* @method static ServerFactory factory(...$parameters)
* @method static Builder|Server newModelQuery()
* @method static Builder|Server newQuery()
* @method static Builder|Server query()
* @method static Builder|Server whereAllocationId($value)
* @method static Builder|Server whereAllocationLimit($value)
* @method static Builder|Server whereBackupLimit($value)
* @method static Builder|Server whereCpu($value)
* @method static Builder|Server whereCreatedAt($value)
* @method static Builder|Server whereDatabaseLimit($value)
* @method static Builder|Server whereDescription($value)
* @method static Builder|Server whereDisk($value)
* @method static Builder|Server whereEggId($value)
* @method static Builder|Server whereExternalId($value)
* @method static Builder|Server whereId($value)
* @method static Builder|Server whereImage($value)
* @method static Builder|Server whereIo($value)
* @method static Builder|Server whereMemory($value)
* @method static Builder|Server whereName($value)
* @method static Builder|Server whereNodeId($value)
* @method static Builder|Server whereOomKiller($value)
* @method static Builder|Server whereOwnerId($value)
* @method static Builder|Server whereSkipScripts($value)
* @method static Builder|Server whereStartup($value)
* @method static Builder|Server whereStatus($value)
* @method static Builder|Server whereSwap($value)
* @method static Builder|Server whereThreads($value)
* @method static Builder|Server whereUpdatedAt($value)
* @method static Builder|Server whereUuid($value)
* @method static Builder|Server whereuuid_short($value)
*
* @property string[]|null $docker_labels
* @property string|null $ports
* @property-read ContainerStatus|ServerState $condition
* @property-read Collection<int, EggVariable> $eggVariables * @property-read Collection<int, EggVariable> $eggVariables
* @property-read int|null $egg_variables_count * @property-read int|null $egg_variables_count
* @property-read string|null $icon
* @property-read Collection<int, Mount> $mounts
* @property-read int|null $mounts_count
* @property-read Node $node
* @property-read DatabaseNotificationCollection<int, DatabaseNotification> $notifications
* @property-read int|null $notifications_count
* @property-read Collection<int, Schedule> $schedules
* @property-read int|null $schedules_count
* @property-read Collection<int, ServerVariable> $serverVariables * @property-read Collection<int, ServerVariable> $serverVariables
* @property-read int|null $server_variables_count * @property-read int|null $server_variables_count
* @property-read Collection<int, Subuser> $subusers
* @property-read int|null $subusers_count
* @property-read ServerTransfer|null $transfer
* @property-read User $user
* @property-read Collection<int, EggVariable> $variables
* @property-read int|null $variables_count
* *
* @method static Builder|Server whereDockerLabels($value) * @method static \Database\Factories\ServerFactory factory($count = null, $state = [])
* @method static Builder|Server whereInstalledAt($value) * @method static Builder<static>|Server newModelQuery()
* @method static Builder|Server wherePorts($value) * @method static Builder<static>|Server newQuery()
* @method static Builder|Server whereUuidShort($value) * @method static Builder<static>|Server query()
* @method static Builder<static>|Server whereAllocationId($value)
* @method static Builder<static>|Server whereAllocationLimit($value)
* @method static Builder<static>|Server whereBackupLimit($value)
* @method static Builder<static>|Server whereCpu($value)
* @method static Builder<static>|Server whereCreatedAt($value)
* @method static Builder<static>|Server whereDatabaseLimit($value)
* @method static Builder<static>|Server whereDescription($value)
* @method static Builder<static>|Server whereDisk($value)
* @method static Builder<static>|Server whereDockerLabels($value)
* @method static Builder<static>|Server whereEggId($value)
* @method static Builder<static>|Server whereExternalId($value)
* @method static Builder<static>|Server whereId($value)
* @method static Builder<static>|Server whereImage($value)
* @method static Builder<static>|Server whereInstalledAt($value)
* @method static Builder<static>|Server whereIo($value)
* @method static Builder<static>|Server whereMemory($value)
* @method static Builder<static>|Server whereName($value)
* @method static Builder<static>|Server whereNodeId($value)
* @method static Builder<static>|Server whereOomKiller($value)
* @method static Builder<static>|Server whereOwnerId($value)
* @method static Builder<static>|Server whereSkipScripts($value)
* @method static Builder<static>|Server whereStartup($value)
* @method static Builder<static>|Server whereStatus($value)
* @method static Builder<static>|Server whereSwap($value)
* @method static Builder<static>|Server whereThreads($value)
* @method static Builder<static>|Server whereUpdatedAt($value)
* @method static Builder<static>|Server whereUuid($value)
* @method static Builder<static>|Server whereUuidShort($value)
*/ */
class Server extends Model implements HasAvatar, Validatable class Server extends Model implements HasAvatar, Validatable
{ {
use HasFactory; use HasFactory;
use HasIcon;
use HasValidation; use HasValidation;
use Notifiable; use Notifiable;
@@ -144,22 +140,6 @@ class Server extends Model implements HasAvatar, Validatable
*/ */
public const RESOURCE_NAME = 'server'; public const RESOURCE_NAME = 'server';
/**
* Path to store server icons relative to storage path.
*/
public const ICON_STORAGE_PATH = 'icons/server';
/**
* Supported image formats: file extension => MIME type
*/
public const IMAGE_FORMATS = [
'png' => 'image/png',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'webp' => 'image/webp',
'svg' => 'image/svg+xml',
];
/** /**
* Default values when creating the model. We want to switch to disabling OOM killer * Default values when creating the model. We want to switch to disabling OOM killer
* on server instances unless the user specifies otherwise in the request. * on server instances unless the user specifies otherwise in the request.
@@ -533,20 +513,8 @@ class Server extends Model implements HasAvatar, Validatable
); );
} }
public function getIconAttribute(): ?string
{
foreach (array_keys(static::IMAGE_FORMATS) as $ext) {
$path = static::ICON_STORAGE_PATH . "/$this->uuid.$ext";
if (Storage::disk('public')->exists($path)) {
return Storage::disk('public')->url($path);
}
}
return null;
}
public function getFilamentAvatarUrl(): ?string public function getFilamentAvatarUrl(): ?string
{ {
return $this->icon ?? $this->egg->image; return $this->icon ?? $this->egg->icon;
} }
} }

View File

@@ -4,30 +4,48 @@ namespace App\Models;
use App\Contracts\Validatable; use App\Contracts\Validatable;
use App\Traits\HasValidation; use App\Traits\HasValidation;
use Carbon\Carbon; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Support\Carbon;
/** /**
* @property int $id * @property int $id
* @property int $server_id * @property int $server_id
* @property bool|null $successful
* @property int $old_node * @property int $old_node
* @property int $new_node * @property int $new_node
* @property int|null $old_allocation * @property int|null $old_allocation
* @property int|null $new_allocation * @property int|null $new_allocation
* @property array<int>|null $old_additional_allocations array of allocation.id's * @property int[]|null $old_additional_allocations
* @property array<int>|null $new_additional_allocations array of allocation.id's * @property int[]|null $new_additional_allocations
* @property bool|null $successful * @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property bool $archived * @property bool $archived
* @property Carbon $created_at * @property-read Node|null $newNode
* @property Carbon $updated_at * @property-read Node|null $oldNode
* @property Server $server * @property-read Server $server
* @property Node $oldNode *
* @property Node $newNode * @method static \Illuminate\Database\Eloquent\Builder<static>|ServerTransfer newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerTransfer newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerTransfer query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerTransfer whereArchived($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerTransfer whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerTransfer whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerTransfer whereNewAdditionalAllocations($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerTransfer whereNewAllocation($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerTransfer whereNewNode($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerTransfer whereOldAdditionalAllocations($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerTransfer whereOldAllocation($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerTransfer whereOldNode($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerTransfer whereServerId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerTransfer whereSuccessful($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerTransfer whereUpdatedAt($value)
*/ */
class ServerTransfer extends Model implements Validatable class ServerTransfer extends Model implements Validatable
{ {
use HasFactory;
use HasValidation; use HasValidation;
/** /**

View File

@@ -15,8 +15,18 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property string $variable_value * @property string $variable_value
* @property CarbonImmutable|null $created_at * @property CarbonImmutable|null $created_at
* @property CarbonImmutable|null $updated_at * @property CarbonImmutable|null $updated_at
* @property EggVariable $variable * @property-read Server $server
* @property Server $server * @property-read EggVariable $variable
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerVariable newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerVariable newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerVariable query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerVariable whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerVariable whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerVariable whereServerId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerVariable whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerVariable whereVariableId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerVariable whereVariableValue($value)
*/ */
class ServerVariable extends Model implements Validatable class ServerVariable extends Model implements Validatable
{ {

View File

@@ -5,21 +5,33 @@ namespace App\Models;
use App\Contracts\Validatable; use App\Contracts\Validatable;
use App\Enums\SubuserPermission; use App\Enums\SubuserPermission;
use App\Traits\HasValidation; use App\Traits\HasValidation;
use Carbon\Carbon; use BackedEnum;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Carbon;
/** /**
* @property int $id * @property int $id
* @property int $user_id * @property int $user_id
* @property int $server_id * @property int $server_id
* @property string[] $permissions * @property Carbon|null $created_at
* @property Carbon $created_at * @property Carbon|null $updated_at
* @property Carbon $updated_at * @property string[]|null $permissions
* @property User $user * @property-read Server $server
* @property Server $server * @property-read User $user
*
* @method static \Database\Factories\SubuserFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder<static>|Subuser newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Subuser newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Subuser query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Subuser whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Subuser whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Subuser wherePermissions($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Subuser whereServerId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Subuser whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Subuser whereUserId($value)
*/ */
class Subuser extends Model implements Validatable class Subuser extends Model implements Validatable
{ {
@@ -33,17 +45,21 @@ class Subuser extends Model implements Validatable
*/ */
public const RESOURCE_NAME = 'server_subuser'; public const RESOURCE_NAME = 'server_subuser';
/** @var array<string, array{name: string, hidden: ?bool, icon: ?string, permissions: string[]}> */ /** @var array<string, array{name: string, hidden: ?bool, icon: null|string|BackedEnum, translation_prefix: ?string, permissions: string[]}> */
protected static array $customPermissions = []; protected static array $customPermissions = [];
/** @param string[] $permissions */ /** @param string[] $permissions */
public static function registerCustomPermissions(string $name, array $permissions, ?string $icon = null, ?bool $hidden = null): void public static function registerCustomPermissions(string $name, array $permissions, ?string $translationPrefix = null, null|string|BackedEnum $icon = null, ?bool $hidden = null): void
{ {
$customPermission = static::$customPermissions[$name] ?? []; $customPermission = static::$customPermissions[$name] ?? [];
$customPermission['name'] = $name; $customPermission['name'] = $name;
$customPermission['permissions'] = array_merge($customPermission['permissions'] ?? [], $permissions); $customPermission['permissions'] = array_merge($customPermission['permissions'] ?? [], $permissions);
if (!is_null($translationPrefix)) {
$customPermission['translation_prefix'] = $translationPrefix;
}
if (!is_null($icon)) { if (!is_null($icon)) {
$customPermission['icon'] = $icon; $customPermission['icon'] = $icon;
} }
@@ -93,7 +109,7 @@ class Subuser extends Model implements Validatable
return $this->belongsTo(User::class); return $this->belongsTo(User::class);
} }
/** @return array<array{name: string, hidden: bool, icon: string, permissions: string[]}> */ /** @return array<array{name: string, hidden: bool, icon: null|string|BackedEnum, translation_prefix: string, permissions: string[]}> */
public static function allPermissionData(): array public static function allPermissionData(): array
{ {
$allPermissions = []; $allPermissions = [];
@@ -106,6 +122,7 @@ class Subuser extends Model implements Validatable
'hidden' => $subuserPermission->isHidden(), 'hidden' => $subuserPermission->isHidden(),
'icon' => $subuserPermission->getIcon(), 'icon' => $subuserPermission->getIcon(),
'permissions' => array_merge($allPermissions[$group]['permissions'] ?? [], [$permission]), 'permissions' => array_merge($allPermissions[$group]['permissions'] ?? [], [$permission]),
'translation_prefix' => 'server/user.permissions',
]; ];
} }
@@ -119,6 +136,7 @@ class Subuser extends Model implements Validatable
'hidden' => $customPermission['hidden'] ?? $groupData['hidden'] ?? false, 'hidden' => $customPermission['hidden'] ?? $groupData['hidden'] ?? false,
'icon' => $customPermission['icon'] ?? $groupData['icon'], 'icon' => $customPermission['icon'] ?? $groupData['icon'],
'permissions' => array_unique(array_merge($groupData['permissions'] ?? [], $customPermission['permissions'])), 'permissions' => array_unique(array_merge($groupData['permissions'] ?? [], $customPermission['permissions'])),
'translation_prefix' => $customPermission['translation_prefix'] ?? $groupData['translation_prefix'] ?? 'server/user.permissions',
]; ];
$allPermissions[$name] = $groupData; $allPermissions[$name] = $groupData;

View File

@@ -6,11 +6,11 @@ use App\Contracts\Validatable;
use App\Extensions\Tasks\TaskSchemaInterface; use App\Extensions\Tasks\TaskSchemaInterface;
use App\Extensions\Tasks\TaskService; use App\Extensions\Tasks\TaskService;
use App\Traits\HasValidation; use App\Traits\HasValidation;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOneThrough; use Illuminate\Database\Eloquent\Relations\HasOneThrough;
use Illuminate\Support\Carbon;
/** /**
* @property int $id * @property int $id
@@ -20,11 +20,26 @@ use Illuminate\Database\Eloquent\Relations\HasOneThrough;
* @property string $payload * @property string $payload
* @property int $time_offset * @property int $time_offset
* @property bool $is_queued * @property bool $is_queued
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property bool $continue_on_failure * @property bool $continue_on_failure
* @property Carbon $created_at * @property-read Schedule $schedule
* @property Carbon $updated_at * @property-read Server|null $server
* @property Schedule $schedule *
* @property Server $server * @method static \Database\Factories\TaskFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder<static>|Task newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Task newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Task query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Task whereAction($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Task whereContinueOnFailure($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Task whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Task whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Task whereIsQueued($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Task wherePayload($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Task whereScheduleId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Task whereSequenceId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Task whereTimeOffset($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Task whereUpdatedAt($value)
*/ */
class Task extends Model implements Validatable class Task extends Model implements Validatable
{ {

View File

@@ -4,13 +4,14 @@ namespace App\Models\Traits;
use App\Extensions\Laravel\Sanctum\NewAccessToken; use App\Extensions\Laravel\Sanctum\NewAccessToken;
use App\Models\ApiKey; use App\Models\ApiKey;
use App\Models\Model;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Laravel\Sanctum\HasApiTokens; use Laravel\Sanctum\HasApiTokens;
use Laravel\Sanctum\Sanctum; use Laravel\Sanctum\Sanctum;
/** /**
* @mixin \App\Models\Model * @mixin Model
*/ */
trait HasAccessTokens trait HasAccessTokens
{ {

View File

@@ -0,0 +1,74 @@
<?php
namespace App\Models\Traits;
use App\Models\Model;
use Exception;
use Illuminate\Support\Facades\Storage;
/**
* @mixin Model
*/
trait HasIcon
{
/**
* Supported icon formats: file extension => MIME type
*
* @var array<string, string>
*/
public static array $iconFormats = [
'png' => 'image/png',
'jpg' => 'image/jpeg',
'webp' => 'image/webp',
];
public static function getIconStoragePath(): string
{
return 'icons/' . static::RESOURCE_NAME;
}
public function getIconAttribute(): ?string
{
foreach (array_keys(static::$iconFormats) as $ext) {
$path = $this->getIconStoragePath() . "/$this->uuid.$ext";
if (Storage::disk('public')->exists($path)) {
return Storage::disk('public')->url($path);
}
}
return null;
}
public function writeIcon(string $extension, string $data): string
{
$normalizedExtension = match (strtolower($extension)) {
'jpeg', 'jpg' => 'jpg',
'png' => 'png',
'webp' => 'webp',
default => null,
};
if (is_null($normalizedExtension)) {
throw new Exception(trans('admin/egg.import.unknown_extension', ['extension' => $extension]));
}
$fileName = static::getIconStoragePath() . "/$this->uuid.$normalizedExtension";
if (!Storage::disk('public')->put($fileName, $data)) {
throw new Exception(trans('admin/egg.import.could_not_write'));
}
foreach (['png', 'jpg', 'jpeg', 'webp', 'svg'] as $ext) {
if ($ext === $normalizedExtension) {
continue;
}
$path = static::getIconStoragePath() . "/$this->uuid.$ext";
if (Storage::disk('public')->exists($path)) {
Storage::disk('public')->delete($path);
}
}
return $fileName;
}
}

View File

@@ -5,12 +5,12 @@ namespace App\Models;
use App\Contracts\Validatable; use App\Contracts\Validatable;
use App\Enums\CustomizationKey; use App\Enums\CustomizationKey;
use App\Enums\SubuserPermission; use App\Enums\SubuserPermission;
use App\Events\User\Deleting;
use App\Exceptions\DisplayException; use App\Exceptions\DisplayException;
use App\Extensions\Avatar\AvatarService; use App\Extensions\Avatar\AvatarService;
use App\Models\Traits\HasAccessTokens; use App\Models\Traits\HasAccessTokens;
use App\Traits\HasValidation; use App\Traits\HasValidation;
use BackedEnum; use BackedEnum;
use Database\Factories\UserFactory;
use DateTimeZone; use DateTimeZone;
use Filament\Auth\MultiFactor\App\Contracts\HasAppAuthentication; use Filament\Auth\MultiFactor\App\Contracts\HasAppAuthentication;
use Filament\Auth\MultiFactor\App\Contracts\HasAppAuthenticationRecovery; use Filament\Auth\MultiFactor\App\Contracts\HasAppAuthenticationRecovery;
@@ -43,56 +43,75 @@ use Illuminate\Support\Facades\Context;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Validation\Rules\In; use Illuminate\Validation\Rules\In;
use ResourceBundle; use ResourceBundle;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Traits\HasRoles; use Spatie\Permission\Traits\HasRoles;
/** /**
* App\Models\User. * App\Models\User.
* *
* @property int $id * @property int $id
* @property string|null $external_id
* @property bool $is_managed_externally
* @property string $uuid * @property string $uuid
* @property string $username
* @property string $email * @property string $email
* @property string $password * @property string $password
* @property string|null $remember_token * @property string|null $remember_token
* @property string $language * @property string $language
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property string $username
* @property string|null $external_id
* @property string $timezone * @property string $timezone
* @property string[]|null $oauth * @property array<string, mixed>|null $oauth
* @property string|array<string, mixed>|null $customization
* @property string|null $mfa_app_secret * @property string|null $mfa_app_secret
* @property string[]|null $mfa_app_recovery_codes * @property string[]|null $mfa_app_recovery_codes
* @property bool $mfa_email_enabled * @property bool $mfa_email_enabled
* @property Carbon|null $created_at * @property bool $is_managed_externally
* @property Carbon|null $updated_at * @property-read \Illuminate\Database\Eloquent\Collection<int, ActivityLog> $activity
* @property \Illuminate\Database\Eloquent\Collection|ApiKey[] $apiKeys * @property-read int|null $activity_count
* @property int|null $api_keys_count * @property-read \Illuminate\Database\Eloquent\Collection<int, ApiKey> $apiKeys
* @property DatabaseNotificationCollection|DatabaseNotification[] $notifications * @property-read int|null $api_keys_count
* @property int|null $notifications_count * @property-read DatabaseNotificationCollection<int, DatabaseNotification> $notifications
* @property \Illuminate\Database\Eloquent\Collection|Server[] $servers * @property-read int|null $notifications_count
* @property int|null $servers_count * @property-read \Illuminate\Database\Eloquent\Collection<int, Permission> $permissions
* @property \Illuminate\Database\Eloquent\Collection|UserSSHKey[] $sshKeys * @property-read int|null $permissions_count
* @property int|null $ssh_keys_count * @property-read \Illuminate\Database\Eloquent\Collection<int, Role> $roles
* @property \Illuminate\Database\Eloquent\Collection|ApiKey[] $tokens * @property-read int|null $roles_count
* @property int|null $tokens_count * @property-read \Illuminate\Database\Eloquent\Collection<int, Server> $servers
* @property \Illuminate\Database\Eloquent\Collection|Role[] $roles * @property-read int|null $servers_count
* @property int|null $roles_count * @property-read \Illuminate\Database\Eloquent\Collection<int, UserSSHKey> $sshKeys
* @property string|array<string, mixed>|null $customization * @property-read int|null $ssh_keys_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Server> $subServers
* @property-read int|null $sub_servers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Subuser> $subusers
* @property-read int|null $subusers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ApiKey> $tokens
* @property-read int|null $tokens_count
* *
* @method static UserFactory factory(...$parameters) * @method static \Database\Factories\UserFactory factory($count = null, $state = [])
* @method static Builder|User newModelQuery() * @method static Builder<static>|User newModelQuery()
* @method static Builder|User newQuery() * @method static Builder<static>|User newQuery()
* @method static Builder|User query() * @method static Builder<static>|User permission($permissions, $without = false)
* @method static Builder|User whereCreatedAt($value) * @method static Builder<static>|User query()
* @method static Builder|User whereEmail($value) * @method static Builder<static>|User role($roles, $guard = null, $without = false)
* @method static Builder|User whereExternalId($value) * @method static Builder<static>|User whereCreatedAt($value)
* @method static Builder|User whereId($value) * @method static Builder<static>|User whereCustomization($value)
* @method static Builder|User whereLanguage($value) * @method static Builder<static>|User whereEmail($value)
* @method static Builder|User whereTimezone($value) * @method static Builder<static>|User whereExternalId($value)
* @method static Builder|User wherePassword($value) * @method static Builder<static>|User whereId($value)
* @method static Builder|User whereRememberToken($value) * @method static Builder<static>|User whereIsManagedExternally($value)
* @method static Builder|User whereUpdatedAt($value) * @method static Builder<static>|User whereLanguage($value)
* @method static Builder|User whereUsername($value) * @method static Builder<static>|User whereMfaAppRecoveryCodes($value)
* @method static Builder|User whereUuid($value) * @method static Builder<static>|User whereMfaAppSecret($value)
* @method static Builder<static>|User whereMfaEmailEnabled($value)
* @method static Builder<static>|User whereOauth($value)
* @method static Builder<static>|User wherePassword($value)
* @method static Builder<static>|User whereRememberToken($value)
* @method static Builder<static>|User whereTimezone($value)
* @method static Builder<static>|User whereUpdatedAt($value)
* @method static Builder<static>|User whereUsername($value)
* @method static Builder<static>|User whereUuid($value)
* @method static Builder<static>|User withoutPermission($permissions)
* @method static Builder<static>|User withoutRole($roles, $guard = null)
*/ */
class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract, FilamentUser, HasAppAuthentication, HasAppAuthenticationRecovery, HasAvatar, HasEmailAuthentication, HasName, HasTenants, Validatable class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract, FilamentUser, HasAppAuthentication, HasAppAuthenticationRecovery, HasAvatar, HasEmailAuthentication, HasName, HasTenants, Validatable
{ {
@@ -183,6 +202,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
'is_managed_externally' => 'boolean', 'is_managed_externally' => 'boolean',
'mfa_app_secret' => 'encrypted', 'mfa_app_secret' => 'encrypted',
'mfa_app_recovery_codes' => 'encrypted:array', 'mfa_app_recovery_codes' => 'encrypted:array',
'mfa_email_enabled' => 'boolean',
'oauth' => 'array', 'oauth' => 'array',
'customization' => 'array', 'customization' => 'array',
]; ];
@@ -206,6 +226,8 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
throw_if($user->servers()->count() > 0, new DisplayException(trans('exceptions.users.has_servers'))); throw_if($user->servers()->count() > 0, new DisplayException(trans('exceptions.users.has_servers')));
throw_if(request()->user()?->id === $user->id, new DisplayException(trans('exceptions.users.is_self'))); throw_if(request()->user()?->id === $user->id, new DisplayException(trans('exceptions.users.is_self')));
event(new Deleting($user));
}); });
} }

View File

@@ -3,7 +3,6 @@
namespace App\Models; namespace App\Models;
use App\Traits\HasValidation; use App\Traits\HasValidation;
use Database\Factories\UserSSHKeyFactory;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@@ -22,23 +21,23 @@ use Illuminate\Support\Carbon;
* @property Carbon|null $created_at * @property Carbon|null $created_at
* @property Carbon|null $updated_at * @property Carbon|null $updated_at
* @property Carbon|null $deleted_at * @property Carbon|null $deleted_at
* @property User $user * @property-read User $user
* *
* @method static Builder|UserSSHKey newModelQuery() * @method static \Database\Factories\UserSSHKeyFactory factory($count = null, $state = [])
* @method static Builder|UserSSHKey newQuery() * @method static Builder<static>|UserSSHKey newModelQuery()
* @method static \Illuminate\Database\Query\Builder|UserSSHKey onlyTrashed() * @method static Builder<static>|UserSSHKey newQuery()
* @method static Builder|UserSSHKey query() * @method static Builder<static>|UserSSHKey onlyTrashed()
* @method static Builder|UserSSHKey whereCreatedAt($value) * @method static Builder<static>|UserSSHKey query()
* @method static Builder|UserSSHKey whereDeletedAt($value) * @method static Builder<static>|UserSSHKey whereCreatedAt($value)
* @method static Builder|UserSSHKey whereFingerprint($value) * @method static Builder<static>|UserSSHKey whereDeletedAt($value)
* @method static Builder|UserSSHKey whereId($value) * @method static Builder<static>|UserSSHKey whereFingerprint($value)
* @method static Builder|UserSSHKey whereName($value) * @method static Builder<static>|UserSSHKey whereId($value)
* @method static Builder|UserSSHKey wherePublicKey($value) * @method static Builder<static>|UserSSHKey whereName($value)
* @method static Builder|UserSSHKey whereUpdatedAt($value) * @method static Builder<static>|UserSSHKey wherePublicKey($value)
* @method static Builder|UserSSHKey whereUserId($value) * @method static Builder<static>|UserSSHKey whereUpdatedAt($value)
* @method static \Illuminate\Database\Query\Builder|UserSSHKey withTrashed() * @method static Builder<static>|UserSSHKey whereUserId($value)
* @method static \Illuminate\Database\Query\Builder|UserSSHKey withoutTrashed() * @method static Builder<static>|UserSSHKey withTrashed(bool $withTrashed = true)
* @method static UserSSHKeyFactory factory(...$parameters) * @method static Builder<static>|UserSSHKey withoutTrashed()
*/ */
class UserSSHKey extends Model class UserSSHKey extends Model
{ {

View File

@@ -2,19 +2,33 @@
namespace App\Models; namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\MassPrunable; use Illuminate\Database\Eloquent\MassPrunable;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
/** /**
* @property int $id
* @property int $webhook_configuration_id
* @property string $event * @property string $event
* @property string $endpoint * @property string $endpoint
* @property \Illuminate\Support\Carbon|null $successful_at * @property Carbon|null $successful_at
* @property array<array-key, mixed> $payload * @property array<array-key, mixed> $payload
* @property \Illuminate\Support\Carbon|null $created_at * @property Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at * @property Carbon|null $updated_at
*
* @method static Builder<static>|Webhook newModelQuery()
* @method static Builder<static>|Webhook newQuery()
* @method static Builder<static>|Webhook query()
* @method static Builder<static>|Webhook whereCreatedAt($value)
* @method static Builder<static>|Webhook whereEndpoint($value)
* @method static Builder<static>|Webhook whereEvent($value)
* @method static Builder<static>|Webhook whereId($value)
* @method static Builder<static>|Webhook wherePayload($value)
* @method static Builder<static>|Webhook whereSuccessfulAt($value)
* @method static Builder<static>|Webhook whereUpdatedAt($value)
* @method static Builder<static>|Webhook whereWebhookConfigurationId($value)
*/ */
class Webhook extends Model class Webhook extends Model
{ {

View File

@@ -14,15 +14,36 @@ use Illuminate\Support\Facades\File;
use Livewire\Features\SupportEvents\HandlesEvents; use Livewire\Features\SupportEvents\HandlesEvents;
/** /**
* @property string|array<string, mixed>|null $payload * @property int $id
* @property string $endpoint * @property string $endpoint
* @property string $description * @property string $description
* @property string[] $events * @property string[] $events
* @property WebhookType|string|null $type
* @property Carbon|null $created_at * @property Carbon|null $created_at
* @property Carbon|null $updated_at * @property Carbon|null $updated_at
* @property Carbon|null $deleted_at * @property Carbon|null $deleted_at
* @property array<string, string>|null $headers * @property WebhookType|null $type
* @property string|array<array-key, mixed>|null $payload
* @property array<array-key, mixed>|null $headers
* @property-read \Illuminate\Database\Eloquent\Collection<int, Webhook> $webhooks
* @property-read int|null $webhooks_count
*
* @method static \Database\Factories\WebhookConfigurationFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder<static>|WebhookConfiguration newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|WebhookConfiguration newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|WebhookConfiguration onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder<static>|WebhookConfiguration query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|WebhookConfiguration whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|WebhookConfiguration whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|WebhookConfiguration whereDescription($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|WebhookConfiguration whereEndpoint($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|WebhookConfiguration whereEvents($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|WebhookConfiguration whereHeaders($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|WebhookConfiguration whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|WebhookConfiguration wherePayload($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|WebhookConfiguration whereType($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|WebhookConfiguration whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|WebhookConfiguration withTrashed(bool $withTrashed = true)
* @method static \Illuminate\Database\Eloquent\Builder<static>|WebhookConfiguration withoutTrashed()
*/ */
class WebhookConfiguration extends Model class WebhookConfiguration extends Model
{ {

View File

@@ -10,6 +10,7 @@ use App\Checks\NodeVersionsCheck;
use App\Checks\PanelVersionCheck; use App\Checks\PanelVersionCheck;
use App\Checks\ScheduleCheck; use App\Checks\ScheduleCheck;
use App\Checks\UsedDiskSpaceCheck; use App\Checks\UsedDiskSpaceCheck;
use App\Http\Responses\LoginResponse;
use App\Models\Allocation; use App\Models\Allocation;
use App\Models\ApiKey; use App\Models\ApiKey;
use App\Models\Backup; use App\Models\Backup;
@@ -126,7 +127,7 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function register(): void public function register(): void
{ {
$this->app->bind(LoginResponseContract::class, \App\Http\Responses\LoginResponse::class); $this->app->bind(LoginResponseContract::class, LoginResponse::class);
Scramble::ignoreDefaultRoutes(); Scramble::ignoreDefaultRoutes();

View File

@@ -7,6 +7,7 @@ use App\Filament\Pages\Auth\EditProfile;
use App\Filament\Pages\Auth\Login; use App\Filament\Pages\Auth\Login;
use App\Http\Middleware\LanguageMiddleware; use App\Http\Middleware\LanguageMiddleware;
use App\Http\Middleware\RequireTwoFactorAuthentication; use App\Http\Middleware\RequireTwoFactorAuthentication;
use App\Http\Middleware\SetSecurityHeaders;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Auth\MultiFactor\App\AppAuthentication; use Filament\Auth\MultiFactor\App\AppAuthentication;
use Filament\Auth\MultiFactor\Email\EmailAuthentication; use Filament\Auth\MultiFactor\Email\EmailAuthentication;
@@ -70,6 +71,7 @@ abstract class PanelProvider extends BasePanelProvider
DisableBladeIconComponents::class, DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class, DispatchServingFilamentEvent::class,
LanguageMiddleware::class, LanguageMiddleware::class,
SetSecurityHeaders::class,
]) ])
->authMiddleware([ ->authMiddleware([
Authenticate::class, Authenticate::class,

View File

@@ -147,7 +147,7 @@ class DaemonServerRepository extends DaemonRepository
} }
/** /**
* Deauthorizes a user (disconnects websockets and SFTP) on the Wings instance for the server. * Deauthorizes a user (disconnects websockets and SFTP) on the Wings instance for the server (or all servers of a node).
* *
* @throws ConnectionException * @throws ConnectionException
*/ */
@@ -156,7 +156,7 @@ class DaemonServerRepository extends DaemonRepository
$this->getHttpClient()->post('/api/deauthorize-user', [ $this->getHttpClient()->post('/api/deauthorize-user', [
'json' => [ 'json' => [
'user' => $user, 'user' => $user,
'servers' => [$this->server->uuid], 'servers' => $this->server ? [$this->server->uuid] : [],
], ],
]); ]);
} }

View File

@@ -56,6 +56,7 @@ class FindAssignableAllocationService
// those belonging to the current server (making it impossible to find unassigned ones) // those belonging to the current server (making it impossible to find unassigned ones)
/** @var Allocation|null $allocation */ /** @var Allocation|null $allocation */
$allocation = Allocation::withoutGlobalScopes() $allocation = Allocation::withoutGlobalScopes()
->lockForUpdate()
->where('node_id', $server->node_id) ->where('node_id', $server->node_id)
->when($server->allocation, function ($query) use ($server) { ->when($server->allocation, function ($query) use ($server) {
$query->where('ip', $server->allocation->ip); $query->where('ip', $server->allocation->ip);
@@ -125,6 +126,7 @@ class FindAssignableAllocationService
/** @var Allocation $allocation */ /** @var Allocation $allocation */
$allocation = Allocation::withoutGlobalScopes() $allocation = Allocation::withoutGlobalScopes()
->lockForUpdate()
->where('node_id', $server->node_id) ->where('node_id', $server->node_id)
->where('ip', $server->allocation->ip) ->where('ip', $server->allocation->ip)
->where('port', $port) ->where('port', $port)

View File

@@ -21,7 +21,7 @@ class DatabaseManagementService
* The regex used to validate that the database name passed through to the function is * The regex used to validate that the database name passed through to the function is
* in the expected format. * in the expected format.
* *
* @see \App\Services\Databases\DatabaseManagementService::generateUniqueDatabaseName() * @see DatabaseManagementService::generateUniqueDatabaseName()
*/ */
private const MATCH_NAME_REGEX = '/^(s[\d]+_)(.*)$/'; private const MATCH_NAME_REGEX = '/^(s[\d]+_)(.*)$/';
@@ -136,7 +136,7 @@ class DatabaseManagementService
/** /**
* Updates a password for a given database. * Updates a password for a given database.
* *
* @throws \Exception * @throws Exception
*/ */
public function rotatePassword(Database $database): void public function rotatePassword(Database $database): void
{ {

View File

@@ -18,7 +18,7 @@ class EggExporterService
public function handle(int $egg, EggFormat $format): string public function handle(int $egg, EggFormat $format): string
{ {
$egg = Egg::with(['scriptFrom', 'configFrom', 'variables'])->findOrFail($egg); $egg = Egg::with(['scriptFrom', 'configFrom', 'variables'])->findOrFail($egg);
$imageBase64 = $this->getEggImageAsBase64($egg); $iconBase64 = $this->getEggIconAsBase64($egg);
$struct = [ $struct = [
'_comment' => 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL', '_comment' => 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL',
@@ -31,7 +31,7 @@ class EggExporterService
'author' => $egg->author, 'author' => $egg->author,
'uuid' => $egg->uuid, 'uuid' => $egg->uuid,
'description' => $egg->description, 'description' => $egg->description,
'image' => $imageBase64, 'icon' => $iconBase64,
'tags' => $egg->tags, 'tags' => $egg->tags,
'features' => $egg->features, 'features' => $egg->features,
'docker_images' => $egg->docker_images, 'docker_images' => $egg->docker_images,
@@ -63,16 +63,14 @@ class EggExporterService
} }
/** /**
* Get the egg image as base64 for export. * Get the egg icon as base64 for export.
*/ */
private function getEggImageAsBase64(Egg $egg): ?string private function getEggIconAsBase64(Egg $egg): ?string
{ {
foreach (array_keys(Egg::IMAGE_FORMATS) as $ext) { foreach (Egg::$iconFormats as $ext => $mimeType) {
$path = Egg::ICON_STORAGE_PATH . "/$egg->uuid.$ext"; $path = Egg::getIconStoragePath() . "/$egg->uuid.$ext";
if (Storage::disk('public')->exists($path)) { if (Storage::disk('public')->exists($path)) {
$mimeType = Egg::IMAGE_FORMATS[$ext];
return 'data:' . $mimeType . ';base64,' . base64_encode(Storage::disk('public')->get($path)); return 'data:' . $mimeType . ';base64,' . base64_encode(Storage::disk('public')->get($path));
} }
} }

View File

@@ -6,12 +6,12 @@ use App\Enums\EggFormat;
use App\Exceptions\Service\InvalidFileUploadException; use App\Exceptions\Service\InvalidFileUploadException;
use App\Models\Egg; use App\Models\Egg;
use App\Models\EggVariable; use App\Models\EggVariable;
use Exception;
use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionInterface;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use JsonException; use JsonException;
use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Uuid;
use stdClass; use stdClass;
@@ -223,6 +223,11 @@ class EggImporterService
} }
} }
if (!empty($parsed['image']) && str_starts_with($parsed['image'], 'data:')) {
$parsed['icon'] = $parsed['image'];
unset($parsed['image']);
}
return $parsed; return $parsed;
} }
@@ -231,9 +236,9 @@ class EggImporterService
*/ */
protected function fillFromParsed(Egg $model, array $parsed): Egg protected function fillFromParsed(Egg $model, array $parsed): Egg
{ {
// Handle image data if present // Handle icon data if present
if (!empty($parsed['image']) && str_starts_with($parsed['image'], 'data:')) { if (!empty($parsed['icon']) && str_starts_with($parsed['icon'], 'data:')) {
$this->saveEggImageFromBase64($parsed['image'], $model); $this->saveEggIconFromBase64($parsed['icon'], $model);
} }
return $model->forceFill([ return $model->forceFill([
@@ -256,28 +261,24 @@ class EggImporterService
} }
/** /**
* Save an egg image from base64 data to a file. * Save an egg icon from base64 data to a file.
*/ */
private function saveEggImageFromBase64(string $base64String, Egg $egg): void private function saveEggIconFromBase64(string $base64String, Egg $egg): void
{ {
if (!preg_match('/^data:image\/([\w+]+);base64,(.+)$/', $base64String, $matches)) { if (!preg_match('/^data:image\/([\w+]+);base64,(.+)$/', $base64String, $matches)) {
return; return;
} }
$extension = $matches[1]; try {
$data = base64_decode($matches[2]); $extension = strtolower($matches[1]);
$data = base64_decode($matches[2]);
if (!$data) { if ($data) {
return; $egg->writeIcon($extension, $data);
}
} catch (Exception $exception) {
report($exception);
} }
$normalizedExtension = match ($extension) {
'svg+xml' => 'svg',
'jpeg' => 'jpg',
default => $extension,
};
Storage::disk('public')->put(Egg::ICON_STORAGE_PATH . "/$egg->uuid.$normalizedExtension", $data);
} }
/** /**

View File

@@ -348,8 +348,7 @@ class PluginService
$this->manageComposerPackages(oldPackages: $pluginPackages); $this->manageComposerPackages(oldPackages: $pluginPackages);
// This throws an error when not called with qualifier foreach (Filament::getPanels() as $panel) {
foreach (\Filament\Facades\Filament::getPanels() as $panel) {
$panel->clearCachedComponents(); $panel->clearCachedComponents();
} }
} }

View File

@@ -40,6 +40,7 @@ class NodeUpdateService
/** @var Node $updated */ /** @var Node $updated */
$updated = $node->replicate(); $updated = $node->replicate();
$updated->exists = true; $updated->exists = true;
$data = array_merge($data, ['created_at' => $node->created_at, 'updated_at' => now()]);
$updated->forceFill($data)->save(); $updated->forceFill($data)->save();
try { try {
$node->fqdn = $updated->fqdn; $node->fqdn = $updated->fqdn;

View File

@@ -2,11 +2,10 @@
namespace App\Services\Servers; namespace App\Services\Servers;
use App\Jobs\RevokeSftpAccessJob;
use App\Models\Server; use App\Models\Server;
use App\Repositories\Daemon\DaemonServerRepository;
use App\Traits\Services\ReturnsUpdatedModels; use App\Traits\Services\ReturnsUpdatedModels;
use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionInterface;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Throwable; use Throwable;
@@ -17,7 +16,7 @@ class DetailsModificationService
/** /**
* DetailsModificationService constructor. * DetailsModificationService constructor.
*/ */
public function __construct(private ConnectionInterface $connection, private DaemonServerRepository $serverRepository) {} public function __construct(private ConnectionInterface $connection) {}
/** /**
* Update the details for a single server instance. * Update the details for a single server instance.
@@ -34,7 +33,7 @@ class DetailsModificationService
public function handle(Server $server, array $data): Server public function handle(Server $server, array $data): Server
{ {
return $this->connection->transaction(function () use ($data, $server) { return $this->connection->transaction(function () use ($data, $server) {
$owner = $server->owner_id; $oldOwner = $server->user;
$server->forceFill([ $server->forceFill([
'external_id' => Arr::get($data, 'external_id'), 'external_id' => Arr::get($data, 'external_id'),
@@ -46,14 +45,8 @@ class DetailsModificationService
// If the owner_id value is changed we need to revoke any tokens that exist for the server // If the owner_id value is changed we need to revoke any tokens that exist for the server
// on the daemon instance so that the old owner no longer has any permission to access the // on the daemon instance so that the old owner no longer has any permission to access the
// websockets. // websockets.
if ($server->owner_id !== $owner) { if ($server->owner_id !== $oldOwner->id) {
try { RevokeSftpAccessJob::dispatch($oldOwner->uuid, $server);
$this->serverRepository->setServer($server)->deauthorize($server->user->uuid);
} catch (ConnectionException) {
// Do nothing. A failure here is not ideal, but it is likely to be caused by daemon
// being offline, or in an entirely broken state. Remember, these tokens reset every
// few minutes by default, we're just trying to help it along a little quicker.
}
} }
return $server; return $server;

View File

@@ -4,17 +4,12 @@ namespace App\Services\Subusers;
use App\Events\Server\SubUserRemoved; use App\Events\Server\SubUserRemoved;
use App\Facades\Activity; use App\Facades\Activity;
use App\Jobs\RevokeSftpAccessJob;
use App\Models\Server; use App\Models\Server;
use App\Models\Subuser; use App\Models\Subuser;
use App\Repositories\Daemon\DaemonServerRepository;
use Illuminate\Http\Client\ConnectionException;
class SubuserDeletionService class SubuserDeletionService
{ {
public function __construct(
private DaemonServerRepository $serverRepository,
) {}
public function handle(Subuser $subuser, Server $server): void public function handle(Subuser $subuser, Server $server): void
{ {
$log = Activity::event('server:subuser.delete') $log = Activity::event('server:subuser.delete')
@@ -27,14 +22,7 @@ class SubuserDeletionService
event(new SubUserRemoved($subuser->server, $subuser->user)); event(new SubUserRemoved($subuser->server, $subuser->user));
try { RevokeSftpAccessJob::dispatch($subuser->user->uuid, $server);
$this->serverRepository->setServer($server)->deauthorize($subuser->user->uuid);
} catch (ConnectionException $exception) {
// Don't block this request if we can't connect to the daemon instance.
logger()->warning($exception, ['user_id' => $subuser->user_id, 'server_id' => $server->id]);
$instance->property('revoked', false);
}
}); });
} }
} }

View File

@@ -4,17 +4,12 @@ namespace App\Services\Subusers;
use App\Enums\SubuserPermission; use App\Enums\SubuserPermission;
use App\Facades\Activity; use App\Facades\Activity;
use App\Jobs\RevokeSftpAccessJob;
use App\Models\Server; use App\Models\Server;
use App\Models\Subuser; use App\Models\Subuser;
use App\Repositories\Daemon\DaemonServerRepository;
use Illuminate\Http\Client\ConnectionException;
class SubuserUpdateService class SubuserUpdateService
{ {
public function __construct(
private DaemonServerRepository $serverRepository,
) {}
/** /**
* @param string[] $permissions * @param string[] $permissions
*/ */
@@ -42,18 +37,10 @@ class SubuserUpdateService
// Only update the database and hit up the daemon instance to invalidate JTI's if the permissions // Only update the database and hit up the daemon instance to invalidate JTI's if the permissions
// have actually changed for the user. // have actually changed for the user.
if ($cleanedPermissions !== $current) { if ($cleanedPermissions !== $current) {
$log->transaction(function ($instance) use ($subuser, $cleanedPermissions, $server) { $log->transaction(function () use ($subuser, $cleanedPermissions, $server) {
$subuser->update(['permissions' => $cleanedPermissions]); $subuser->update(['permissions' => $cleanedPermissions]);
try { RevokeSftpAccessJob::dispatch($subuser->user->uuid, $server);
$this->serverRepository->setServer($server)->deauthorize($subuser->user->uuid);
} catch (ConnectionException $exception) {
// Don't block this request if we can't connect to the daemon instance. Chances are it is
// offline and the token will be invalid once daemon boots back.
logger()->warning($exception, ['user_id' => $subuser->user_id, 'server_id' => $server->id]);
$instance->property('revoked', false);
}
}); });
} }

View File

@@ -2,6 +2,7 @@
namespace App\Services\Users; namespace App\Services\Users;
use App\Events\User\PasswordChanged;
use App\Models\User; use App\Models\User;
use App\Traits\Services\HasUserLevels; use App\Traits\Services\HasUserLevels;
use Illuminate\Contracts\Hashing\Hasher; use Illuminate\Contracts\Hashing\Hasher;
@@ -30,6 +31,10 @@ class UserUpdateService
$user->forceFill($data)->saveOrFail(); $user->forceFill($data)->saveOrFail();
if (isset($data['password'])) {
PasswordChanged::dispatch($user);
}
return $user->refresh(); return $user->refresh();
} }
} }

View File

@@ -46,10 +46,10 @@ class EggTransformer extends BaseTransformer
'name' => $model->name, 'name' => $model->name,
'author' => $model->author, 'author' => $model->author,
'description' => $model->description, 'description' => $model->description,
'image' => $model->image, 'icon' => $model->icon,
'features' => $model->features, 'features' => $model->features,
'tags' => $model->tags, 'tags' => $model->tags,
'docker_image' => Arr::first($model->docker_images, default: ''), // docker_images, use startup_commands 'docker_image' => Arr::first($model->docker_images, default: ''), // deprecated, use docker_images
'docker_images' => $model->docker_images, 'docker_images' => $model->docker_images,
'config' => [ 'config' => [
'files' => $files, 'files' => $files,

View File

@@ -1,5 +1,7 @@
<?php <?php
use App\Models\User;
if (!function_exists('is_digit')) { if (!function_exists('is_digit')) {
/** /**
* Deal with normal (and irritating) PHP behavior to determine if * Deal with normal (and irritating) PHP behavior to determine if
@@ -130,8 +132,29 @@ if (!function_exists('encode_path')) {
} }
} }
if (!function_exists('convert_to_utf8')) {
/**
* Convert a string to UTF-8 from an unknown encoding
*/
function convert_to_utf8(string $contents): string
{
// Valid UTF-8 passes through unchanged
if (mb_check_encoding($contents, 'UTF-8')) {
return $contents;
}
// Only detect UTF-16 by BOM instead of mb_check_encoding('UTF-16') which can cause false positives
if (str_starts_with($contents, "\xFF\xFE") || str_starts_with($contents, "\xFE\xFF")) {
return mb_convert_encoding($contents, 'UTF-8', 'UTF-16');
}
// ISO-8859-1 serves as a universal fallback since any byte sequence is valid in it
return mb_convert_encoding($contents, 'UTF-8', 'ISO-8859-1');
}
}
if (!function_exists('user')) { if (!function_exists('user')) {
function user(): ?App\Models\User function user(): ?User
{ {
return auth(config('auth.defaults.guard', 'web'))->user(); return auth(config('auth.defaults.guard', 'web'))->user();
} }

View File

@@ -1,8 +1,25 @@
<?php <?php
use App\Console\Kernel;
use App\Exceptions\Handler;
use App\Http\Middleware\Activity\TrackAPIKey;
use App\Http\Middleware\Api\Application\AuthenticateApplicationUser;
use App\Http\Middleware\Api\AuthenticateIPAccess;
use App\Http\Middleware\Api\Client\RequireClientApiKey;
use App\Http\Middleware\Api\Daemon\DaemonAuthenticate;
use App\Http\Middleware\Api\IsValidJson;
use App\Http\Middleware\EnsureStatefulRequests;
use App\Http\Middleware\LanguageMiddleware;
use App\Http\Middleware\MaintenanceMiddleware;
use App\Http\Middleware\RedirectIfAuthenticated;
use App\Http\Middleware\SetSecurityHeaders;
use App\Http\Middleware\VerifyCsrfToken;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware; use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
use Illuminate\Routing\Middleware\SubstituteBindings;
return Application::configure(basePath: dirname(__DIR__)) return Application::configure(basePath: dirname(__DIR__))
->withProviders() ->withProviders()
@@ -12,46 +29,49 @@ return Application::configure(basePath: dirname(__DIR__))
->withMiddleware(function (Middleware $middleware) { ->withMiddleware(function (Middleware $middleware) {
$middleware->redirectGuestsTo(fn () => route('filament.app.auth.login')); $middleware->redirectGuestsTo(fn () => route('filament.app.auth.login'));
$middleware->web(\App\Http\Middleware\LanguageMiddleware::class); $middleware->web([
LanguageMiddleware::class,
SetSecurityHeaders::class,
]);
$middleware->api([ $middleware->api([
\App\Http\Middleware\EnsureStatefulRequests::class, EnsureStatefulRequests::class,
'auth:sanctum', 'auth:sanctum',
\App\Http\Middleware\Api\IsValidJson::class, IsValidJson::class,
\App\Http\Middleware\Activity\TrackAPIKey::class, TrackAPIKey::class,
\App\Http\Middleware\Api\AuthenticateIPAccess::class, AuthenticateIPAccess::class,
]); ]);
$middleware->group('application-api', [ $middleware->group('application-api', [
\Illuminate\Routing\Middleware\SubstituteBindings::class, SubstituteBindings::class,
\App\Http\Middleware\Api\Application\AuthenticateApplicationUser::class, AuthenticateApplicationUser::class,
]); ]);
$middleware->group('client-api', [ $middleware->group('client-api', [
\Illuminate\Routing\Middleware\SubstituteBindings::class, SubstituteBindings::class,
\App\Http\Middleware\Api\Client\RequireClientApiKey::class, RequireClientApiKey::class,
]); ]);
$middleware->group('daemon', [ $middleware->group('daemon', [
\Illuminate\Routing\Middleware\SubstituteBindings::class, SubstituteBindings::class,
\App\Http\Middleware\Api\Daemon\DaemonAuthenticate::class, DaemonAuthenticate::class,
]); ]);
$middleware->replaceInGroup('web', \Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, \App\Http\Middleware\VerifyCsrfToken::class); $middleware->replaceInGroup('web', ValidateCsrfToken::class, VerifyCsrfToken::class);
$middleware->alias([ $middleware->alias([
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 'bindings' => SubstituteBindings::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'guest' => RedirectIfAuthenticated::class,
'node.maintenance' => \App\Http\Middleware\MaintenanceMiddleware::class, 'node.maintenance' => MaintenanceMiddleware::class,
]); ]);
$middleware->priority([ $middleware->priority([
\Illuminate\Routing\Middleware\SubstituteBindings::class, SubstituteBindings::class,
]); ]);
}) })
->withSingletons([ ->withSingletons([
\Illuminate\Contracts\Console\Kernel::class => \App\Console\Kernel::class, Illuminate\Contracts\Console\Kernel::class => Kernel::class,
\Illuminate\Contracts\Debug\ExceptionHandler::class => \App\Exceptions\Handler::class, ExceptionHandler::class => Handler::class,
]) ])
->withExceptions(function (Exceptions $exceptions) {}) ->withExceptions(function (Exceptions $exceptions) {})
->create(); ->create();

View File

@@ -1,19 +1,35 @@
<?php <?php
use App\Providers\ActivityLogServiceProvider;
use App\Providers\AppServiceProvider;
use App\Providers\BackupsServiceProvider;
use App\Providers\EventServiceProvider;
use App\Providers\Extensions\AvatarServiceProvider;
use App\Providers\Extensions\CaptchaServiceProvider;
use App\Providers\Extensions\FeatureServiceProvider;
use App\Providers\Extensions\OAuthServiceProvider;
use App\Providers\Extensions\TaskServiceProvider;
use App\Providers\Filament\AdminPanelProvider;
use App\Providers\Filament\AppPanelProvider;
use App\Providers\Filament\FilamentServiceProvider;
use App\Providers\Filament\ServerPanelProvider;
use App\Providers\RouteServiceProvider;
use SocialiteProviders\Manager\ServiceProvider;
return [ return [
App\Providers\ActivityLogServiceProvider::class, ActivityLogServiceProvider::class,
App\Providers\AppServiceProvider::class, AppServiceProvider::class,
App\Providers\BackupsServiceProvider::class, BackupsServiceProvider::class,
App\Providers\EventServiceProvider::class, EventServiceProvider::class,
App\Providers\Extensions\AvatarServiceProvider::class, AvatarServiceProvider::class,
App\Providers\Extensions\CaptchaServiceProvider::class, CaptchaServiceProvider::class,
App\Providers\Extensions\FeatureServiceProvider::class, FeatureServiceProvider::class,
App\Providers\Extensions\OAuthServiceProvider::class, OAuthServiceProvider::class,
App\Providers\Extensions\TaskServiceProvider::class, TaskServiceProvider::class,
App\Providers\Filament\FilamentServiceProvider::class, FilamentServiceProvider::class,
App\Providers\Filament\AdminPanelProvider::class, AdminPanelProvider::class,
App\Providers\Filament\AppPanelProvider::class, AppPanelProvider::class,
App\Providers\Filament\ServerPanelProvider::class, ServerPanelProvider::class,
App\Providers\RouteServiceProvider::class, RouteServiceProvider::class,
SocialiteProviders\Manager\ServiceProvider::class, ServiceProvider::class,
]; ];

View File

@@ -9,51 +9,51 @@
"ext-mbstring": "*", "ext-mbstring": "*",
"ext-pdo": "*", "ext-pdo": "*",
"ext-zip": "*", "ext-zip": "*",
"aws/aws-sdk-php": "^3.369", "aws/aws-sdk-php": "^3.373",
"calebporzio/sushi": "^2.5", "calebporzio/sushi": "^2.5",
"dedoc/scramble": "^0.13", "dedoc/scramble": "^0.13",
"filament/filament": "^4.5", "filament/filament": "^4.8",
"gboquizosanchez/filament-log-viewer": "^2.2", "gboquizosanchez/filament-log-viewer": "^2.2",
"guzzlehttp/guzzle": "^7.10", "guzzlehttp/guzzle": "^7.10",
"laravel/framework": "^12.52", "laravel/framework": "^12.56",
"laravel/helpers": "^1.8", "laravel/helpers": "^1.8",
"laravel/sanctum": "^4.3", "laravel/sanctum": "^4.3",
"laravel/socialite": "^5.24", "laravel/socialite": "^5.25",
"laravel/tinker": "^2.10.1", "laravel/tinker": "^2.11",
"laravel/ui": "^4.6", "laravel/ui": "^4.6",
"lcobucci/jwt": "^5.6", "lcobucci/jwt": "^5.6",
"league/flysystem-aws-s3-v3": "^3.31", "league/flysystem-aws-s3-v3": "^3.32",
"league/flysystem-memory": "^3.31", "league/flysystem-memory": "^3.31",
"phiki/phiki": "^2.0", "phiki/phiki": "^2.1",
"phpseclib/phpseclib": "~3.0.18", "phpseclib/phpseclib": "^3.0.49",
"predis/predis": "^2.3", "predis/predis": "^2.4",
"s1lentium/iptools": "~1.2.0", "s1lentium/iptools": "^1.2",
"secondnetwork/blade-tabler-icons": "^3.26", "secondnetwork/blade-tabler-icons": "^3.40",
"socialiteproviders/authentik": "^5.2", "socialiteproviders/authentik": "^5.3",
"socialiteproviders/discord": "^4.2", "socialiteproviders/discord": "^4.2",
"socialiteproviders/steam": "^4.3", "socialiteproviders/steam": "^4.3",
"spatie/laravel-data": "^4.19", "spatie/laravel-data": "^4.20",
"spatie/laravel-fractal": "^6.3", "spatie/laravel-fractal": "^6.4",
"spatie/laravel-health": "^1.37", "spatie/laravel-health": "^1.39",
"spatie/laravel-permission": "^6.24", "spatie/laravel-permission": "^6.25",
"spatie/laravel-query-builder": "^6.4", "spatie/laravel-query-builder": "^6.4",
"spatie/temporary-directory": "^2.3", "spatie/temporary-directory": "^2.3",
"symfony/http-client": "^7.2", "symfony/http-client": "^7.4",
"symfony/mailgun-mailer": "^7.2", "symfony/mailgun-mailer": "^7.4",
"symfony/postmark-mailer": "^7.2", "symfony/postmark-mailer": "^7.4",
"symfony/yaml": "^7.2", "symfony/yaml": "^7.4",
"webmozart/assert": "~1.11.0" "webmozart/assert": "^1.12"
}, },
"require-dev": { "require-dev": {
"barryvdh/laravel-ide-helper": "^3.6", "barryvdh/laravel-ide-helper": "^3.7",
"fakerphp/faker": "^1.23.1", "fakerphp/faker": "^1.24",
"larastan/larastan": "^3.4", "larastan/larastan": "^3.9",
"laravel/pail": "^1.2.2", "laravel/pail": "^1.2.6",
"laravel/pint": "^1.15.3", "laravel/pint": "^1.28",
"laravel/sail": "^1.41", "laravel/sail": "^1.53",
"mockery/mockery": "^1.6.11", "mockery/mockery": "^1.6.12",
"nunomaduro/collision": "^8.6", "nunomaduro/collision": "^8.9",
"pestphp/pest": "^3.7", "pestphp/pest": "^3.8",
"pestphp/pest-plugin-faker": "^3.0", "pestphp/pest-plugin-faker": "^3.0",
"pestphp/pest-plugin-livewire": "^3.0" "pestphp/pest-plugin-livewire": "^3.0"
}, },

1270
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
<?php <?php
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Pdo\Mysql;
$database = env('DB_DATABASE', 'database.sqlite'); $database = env('DB_DATABASE', 'database.sqlite');
$databasePath = database_path($database); $databasePath = database_path($database);
@@ -65,7 +66,7 @@ return [
'strict' => env('DB_STRICT_MODE', false), 'strict' => env('DB_STRICT_MODE', false),
'engine' => null, 'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([ 'options' => extension_loaded('pdo_mysql') ? array_filter([
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), (PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
]) : [], ]) : [],
], ],
@@ -85,7 +86,7 @@ return [
'strict' => env('DB_STRICT_MODE', false), 'strict' => env('DB_STRICT_MODE', false),
'engine' => null, 'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([ 'options' => extension_loaded('pdo_mysql') ? array_filter([
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), (PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
]) : [], ]) : [],
], ],

View File

@@ -1,5 +1,7 @@
<?php <?php
use League\Fractal\Serializer\JsonApiSerializer;
return [ return [
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@@ -12,7 +14,7 @@ return [
| |
*/ */
'default_serializer' => League\Fractal\Serializer\JsonApiSerializer::class, 'default_serializer' => JsonApiSerializer::class,
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@@ -1,5 +1,9 @@
<?php <?php
use Spatie\Health\Notifications\CheckFailedNotification;
use Spatie\Health\Notifications\Notifiable;
use Spatie\Health\ResultStores\CacheHealthResultStore;
return [ return [
/* /*
* A result store is responsible for saving the results of the checks. The * A result store is responsible for saving the results of the checks. The
@@ -15,7 +19,7 @@ return [
], ],
*/ */
Spatie\Health\ResultStores\CacheHealthResultStore::class => [ CacheHealthResultStore::class => [
'store' => 'file', 'store' => 'file',
], ],
@@ -40,14 +44,14 @@ return [
'enabled' => false, 'enabled' => false,
'notifications' => [ 'notifications' => [
Spatie\Health\Notifications\CheckFailedNotification::class => ['mail'], CheckFailedNotification::class => ['mail'],
], ],
/* /*
* Here you can specify the notifiable to which the notifications should be sent. The default * Here you can specify the notifiable to which the notifications should be sent. The default
* notifiable will use the variables specified in this config file. * notifiable will use the variables specified in this config file.
*/ */
'notifiable' => Spatie\Health\Notifications\Notifiable::class, 'notifiable' => Notifiable::class,
/* /*
* When checks start failing, you could potentially end up getting * When checks start failing, you could potentially end up getting

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