mirror of
https://github.com/pelican-dev/panel.git
synced 2026-02-14 11:20:17 +03:00
Compare commits
5 Commits
main
...
lance/2069
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52e5d676a8 | ||
|
|
efe541fb73 | ||
|
|
b183a75c36 | ||
|
|
892490176c | ||
|
|
f95d9b414a |
14
.github/workflows/ci.yaml
vendored
14
.github/workflows/ci.yaml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
php: [8.3, 8.4, 8.5]
|
||||
php: [8.2, 8.3, 8.4, 8.5]
|
||||
env:
|
||||
DB_CONNECTION: sqlite
|
||||
DB_DATABASE: testing.sqlite
|
||||
@@ -79,8 +79,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
php: [8.5]
|
||||
database: ["mysql:8.4", "mysql:9.6"]
|
||||
php: [8.2, 8.3, 8.4, 8.5]
|
||||
database: ["mysql:8"]
|
||||
services:
|
||||
database:
|
||||
image: ${{ matrix.database }}
|
||||
@@ -147,8 +147,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
php: [8.5]
|
||||
database: ["mariadb:10.11", "mariadb:11.4"]
|
||||
php: [8.2, 8.3, 8.4, 8.5]
|
||||
database: ["mariadb:10.6", "mariadb:10.11", "mariadb:11.4"]
|
||||
services:
|
||||
database:
|
||||
image: ${{ matrix.database }}
|
||||
@@ -215,8 +215,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
php: [8.5]
|
||||
database: ["postgres:17", "postgres:18"]
|
||||
php: [8.2, 8.3, 8.4, 8.5]
|
||||
database: ["postgres:14"]
|
||||
services:
|
||||
database:
|
||||
image: ${{ matrix.database }}
|
||||
|
||||
@@ -68,9 +68,6 @@ RUN apk add --no-cache \
|
||||
# required for installing plugins. Pulled from https://github.com/pelican-dev/panel/pull/2034
|
||||
zip unzip 7zip bzip2-dev yarn git
|
||||
|
||||
# Copy composer binary for runtime plugin dependency management
|
||||
COPY --from=composer /usr/local/bin/composer /usr/local/bin/composer
|
||||
|
||||
COPY --chown=root:www-data --chmod=770 --from=composerbuild /build .
|
||||
COPY --chown=root:www-data --chmod=770 --from=yarnbuild /build/public ./public
|
||||
|
||||
@@ -86,7 +83,8 @@ RUN mkdir -p /pelican-data/storage /pelican-data/plugins /var/run/supervisord \
|
||||
# Allow www-data write permissions where necessary
|
||||
&& chown -R www-data: /pelican-data .env ./storage ./plugins ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
|
||||
&& chmod -R 770 /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
|
||||
&& chown -R www-data: /usr/local/etc/php/ /usr/local/etc/php-fpm.d/ /var/www/html/composer.json /var/www/html/composer.lock
|
||||
&& chown -R www-data: /usr/local/etc/php/ /usr/local/etc/php-fpm.d/
|
||||
|
||||
# Configure Supervisor
|
||||
COPY docker/supervisord.conf /etc/supervisord.conf
|
||||
COPY docker/Caddyfile /etc/caddy/Caddyfile
|
||||
|
||||
@@ -73,9 +73,6 @@ RUN apk add --no-cache \
|
||||
# required for installing plugins. Pulled from https://github.com/pelican-dev/panel/pull/2034
|
||||
zip unzip 7zip bzip2-dev yarn git
|
||||
|
||||
# Copy composer binary for runtime plugin dependency management
|
||||
COPY --from=composer /usr/local/bin/composer /usr/local/bin/composer
|
||||
|
||||
COPY --chown=root:www-data --chmod=770 --from=composerbuild /build .
|
||||
COPY --chown=root:www-data --chmod=770 --from=yarnbuild /build/public ./public
|
||||
|
||||
@@ -91,7 +88,7 @@ RUN mkdir -p /pelican-data/storage /pelican-data/plugins /var/run/supervisord \
|
||||
# Allow www-data write permissions where necessary
|
||||
&& chown -R www-data: /pelican-data .env ./storage ./plugins ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
|
||||
&& chmod -R 770 /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
|
||||
&& chown -R www-data: /usr/local/etc/php/ /usr/local/etc/php-fpm.d/ /var/www/html/composer.json /var/www/html/composer.lock
|
||||
&& chown -R www-data: /usr/local/etc/php/ /usr/local/etc/php-fpm.d/
|
||||
|
||||
# Configure Supervisor
|
||||
COPY docker/supervisord.conf /etc/supervisord.conf
|
||||
|
||||
@@ -12,7 +12,6 @@ enum CustomizationKey: string
|
||||
case DashboardLayout = 'dashboard_layout';
|
||||
|
||||
case ButtonStyle = 'button_style';
|
||||
case RedirectToAdmin = 'redirect_to_admin';
|
||||
|
||||
public function getDefaultValue(): string|int|bool
|
||||
{
|
||||
@@ -24,7 +23,6 @@ enum CustomizationKey: string
|
||||
self::TopNavigation => config('panel.filament.default-navigation', 'sidebar'),
|
||||
self::DashboardLayout => 'grid',
|
||||
self::ButtonStyle => true,
|
||||
self::RedirectToAdmin => false,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,8 +13,6 @@ use App\Traits\Filament\CanCustomizeHeaderActions;
|
||||
use App\Traits\Filament\CanCustomizeHeaderWidgets;
|
||||
use App\Traits\Filament\CanCustomizeTabs;
|
||||
use BackedEnum;
|
||||
use BladeUI\Icons\Exceptions\SvgNotFound;
|
||||
use BladeUI\Icons\Factory as IconFactory;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\ActionGroup;
|
||||
@@ -70,8 +68,6 @@ class Settings extends Page implements HasSchemas
|
||||
|
||||
protected CaptchaService $captchaService;
|
||||
|
||||
protected IconFactory $iconFactory;
|
||||
|
||||
/** @var array<mixed>|null */
|
||||
public ?array $data = [];
|
||||
|
||||
@@ -80,12 +76,11 @@ class Settings extends Page implements HasSchemas
|
||||
$this->form->fill();
|
||||
}
|
||||
|
||||
public function boot(OAuthService $oauthService, AvatarService $avatarService, CaptchaService $captchaService, IconFactory $iconFactory): void
|
||||
public function boot(OAuthService $oauthService, AvatarService $avatarService, CaptchaService $captchaService): void
|
||||
{
|
||||
$this->oauthService = $oauthService;
|
||||
$this->avatarService = $avatarService;
|
||||
$this->captchaService = $captchaService;
|
||||
$this->iconFactory = $iconFactory;
|
||||
}
|
||||
|
||||
public static function canAccess(): bool
|
||||
@@ -570,18 +565,9 @@ class Settings extends Page implements HasSchemas
|
||||
foreach ($oauthSchemas as $schema) {
|
||||
$key = $schema->getConfigKey();
|
||||
|
||||
$icon = $schema->getIcon();
|
||||
if (is_string($icon)) {
|
||||
try {
|
||||
$this->iconFactory->svg($icon);
|
||||
} catch (SvgNotFound) {
|
||||
$icon = null;
|
||||
}
|
||||
}
|
||||
|
||||
$formFields[] = Section::make($schema->getName())
|
||||
->columns(5)
|
||||
->icon($icon ?? TablerIcon::BrandOauth)
|
||||
->icon($schema->getIcon() ?? TablerIcon::BrandOauth)
|
||||
->collapsed(fn () => !$schema->isEnabled())
|
||||
->collapsible()
|
||||
->schema([
|
||||
|
||||
@@ -8,7 +8,6 @@ use App\Services\Databases\Hosts\HostCreationService;
|
||||
use App\Traits\Filament\CanCustomizeHeaderActions;
|
||||
use App\Traits\Filament\CanCustomizeHeaderWidgets;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
@@ -46,17 +45,6 @@ class CreateDatabaseHost extends CreateRecord
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
protected function getCreateFormAction(): Action
|
||||
{
|
||||
$hasFormWrapper = $this->hasFormWrapper();
|
||||
|
||||
return Action::make('exclude_create')
|
||||
->label(trans('filament-panels::resources/pages/create-record.form.actions.create.label'))
|
||||
->submit($hasFormWrapper ? $this->getSubmitFormLivewireMethodName() : null)
|
||||
->action($hasFormWrapper ? null : $this->getSubmitFormLivewireMethodName())
|
||||
->keyBindings(['mod+s']);
|
||||
}
|
||||
|
||||
/** @return Step[]
|
||||
* @throws Exception
|
||||
*/
|
||||
|
||||
@@ -318,7 +318,6 @@ class EditEgg extends EditRecord
|
||||
->helperText(trans('admin/egg.start_config_help')),
|
||||
Textarea::make('config_files')->rows(10)->json()
|
||||
->label(trans('admin/egg.config_files'))
|
||||
->dehydrateStateUsing(fn ($state) => blank($state) ? '{}' : $state)
|
||||
->helperText(trans('admin/egg.config_files_help')),
|
||||
Textarea::make('config_logs')->rows(10)->json()
|
||||
->label(trans('admin/egg.log_config'))
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace App\Filament\Admin\Resources\Nodes\Pages;
|
||||
|
||||
use App\Enums\TablerIcon;
|
||||
use App\Filament\Admin\Resources\Nodes\NodeResource;
|
||||
use App\Filament\Components\Tables\Columns\NodeClientHealthColumn;
|
||||
use App\Filament\Components\Tables\Columns\NodeHealthColumn;
|
||||
use App\Filament\Components\Tables\Filters\TagsFilter;
|
||||
use App\Models\Node;
|
||||
@@ -35,7 +34,6 @@ class ListNodes extends ListRecords
|
||||
->searchable()
|
||||
->hidden(),
|
||||
NodeHealthColumn::make('health'),
|
||||
NodeClientHealthColumn::make('reachable'),
|
||||
TextColumn::make('name')
|
||||
->label(trans('admin/node.table.name'))
|
||||
->sortable()
|
||||
|
||||
@@ -91,10 +91,7 @@ class AllocationsRelationManager extends RelationManager
|
||||
->icon(TablerIcon::WorldPlus)
|
||||
->schema(fn () => [
|
||||
Select::make('allocation_ip')
|
||||
->options(fn (Get $get) => collect($this->getOwnerRecord()->ipAddresses())
|
||||
->when($get('allocation_ip'), fn ($ips, $current) => $ips->push($current))
|
||||
->unique()
|
||||
->mapWithKeys(fn (string $ip) => [$ip => $ip]))
|
||||
->options(fn () => collect($this->getOwnerRecord()->ipAddresses())->mapWithKeys(fn (string $ip) => [$ip => $ip]))
|
||||
->label(trans('admin/node.ip_address'))
|
||||
->inlineLabel()
|
||||
->ip()
|
||||
@@ -109,18 +106,6 @@ class AllocationsRelationManager extends RelationManager
|
||||
cache()->forget("nodes.{$this->getOwnerRecord()->id}.ips");
|
||||
})
|
||||
)
|
||||
->suffixAction(
|
||||
Action::make('custom_ip')
|
||||
->icon(TablerIcon::Keyboard)
|
||||
->tooltip(trans('admin/node.custom_ip'))
|
||||
->schema([
|
||||
TextInput::make('custom_ip')
|
||||
->label(trans('admin/node.ip_address'))
|
||||
->ip()
|
||||
->required(),
|
||||
])
|
||||
->action(fn (array $data, Set $set) => $set('allocation_ip', $data['custom_ip']))
|
||||
)
|
||||
->required(),
|
||||
TextInput::make('allocation_alias')
|
||||
->label(trans('admin/node.table.alias'))
|
||||
|
||||
@@ -251,10 +251,7 @@ class CreateServer extends CreateRecord
|
||||
|
||||
return [
|
||||
Select::make('allocation_ip')
|
||||
->options(fn (Get $get) => collect(Node::find($getPage('node_id'))?->ipAddresses())
|
||||
->when($get('allocation_ip'), fn ($ips, $current) => $ips->push($current))
|
||||
->unique()
|
||||
->mapWithKeys(fn (string $ip) => [$ip => $ip]))
|
||||
->options(fn () => collect(Node::find($get('node_id'))?->ipAddresses())->mapWithKeys(fn (string $ip) => [$ip => $ip]))
|
||||
->label(trans('admin/server.ip_address'))->inlineLabel()
|
||||
->helperText(trans('admin/server.ip_address_helper'))
|
||||
->afterStateUpdated(fn (Set $set) => $set('allocation_ports', []))
|
||||
@@ -269,18 +266,6 @@ class CreateServer extends CreateRecord
|
||||
cache()->forget("nodes.{$get('node_id')}.ips");
|
||||
})
|
||||
)
|
||||
->suffixAction(
|
||||
Action::make('custom_ip')
|
||||
->icon(TablerIcon::Keyboard)
|
||||
->tooltip(trans('admin/node.custom_ip'))
|
||||
->schema([
|
||||
TextInput::make('custom_ip')
|
||||
->label(trans('admin/node.ip_address'))
|
||||
->ip()
|
||||
->required(),
|
||||
])
|
||||
->action(fn (array $data, Set $set) => $set('allocation_ip', $data['custom_ip']))
|
||||
)
|
||||
->required(),
|
||||
TextInput::make('allocation_alias')
|
||||
->label(trans('admin/server.alias'))->inlineLabel()
|
||||
|
||||
@@ -935,7 +935,7 @@ class EditServer extends EditRecord
|
||||
->send();
|
||||
}
|
||||
}),
|
||||
Action::make('exclude_toggle_unsuspend')
|
||||
Action::make('toggleUnsuspend')
|
||||
->label(trans('admin/server.unsuspend'))
|
||||
->color('success')
|
||||
->hidden(fn (Server $server) => !$server->isSuspended())
|
||||
|
||||
@@ -113,10 +113,7 @@ class AllocationsRelationManager extends RelationManager
|
||||
->createAnother(false)
|
||||
->schema(fn () => [
|
||||
Select::make('allocation_ip')
|
||||
->options(fn (Get $get) => collect($this->getOwnerRecord()->node->ipAddresses())
|
||||
->when($get('allocation_ip'), fn ($ips, $current) => $ips->push($current))
|
||||
->unique()
|
||||
->mapWithKeys(fn (string $ip) => [$ip => $ip]))
|
||||
->options(fn () => collect($this->getOwnerRecord()->node->ipAddresses())->mapWithKeys(fn (string $ip) => [$ip => $ip]))
|
||||
->label(trans('admin/server.ip_address'))
|
||||
->inlineLabel()
|
||||
->ip()
|
||||
@@ -129,18 +126,6 @@ class AllocationsRelationManager extends RelationManager
|
||||
cache()->forget("nodes.{$this->getOwnerRecord()->node->id}.ips");
|
||||
})
|
||||
)
|
||||
->suffixAction(
|
||||
Action::make('custom_ip')
|
||||
->icon(TablerIcon::Keyboard)
|
||||
->tooltip(trans('admin/node.custom_ip'))
|
||||
->schema([
|
||||
TextInput::make('custom_ip')
|
||||
->label(trans('admin/node.ip_address'))
|
||||
->ip()
|
||||
->required(),
|
||||
])
|
||||
->action(fn (array $data, Set $set) => $set('allocation_ip', $data['custom_ip']))
|
||||
)
|
||||
->afterStateUpdated(fn (Set $set) => $set('allocation_ports', []))
|
||||
->required(),
|
||||
TextInput::make('allocation_alias')
|
||||
|
||||
@@ -142,7 +142,7 @@ class UserResource extends Resource
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make('exclude_bulk_delete'),
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
CreateAction::make()
|
||||
->hiddenLabel()
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Components\Tables\Columns;
|
||||
|
||||
use Filament\Support\Enums\Alignment;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
|
||||
class NodeClientHealthColumn extends IconColumn
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->label(trans('admin/node.table.reachable'));
|
||||
|
||||
$this->alignCenter();
|
||||
}
|
||||
|
||||
public function toEmbeddedHtml(): string
|
||||
{
|
||||
$alignment = $this->getAlignment();
|
||||
|
||||
$attributes = $this->getExtraAttributeBag()
|
||||
->class([
|
||||
'fi-ta-icon',
|
||||
'fi-inline' => $this->isInline(),
|
||||
'fi-ta-icon-has-line-breaks' => $this->isListWithLineBreaks(),
|
||||
'fi-wrapped' => $this->canWrap(),
|
||||
($alignment instanceof Alignment) ? "fi-align-{$alignment->value}" : (is_string($alignment) ? $alignment : ''),
|
||||
])
|
||||
->toHtml();
|
||||
|
||||
return Blade::render(<<<'BLADE'
|
||||
<div <?= $attributes ?>>
|
||||
@livewire('node-client-connectivity', ['node' => $record, 'lazy' => true])
|
||||
</div>
|
||||
BLADE, [
|
||||
'attributes' => $attributes,
|
||||
'record' => $this->getRecord(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -254,7 +254,7 @@ class EditProfile extends BaseEditProfile
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->headerActions([
|
||||
Action::make('exclude_create_api_key')
|
||||
Action::make('create_api_key')
|
||||
->label(trans('filament-actions::create.single.modal.actions.create.label'))
|
||||
->disabled(fn (Get $get) => empty($get('description')))
|
||||
->successRedirectUrl(self::getUrl(['tab' => 'api-keys::data::tab'], panel: 'app'))
|
||||
@@ -343,7 +343,7 @@ class EditProfile extends BaseEditProfile
|
||||
->live(),
|
||||
])
|
||||
->headerActions([
|
||||
Action::make('exclude_create_ssh_key')
|
||||
Action::make('create_ssh_key')
|
||||
->label(trans('filament-actions::create.single.modal.actions.create.label'))
|
||||
->disabled(fn (Get $get) => empty($get('name')) || empty($get('public_key')))
|
||||
->successRedirectUrl(self::getUrl(['tab' => 'ssh-keys::data::tab'], panel: 'app'))
|
||||
@@ -474,17 +474,6 @@ class EditProfile extends BaseEditProfile
|
||||
false => 'Icon Button',
|
||||
]),
|
||||
]),
|
||||
Section::make(trans('profile.admin'))
|
||||
->collapsible()
|
||||
->icon(TablerIcon::Shield)
|
||||
->visible(fn (User $user) => $user->isAdmin())
|
||||
->schema([
|
||||
ToggleButtons::make('redirect_to_admin')
|
||||
->label(trans('profile.redirect_to_admin'))
|
||||
->helperText(trans('profile.redirect_to_admin_help'))
|
||||
->inline()
|
||||
->boolean(),
|
||||
]),
|
||||
Section::make(trans('profile.console'))
|
||||
->collapsible()
|
||||
->icon(TablerIcon::Terminal2)
|
||||
@@ -610,7 +599,6 @@ class EditProfile extends BaseEditProfile
|
||||
'dashboard_layout' => $data['dashboard_layout'],
|
||||
'top_navigation' => $data['top_navigation'],
|
||||
'button_style' => $data['button_style'],
|
||||
'redirect_to_admin' => $data['redirect_to_admin'] ?? $this->getUser()->getCustomization(CustomizationKey::RedirectToAdmin),
|
||||
];
|
||||
|
||||
unset(
|
||||
@@ -620,7 +608,6 @@ class EditProfile extends BaseEditProfile
|
||||
$data['dashboard_layout'],
|
||||
$data['top_navigation'],
|
||||
$data['button_style'],
|
||||
$data['redirect_to_admin'],
|
||||
);
|
||||
|
||||
$data['customization'] = json_encode($customization);
|
||||
@@ -636,7 +623,6 @@ class EditProfile extends BaseEditProfile
|
||||
$data['console_graph_period'] = (int) $this->getUser()->getCustomization(CustomizationKey::ConsoleGraphPeriod);
|
||||
$data['dashboard_layout'] = $this->getUser()->getCustomization(CustomizationKey::DashboardLayout);
|
||||
$data['button_style'] = $this->getUser()->getCustomization(CustomizationKey::ButtonStyle);
|
||||
$data['redirect_to_admin'] = $this->getUser()->getCustomization(CustomizationKey::RedirectToAdmin);
|
||||
|
||||
// Handle migration from boolean to string navigation types
|
||||
$topNavigation = $this->getUser()->getCustomization(CustomizationKey::TopNavigation);
|
||||
|
||||
@@ -4,8 +4,6 @@ namespace App\Filament\Pages\Auth;
|
||||
|
||||
use App\Extensions\Captcha\CaptchaService;
|
||||
use App\Extensions\OAuth\OAuthService;
|
||||
use BladeUI\Icons\Exceptions\SvgNotFound;
|
||||
use BladeUI\Icons\Factory as IconFactory;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Auth\Pages\Login as BaseLogin;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
@@ -21,13 +19,10 @@ class Login extends BaseLogin
|
||||
|
||||
protected CaptchaService $captchaService;
|
||||
|
||||
protected IconFactory $iconFactory;
|
||||
|
||||
public function boot(OAuthService $oauthService, CaptchaService $captchaService, IconFactory $iconFactory): void
|
||||
public function boot(OAuthService $oauthService, CaptchaService $captchaService): void
|
||||
{
|
||||
$this->oauthService = $oauthService;
|
||||
$this->captchaService = $captchaService;
|
||||
$this->iconFactory = $iconFactory;
|
||||
}
|
||||
|
||||
public function form(Schema $schema): Schema
|
||||
@@ -92,18 +87,9 @@ class Login extends BaseLogin
|
||||
$color = $schema->getHexColor();
|
||||
$color = is_string($color) ? Color::hex($color) : null;
|
||||
|
||||
$icon = $schema->getIcon();
|
||||
if (is_string($icon)) {
|
||||
try {
|
||||
$this->iconFactory->svg($icon);
|
||||
} catch (SvgNotFound) {
|
||||
$icon = null;
|
||||
}
|
||||
}
|
||||
|
||||
$actions[] = Action::make("oauth_$id")
|
||||
->label($schema->getName())
|
||||
->icon($icon)
|
||||
->icon($schema->getIcon())
|
||||
->color($color)
|
||||
->url(route('auth.oauth.redirect', ['driver' => $id], false));
|
||||
}
|
||||
|
||||
@@ -78,15 +78,11 @@ class Console extends Page
|
||||
$feature = data_get($data, 'key');
|
||||
|
||||
$feature = $this->featureService->get($feature);
|
||||
if (!$feature) {
|
||||
if (!$feature || $this->getMountedAction()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->getMountedAction()) {
|
||||
$this->replaceMountedAction($feature->getId());
|
||||
} else {
|
||||
$this->mountAction($feature->getId());
|
||||
}
|
||||
$this->mountAction($feature->getId());
|
||||
sleep(2); // TODO find a better way
|
||||
}
|
||||
|
||||
public function getWidgetData(): array
|
||||
|
||||
@@ -27,6 +27,7 @@ use BackedEnum;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\ActionGroup;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
@@ -127,7 +128,7 @@ class BackupResource extends Resource
|
||||
])
|
||||
->recordActions([
|
||||
ActionGroup::make([
|
||||
Action::make('exclude_rename')
|
||||
Action::make('rename')
|
||||
->icon(TablerIcon::Pencil)
|
||||
->authorize(fn () => user()?->can(SubuserPermission::BackupDelete, $server))
|
||||
->label(trans('server/backup.actions.rename.title'))
|
||||
@@ -157,14 +158,14 @@ class BackupResource extends Resource
|
||||
->send();
|
||||
})
|
||||
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
|
||||
Action::make('exclude_lock')
|
||||
Action::make('lock')
|
||||
->iconSize(IconSize::Large)
|
||||
->icon(fn (Backup $backup) => !$backup->is_locked ? TablerIcon::Lock : TablerIcon::LockOpen)
|
||||
->authorize(fn () => user()?->can(SubuserPermission::BackupDelete, $server))
|
||||
->label(fn (Backup $backup) => !$backup->is_locked ? trans('server/backup.actions.lock.lock') : trans('server/backup.actions.lock.unlock'))
|
||||
->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->toggleLock($request, $server, $backup))
|
||||
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
|
||||
Action::make('exclude_download')
|
||||
Action::make('download')
|
||||
->label(trans('server/backup.actions.download'))
|
||||
->iconSize(IconSize::Large)
|
||||
->color('primary')
|
||||
@@ -172,7 +173,7 @@ class BackupResource extends Resource
|
||||
->authorize(fn () => user()?->can(SubuserPermission::BackupDownload, $server))
|
||||
->url(fn (DownloadLinkService $downloadLinkService, Backup $backup, Request $request) => $downloadLinkService->handle($backup, $request->user()), true)
|
||||
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
|
||||
Action::make('exclude_restore')
|
||||
Action::make('restore')
|
||||
->label(trans('server/backup.actions.restore.title'))
|
||||
->iconSize(IconSize::Large)
|
||||
->color('success')
|
||||
@@ -225,12 +226,7 @@ class BackupResource extends Resource
|
||||
->send();
|
||||
})
|
||||
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
|
||||
Action::make('exclude_delete')
|
||||
->icon(TablerIcon::Trash)
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->authorize(fn () => user()?->can(SubuserPermission::BackupDelete, $server))
|
||||
->label(trans('filament-actions::delete.single.label'))
|
||||
DeleteAction::make('delete')
|
||||
->iconSize(IconSize::Large)
|
||||
->disabled(fn (Backup $backup) => $backup->is_locked && $backup->status !== BackupStatus::Failed)
|
||||
->modalDescription(fn (Backup $backup) => trans('server/backup.actions.delete.description', ['backup' => $backup->name]))
|
||||
|
||||
@@ -209,18 +209,16 @@ class ListFiles extends ListRecords
|
||||
->required()
|
||||
->live(),
|
||||
TextEntry::make('new_location')
|
||||
->state(fn (Get $get, File $file) => resolve_path(join_paths($this->path, str_ends_with($get('location') ?? '/', '/') ? join_paths($get('location') ?? '/', $file->name) : $get('location') ?? '/'))),
|
||||
->state(fn (Get $get, File $file) => resolve_path(join_paths($this->path, $get('location') ?? '/', $file->name))),
|
||||
])
|
||||
->action(function ($data, File $file) {
|
||||
$location = $data['location'];
|
||||
$endsWithSlash = str_ends_with($location, '/');
|
||||
$to = $endsWithSlash ? join_paths($location, $file->name) : $location;
|
||||
$files = [['to' => $to, 'from' => $file->name]];
|
||||
$files = [['to' => join_paths($location, $file->name), 'from' => $file->name]];
|
||||
|
||||
$this->getDaemonFileRepository()->renameFiles($this->path, $files);
|
||||
|
||||
$oldLocation = join_paths($this->path, $file->name);
|
||||
$newLocation = resolve_path(join_paths($this->path, $to));
|
||||
$newLocation = resolve_path(join_paths($this->path, $location, $file->name));
|
||||
|
||||
Activity::event('server:file.rename')
|
||||
->property('directory', $this->path)
|
||||
|
||||
@@ -8,7 +8,6 @@ use App\Models\Schedule;
|
||||
use App\Models\Server;
|
||||
use App\Traits\Filament\CanCustomizeHeaderActions;
|
||||
use App\Traits\Filament\CanCustomizeHeaderWidgets;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
@@ -21,17 +20,6 @@ class CreateSchedule extends CreateRecord
|
||||
|
||||
protected static bool $canCreateAnother = false;
|
||||
|
||||
protected function getCreateFormAction(): Action
|
||||
{
|
||||
$hasFormWrapper = $this->hasFormWrapper();
|
||||
|
||||
return Action::make('exclude_create')
|
||||
->label(__('filament-panels::resources/pages/create-record.form.actions.create.label'))
|
||||
->submit($hasFormWrapper ? $this->getSubmitFormLivewireMethodName() : null)
|
||||
->action($hasFormWrapper ? null : $this->getSubmitFormLivewireMethodName())
|
||||
->keyBindings(['mod+s']);
|
||||
}
|
||||
|
||||
protected function afterCreate(): void
|
||||
{
|
||||
/** @var Schedule $schedule */
|
||||
|
||||
@@ -77,17 +77,30 @@ class SubuserResource extends Resource
|
||||
$options = [];
|
||||
$descriptions = [];
|
||||
|
||||
$translationPrefix = $data['translationPrefix'] ?? 'server/user.permissions.';
|
||||
$customDescriptions = $data['descriptions'] ?? [];
|
||||
|
||||
foreach ($data['permissions'] as $permission) {
|
||||
$options[$permission] = str($permission)->headline();
|
||||
$descriptions[$permission] = trans('server/user.permissions.' . $data['name'] . '_' . str($permission)->replace('-', '_'));
|
||||
|
||||
if (isset($customDescriptions[$permission])) {
|
||||
$descriptions[$permission] = $customDescriptions[$permission];
|
||||
} else {
|
||||
$descKey = $translationPrefix . $data['name'] . '_' . str($permission)->replace('-', '_');
|
||||
$descriptions[$permission] = trans()->has($descKey) ? trans($descKey) : null;
|
||||
}
|
||||
|
||||
$permissionsArray[$data['name']][] = $permission;
|
||||
}
|
||||
|
||||
$tabLabelKey = $translationPrefix . $data['name'];
|
||||
$groupDescKey = $translationPrefix . $data['name'] . '_desc';
|
||||
|
||||
$tabs[] = Tab::make($data['name'])
|
||||
->label(str($data['name'])->headline())
|
||||
->label($data['label'] ?? (trans()->has($tabLabelKey) ? trans($tabLabelKey) : str($data['name'])->headline()))
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/user.permissions.' . $data['name'] . '_desc'))
|
||||
->description($data['description'] ?? (trans()->has($groupDescKey) ? trans($groupDescKey) : null))
|
||||
->icon($data['icon'])
|
||||
->contained(false)
|
||||
->schema([
|
||||
@@ -123,6 +136,18 @@ class SubuserResource extends Resource
|
||||
),
|
||||
])
|
||||
->recordActions([
|
||||
DeleteAction::make()
|
||||
->label(trans('server/user.delete'))
|
||||
->hidden(fn (Subuser $subuser) => user()?->id === $subuser->user->id)
|
||||
->successNotificationTitle(null)
|
||||
->action(function (Subuser $subuser, SubuserDeletionService $subuserDeletionService) use ($server) {
|
||||
$subuserDeletionService->handle($subuser, $server);
|
||||
|
||||
Notification::make()
|
||||
->title(trans('server/user.notification_delete'))
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
EditAction::make()
|
||||
->label(trans('server/user.edit'))
|
||||
->hidden(fn (Subuser $subuser) => user()?->id === $subuser->user->id)
|
||||
@@ -202,19 +227,6 @@ class SubuserResource extends Resource
|
||||
|
||||
return $data;
|
||||
}),
|
||||
DeleteAction::make()
|
||||
->label(trans('server/user.delete'))
|
||||
->hidden(fn (Subuser $subuser) => user()?->id === $subuser->user->id)
|
||||
->authorize(fn () => user()?->can(SubuserPermission::UserDelete, $server))
|
||||
->successNotificationTitle(null)
|
||||
->action(function (Subuser $subuser, SubuserDeletionService $subuserDeletionService) use ($server) {
|
||||
$subuserDeletionService->handle($subuser, $server);
|
||||
|
||||
Notification::make()
|
||||
->title(trans('server/user.notification_delete'))
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
])
|
||||
->toolbarActions([
|
||||
CreateAction::make('invite')
|
||||
|
||||
@@ -74,7 +74,7 @@ class OAuthController extends Controller
|
||||
$email = $oauthUser->getEmail();
|
||||
|
||||
if (!$email) {
|
||||
return $this->errorRedirect('No email was linked to your account on the OAuth provider.');
|
||||
return $this->errorRedirect();
|
||||
}
|
||||
|
||||
$user = User::whereEmail($email)->first();
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Responses;
|
||||
|
||||
use App\Enums\CustomizationKey;
|
||||
use App\Models\User;
|
||||
use Filament\Auth\Http\Responses\Contracts\LoginResponse as LoginResponseContract;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Livewire\Features\SupportRedirects\Redirector;
|
||||
|
||||
class LoginResponse implements LoginResponseContract
|
||||
{
|
||||
public function toResponse($request): RedirectResponse|Redirector
|
||||
{
|
||||
/** @var User|null $user */
|
||||
$user = Filament::auth()->user();
|
||||
|
||||
if ($user?->getCustomization(CustomizationKey::RedirectToAdmin) && $user->canAccessPanel(Filament::getPanel('admin'))) {
|
||||
return redirect()->intended(Filament::getPanel('admin')->getUrl());
|
||||
}
|
||||
|
||||
return redirect()->intended(Filament::getUrl());
|
||||
}
|
||||
}
|
||||
@@ -34,10 +34,6 @@ class ProcessWebhook implements ShouldQueue
|
||||
$data = reset($data);
|
||||
}
|
||||
|
||||
if (is_object($data)) {
|
||||
$data = get_object_vars($data);
|
||||
}
|
||||
|
||||
if (is_string($data)) {
|
||||
$data = Arr::wrap(json_decode($data, true) ?? []);
|
||||
}
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Enums\TablerIcon;
|
||||
use App\Models\Node;
|
||||
use App\Services\Nodes\NodeJWTService;
|
||||
use App\Services\Servers\GetUserPermissionsService;
|
||||
use Filament\Support\Enums\IconSize;
|
||||
use Filament\Tables\View\Components\Columns\IconColumnComponent\IconComponent;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\ComponentAttributeBag;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Component;
|
||||
|
||||
use function Filament\Support\generate_icon_html;
|
||||
|
||||
class NodeClientConnectivity extends Component
|
||||
{
|
||||
#[Locked]
|
||||
public Node $node;
|
||||
|
||||
private GetUserPermissionsService $getUserPermissionsService;
|
||||
|
||||
private NodeJWTService $nodeJWTService;
|
||||
|
||||
public function boot(GetUserPermissionsService $getUserPermissionsService, NodeJWTService $nodeJWTService): void
|
||||
{
|
||||
$this->getUserPermissionsService = $getUserPermissionsService;
|
||||
$this->nodeJWTService = $nodeJWTService;
|
||||
}
|
||||
|
||||
public function render(): \Illuminate\Contracts\View\View
|
||||
{
|
||||
$httpUrl = $this->node->getConnectionAddress();
|
||||
|
||||
$wsUrl = null;
|
||||
$wsToken = null;
|
||||
|
||||
$server = $this->node->servers()->first();
|
||||
|
||||
if ($server) {
|
||||
$user = Auth::user();
|
||||
|
||||
$permissions = $this->getUserPermissionsService->handle($server, $user);
|
||||
|
||||
$wsToken = $this->nodeJWTService
|
||||
->setExpiresAt(now()->addMinute()->toImmutable())
|
||||
->setUser($user)
|
||||
->setClaims([
|
||||
'server_uuid' => $server->uuid,
|
||||
'permissions' => $permissions,
|
||||
])
|
||||
->handle($this->node, $user->id . $server->uuid)->toString();
|
||||
|
||||
$wsUrl = str_replace(['https://', 'http://'], ['wss://', 'ws://'], $this->node->getConnectionAddress());
|
||||
$wsUrl .= sprintf('/api/servers/%s/ws', $server->uuid);
|
||||
}
|
||||
|
||||
return view('livewire.node-client-connectivity', [
|
||||
'httpUrl' => $httpUrl,
|
||||
'wsUrl' => $wsUrl,
|
||||
'wsToken' => $wsToken,
|
||||
'loadingIcon' => $this->makeIcon(TablerIcon::WorldQuestion, 'warning', 'Checking...'),
|
||||
'offlineIcon' => $this->makeIcon(TablerIcon::WorldX, 'danger', 'Node is not reachable from your browser'),
|
||||
'onlineIcon' => $this->makeIcon(TablerIcon::WorldCheck, 'success', 'Node is reachable'),
|
||||
'warningIcon' => $this->makeIcon(TablerIcon::WorldExclamation, 'warning', 'Node is reachable, but WebSocket failed. Check reverse proxy config.'),
|
||||
'onlineNoWsIcon' => $this->makeIcon(TablerIcon::WorldCheck, 'success', 'Node is reachable (WebSocket not tested — no servers)'),
|
||||
]);
|
||||
}
|
||||
|
||||
private function makeIcon(TablerIcon $icon, string $color, string $tooltip): string
|
||||
{
|
||||
return generate_icon_html($icon, attributes: (new ComponentAttributeBag())
|
||||
->merge([
|
||||
'x-tooltip' => '{
|
||||
content: "' . $tooltip . '",
|
||||
theme: $store.theme,
|
||||
allowHTML: true,
|
||||
placement: "bottom",
|
||||
}',
|
||||
'style' => 'color: var(--dark-text, var(--text))',
|
||||
], escape: false)
|
||||
->color(IconComponent::class, $color), size: IconSize::Large)
|
||||
->toHtml();
|
||||
}
|
||||
|
||||
public function placeholder(): string
|
||||
{
|
||||
return generate_icon_html(TablerIcon::WorldQuestion, attributes: (new ComponentAttributeBag())
|
||||
->color(IconComponent::class, 'warning'), size: IconSize::Large)
|
||||
->toHtml();
|
||||
}
|
||||
}
|
||||
@@ -255,8 +255,6 @@ class Node extends Model implements Validatable
|
||||
|
||||
/**
|
||||
* Gets the servers associated with a node.
|
||||
*
|
||||
* @return HasMany<Server, $this>
|
||||
*/
|
||||
public function servers(): HasMany
|
||||
{
|
||||
@@ -360,7 +358,7 @@ class Node extends Model implements Validatable
|
||||
'disk_used' => 0,
|
||||
];
|
||||
|
||||
return cache()->flexible("nodes.$this->id.statistics", [5, 30], function () use ($default) {
|
||||
return cache()->remember("nodes.$this->id.statistics", now()->addSeconds(360), function () use ($default) {
|
||||
try {
|
||||
|
||||
$data = Http::daemon($this)
|
||||
|
||||
@@ -33,11 +33,17 @@ class Subuser extends Model implements Validatable
|
||||
*/
|
||||
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?: ?string, permissions: string[], translationPrefix?: string, label?: string, description?: string, descriptions?: array<string, string>}> */
|
||||
protected static array $customPermissions = [];
|
||||
|
||||
/** @param string[] $permissions */
|
||||
public static function registerCustomPermissions(string $name, array $permissions, ?string $icon = null, ?bool $hidden = null): void
|
||||
/**
|
||||
* @param string[] $permissions
|
||||
* @param ?string $label Custom label for the permission tab (overrides translation lookup)
|
||||
* @param ?string $description Custom description for the permission group (overrides translation lookup)
|
||||
* @param ?array<string, string> $descriptions Custom descriptions keyed by permission name (overrides translation lookup)
|
||||
* @param ?string $translationPrefix Translation prefix for looking up labels/descriptions (e.g. 'my-plugin::permissions.')
|
||||
*/
|
||||
public static function registerCustomPermissions(string $name, array $permissions, ?string $icon = null, ?bool $hidden = null, ?string $translationPrefix = null, ?string $label = null, ?string $description = null, ?array $descriptions = null): void
|
||||
{
|
||||
$customPermission = static::$customPermissions[$name] ?? [];
|
||||
|
||||
@@ -52,6 +58,22 @@ class Subuser extends Model implements Validatable
|
||||
$customPermission['hidden'] = $hidden;
|
||||
}
|
||||
|
||||
if (!is_null($translationPrefix)) {
|
||||
$customPermission['translationPrefix'] = $translationPrefix;
|
||||
}
|
||||
|
||||
if (!is_null($label)) {
|
||||
$customPermission['label'] = $label;
|
||||
}
|
||||
|
||||
if (!is_null($description)) {
|
||||
$customPermission['description'] = $description;
|
||||
}
|
||||
|
||||
if (!is_null($descriptions)) {
|
||||
$customPermission['descriptions'] = array_merge($customPermission['descriptions'] ?? [], $descriptions);
|
||||
}
|
||||
|
||||
static::$customPermissions[$name] = $customPermission;
|
||||
}
|
||||
|
||||
@@ -93,7 +115,7 @@ class Subuser extends Model implements Validatable
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/** @return array<array{name: string, hidden: bool, icon: string, permissions: string[]}> */
|
||||
/** @return array<array{name: string, hidden: bool, icon: string, permissions: string[], translationPrefix?: ?string, label?: ?string, description?: ?string, descriptions?: array<string, string>}> */
|
||||
public static function allPermissionData(): array
|
||||
{
|
||||
$allPermissions = [];
|
||||
@@ -106,22 +128,28 @@ class Subuser extends Model implements Validatable
|
||||
'hidden' => $subuserPermission->isHidden(),
|
||||
'icon' => $subuserPermission->getIcon(),
|
||||
'permissions' => array_merge($allPermissions[$group]['permissions'] ?? [], [$permission]),
|
||||
'translationPrefix' => null,
|
||||
'label' => null,
|
||||
'description' => null,
|
||||
'descriptions' => [],
|
||||
];
|
||||
}
|
||||
|
||||
foreach (static::$customPermissions as $customPermission) {
|
||||
$name = $customPermission['name'];
|
||||
|
||||
$groupData = $allPermissions[$name] ?? [];
|
||||
$existing = $allPermissions[$name] ?? null;
|
||||
|
||||
$groupData = [
|
||||
$allPermissions[$name] = [
|
||||
'name' => $name,
|
||||
'hidden' => $customPermission['hidden'] ?? $groupData['hidden'] ?? false,
|
||||
'icon' => $customPermission['icon'] ?? $groupData['icon'],
|
||||
'permissions' => array_unique(array_merge($groupData['permissions'] ?? [], $customPermission['permissions'])),
|
||||
'hidden' => $customPermission['hidden'] ?? ($existing !== null ? $existing['hidden'] : false),
|
||||
'icon' => $customPermission['icon'] ?? ($existing !== null ? $existing['icon'] : null),
|
||||
'permissions' => array_unique(array_merge($existing !== null ? $existing['permissions'] : [], $customPermission['permissions'])),
|
||||
'translationPrefix' => ($customPermission['translationPrefix'] ?? null) ?? ($existing !== null ? $existing['translationPrefix'] : null),
|
||||
'label' => ($customPermission['label'] ?? null) ?? ($existing !== null ? $existing['label'] : null),
|
||||
'description' => ($customPermission['description'] ?? null) ?? ($existing !== null ? $existing['description'] : null),
|
||||
'descriptions' => array_merge($existing !== null ? $existing['descriptions'] : [], $customPermission['descriptions'] ?? []),
|
||||
];
|
||||
|
||||
$allPermissions[$name] = $groupData;
|
||||
}
|
||||
|
||||
return array_values($allPermissions);
|
||||
|
||||
@@ -23,16 +23,14 @@ class AccountCreated extends Notification implements ShouldQueue
|
||||
|
||||
public function toMail(User $notifiable): MailMessage
|
||||
{
|
||||
$locale = $notifiable->language ?? 'en';
|
||||
|
||||
$message = (new MailMessage())
|
||||
->greeting(trans('mail.greeting', ['name' => $notifiable->username], $locale))
|
||||
->line(trans('mail.account_created.body', ['app' => config('app.name')], $locale))
|
||||
->line(trans('mail.account_created.username', ['username' => $notifiable->username], $locale))
|
||||
->line(trans('mail.account_created.email', ['email' => $notifiable->email], $locale));
|
||||
->greeting('Hello ' . $notifiable->username . '!')
|
||||
->line('You are receiving this email because an account has been created for you on ' . config('app.name') . '.')
|
||||
->line('Username: ' . $notifiable->username)
|
||||
->line('Email: ' . $notifiable->email);
|
||||
|
||||
if (!is_null($this->token)) {
|
||||
return $message->action(trans('mail.account_created.action', locale: $locale), Filament::getPanel('app')->getResetPasswordUrl($this->token, $notifiable));
|
||||
return $message->action('Setup Your Account', Filament::getPanel('app')->getResetPasswordUrl($this->token, $notifiable));
|
||||
}
|
||||
|
||||
return $message;
|
||||
|
||||
@@ -24,12 +24,10 @@ class AddedToServer extends Notification implements ShouldQueue
|
||||
|
||||
public function toMail(User $notifiable): MailMessage
|
||||
{
|
||||
$locale = $notifiable->language ?? 'en';
|
||||
|
||||
return (new MailMessage())
|
||||
->greeting(trans('mail.greeting', ['name' => $notifiable->username], $locale))
|
||||
->line(trans('mail.added_to_server.body', locale: $locale))
|
||||
->line(trans('mail.added_to_server.server_name', ['name' => $this->server->name], $locale))
|
||||
->action(trans('mail.added_to_server.action', locale: $locale), Console::getUrl(panel: 'server', tenant: $this->server));
|
||||
->greeting('Hello ' . $notifiable->username . '!')
|
||||
->line('You have been added as a subuser for the following server, allowing you certain control over the server.')
|
||||
->line('Server Name: ' . $this->server->name)
|
||||
->action('Visit Server', Console::getUrl(panel: 'server', tenant: $this->server));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,11 +20,9 @@ class MailTested extends Notification
|
||||
|
||||
public function toMail(): MailMessage
|
||||
{
|
||||
$locale = $this->user->language ?? 'en';
|
||||
|
||||
return (new MailMessage())
|
||||
->subject(trans('mail.mail_tested.subject', locale: $locale))
|
||||
->greeting(trans('mail.greeting', ['name' => $this->user->username], $locale))
|
||||
->line(trans('mail.mail_tested.body', locale: $locale));
|
||||
->subject('Panel Test Message')
|
||||
->greeting('Hello ' . $this->user->username . '!')
|
||||
->line('This is a test of the Panel mail system. You\'re good to go!');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,13 +23,11 @@ class RemovedFromServer extends Notification implements ShouldQueue
|
||||
|
||||
public function toMail(User $notifiable): MailMessage
|
||||
{
|
||||
$locale = $notifiable->language ?? 'en';
|
||||
|
||||
return (new MailMessage())
|
||||
->error()
|
||||
->greeting(trans('mail.greeting', ['name' => $notifiable->username], $locale))
|
||||
->line(trans('mail.removed_from_server.body', locale: $locale))
|
||||
->line(trans('mail.removed_from_server.server_name', ['name' => $this->server->name], $locale))
|
||||
->action(trans('mail.removed_from_server.action', locale: $locale), url(''));
|
||||
->greeting('Hello ' . $notifiable->username . '.')
|
||||
->line('You have been removed as a subuser for the following server.')
|
||||
->line('Server Name: ' . $this->server->name)
|
||||
->action('Visit Panel', url(''));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,12 +24,10 @@ class ServerInstalled extends Notification implements ShouldQueue
|
||||
|
||||
public function toMail(User $notifiable): MailMessage
|
||||
{
|
||||
$locale = $notifiable->language ?? 'en';
|
||||
|
||||
return (new MailMessage())
|
||||
->greeting(trans('mail.greeting', ['name' => $notifiable->username], $locale))
|
||||
->line(trans('mail.server_installed.body', locale: $locale))
|
||||
->line(trans('mail.server_installed.server_name', ['name' => $this->server->name], $locale))
|
||||
->action(trans('mail.server_installed.action', locale: $locale), Console::getUrl(panel: 'server', tenant: $this->server));
|
||||
->greeting('Hello ' . $notifiable->username . '.')
|
||||
->line('Your server has finished installing and is now ready for you to use.')
|
||||
->line('Server Name: ' . $this->server->name)
|
||||
->action('Login and Begin Using', Console::getUrl(panel: 'server', tenant: $this->server));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ use App\Services\Helpers\SoftwareVersionService;
|
||||
use Dedoc\Scramble\Scramble;
|
||||
use Dedoc\Scramble\Support\Generator\OpenApi;
|
||||
use Dedoc\Scramble\Support\Generator\SecurityScheme;
|
||||
use Filament\Auth\Http\Responses\Contracts\LoginResponse as LoginResponseContract;
|
||||
use Illuminate\Config\Repository;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Foundation\Application;
|
||||
@@ -126,8 +125,6 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->bind(LoginResponseContract::class, \App\Http\Responses\LoginResponse::class);
|
||||
|
||||
Scramble::ignoreDefaultRoutes();
|
||||
|
||||
/** @var PluginService $pluginService */
|
||||
|
||||
@@ -29,9 +29,6 @@ abstract class PanelProvider extends BasePanelProvider
|
||||
{
|
||||
return $panel
|
||||
->spa(fn () => !request()->routeIs('filament.server.pages.console'))
|
||||
->spaUrlExceptions([
|
||||
'*/oauth/redirect/*',
|
||||
])
|
||||
->databaseNotifications()
|
||||
->brandName(config('app.name', 'Pelican'))
|
||||
->brandLogo(config('app.logo'))
|
||||
|
||||
@@ -32,10 +32,10 @@ class EggConfigurationService
|
||||
*/
|
||||
public function handle(Server $server): array
|
||||
{
|
||||
$configFiles = json_decode($server->egg->inherit_config_files ?? '{}');
|
||||
$configs = is_object($configFiles) || is_array($configFiles)
|
||||
? $this->replacePlaceholders($server, $configFiles)
|
||||
: [];
|
||||
$configs = $this->replacePlaceholders(
|
||||
$server,
|
||||
json_decode($server->egg->inherit_config_files)
|
||||
);
|
||||
|
||||
return [
|
||||
'startup' => $this->convertStartupToNewFormat(json_decode($server->egg->inherit_config_startup, true)),
|
||||
|
||||
@@ -92,7 +92,7 @@ class EggExporterService
|
||||
return $this->yamlExport($decoded);
|
||||
}
|
||||
|
||||
return str_replace("\r\n", "\n", $data);
|
||||
return str_replace(["\r\n", '\\r\\n', '\\n'], "\n", $data);
|
||||
}
|
||||
|
||||
if (is_array($data)) {
|
||||
|
||||
@@ -189,18 +189,6 @@ class EggImporterService
|
||||
}
|
||||
}
|
||||
|
||||
// Convert YAML booleans to strings to prevent Laravel from converting them to 1/0
|
||||
// when saving to TEXT field. Required for validation rules like "in:true,false".
|
||||
if (isset($parsed['variables'])) {
|
||||
$parsed['variables'] = array_map(function ($variable) {
|
||||
if (isset($variable['default_value']) && is_bool($variable['default_value'])) {
|
||||
$variable['default_value'] = $variable['default_value'] ? 'true' : 'false';
|
||||
}
|
||||
|
||||
return $variable;
|
||||
}, $parsed['variables']);
|
||||
}
|
||||
|
||||
// Reserved env var name handling
|
||||
[$forbidden, $allowed] = collect($parsed['variables'])
|
||||
->map(fn ($variable) => array_merge(
|
||||
|
||||
@@ -31,22 +31,13 @@ class GetUserPermissionsService
|
||||
'admin.websocket.transfer',
|
||||
];
|
||||
|
||||
if ($isAdmin && ($isOwner || $user->can('update', $server))) {
|
||||
return array_merge(['*'], $adminPermissions);
|
||||
if ($isAdmin) {
|
||||
return $isOwner || $user->can('update', $server) ? array_merge(['*'], $adminPermissions) : array_merge([SubuserPermission::WebsocketConnect->value], $adminPermissions);
|
||||
}
|
||||
|
||||
/** @var Subuser|null $subuser */
|
||||
$subuser = $server->subusers()->where('user_id', $user->id)->first();
|
||||
$subuserPermissions = $subuser !== null ? $subuser->permissions : [];
|
||||
|
||||
if ($isAdmin) {
|
||||
return array_unique(array_merge(
|
||||
[SubuserPermission::WebsocketConnect->value],
|
||||
$adminPermissions,
|
||||
$subuserPermissions,
|
||||
));
|
||||
}
|
||||
|
||||
return $subuserPermissions;
|
||||
return $subuser->permissions ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class EggTransformer extends BaseTransformer
|
||||
{
|
||||
$model->loadMissing('configFrom');
|
||||
|
||||
$files = json_decode($model->inherit_config_files ?: '{}', true, 512, JSON_THROW_ON_ERROR);
|
||||
$files = json_decode($model->inherit_config_files, true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
$model->loadMissing('scriptFrom');
|
||||
|
||||
@@ -53,9 +53,9 @@ class EggTransformer extends BaseTransformer
|
||||
'docker_images' => $model->docker_images,
|
||||
'config' => [
|
||||
'files' => $files,
|
||||
'startup' => json_decode($model->inherit_config_startup ?: '{}', true),
|
||||
'startup' => json_decode($model->inherit_config_startup, true),
|
||||
'stop' => $model->inherit_config_stop,
|
||||
'logs' => json_decode($model->inherit_config_logs ?: '{}', true),
|
||||
'logs' => json_decode($model->inherit_config_logs, true),
|
||||
'file_denylist' => $model->inherit_file_denylist,
|
||||
'extends' => $model->config_from,
|
||||
],
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"filament/filament": "^4.5",
|
||||
"gboquizosanchez/filament-log-viewer": "^2.1",
|
||||
"guzzlehttp/guzzle": "^7.10",
|
||||
"laravel/framework": "^12.51",
|
||||
"laravel/framework": "^12.49",
|
||||
"laravel/helpers": "^1.8",
|
||||
"laravel/sanctum": "^4.2",
|
||||
"laravel/socialite": "^5.24",
|
||||
|
||||
479
composer.lock
generated
479
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,6 @@ return [
|
||||
],
|
||||
'table' => [
|
||||
'health' => 'Health',
|
||||
'reachable' => 'Reachable',
|
||||
'name' => 'Name',
|
||||
'address' => 'Address',
|
||||
'public' => 'Public',
|
||||
@@ -40,7 +39,6 @@ return [
|
||||
'ip_help' => 'Usually your machine\'s public IP unless you are port forwarding.',
|
||||
'alias_help' => 'Optional display name to help you remember what these are.',
|
||||
'refresh' => 'Refresh',
|
||||
'custom_ip' => 'Enter Custom IP',
|
||||
'domain' => 'Domain Name',
|
||||
'ssl_ip' => 'You cannot connect to an IP Address over SSL',
|
||||
'error' => 'This is the domain name that points to your node\'s IP Address. If you\'ve already set up this, you can verify it by checking the next field!',
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'greeting' => 'Hello :name!',
|
||||
|
||||
'account_created' => [
|
||||
'body' => 'You are receiving this email because an account has been created for you on :app.',
|
||||
'username' => 'Username: :username',
|
||||
'email' => 'Email: :email',
|
||||
'action' => 'Setup Your Account',
|
||||
],
|
||||
|
||||
'added_to_server' => [
|
||||
'body' => 'You have been added as a subuser for the following server, allowing you certain control over the server.',
|
||||
'server_name' => 'Server Name: :name',
|
||||
'action' => 'Visit Server',
|
||||
],
|
||||
|
||||
'removed_from_server' => [
|
||||
'body' => 'You have been removed as a subuser for the following server.',
|
||||
'server_name' => 'Server Name: :name',
|
||||
'action' => 'Visit Panel',
|
||||
],
|
||||
|
||||
'server_installed' => [
|
||||
'body' => 'Your server has finished installing and is now ready for you to use.',
|
||||
'server_name' => 'Server Name: :name',
|
||||
'action' => 'Login and Begin Using',
|
||||
],
|
||||
|
||||
'mail_tested' => [
|
||||
'subject' => 'Panel Test Message',
|
||||
'body' => 'This is a test of the Panel mail system. You\'re good to go!',
|
||||
],
|
||||
];
|
||||
@@ -64,8 +64,6 @@ return [
|
||||
'sidebar' => 'Sidebar',
|
||||
'topbar' => 'Topbar',
|
||||
'mixed' => 'Mixed',
|
||||
'redirect_to_admin' => 'Redirect to Admin on Login',
|
||||
'redirect_to_admin_help' => 'When enabled, you will be redirected to the admin area after logging in instead of the server list.',
|
||||
'no_oauth' => 'No Accounts Linked',
|
||||
'no_api_keys' => 'No API Keys',
|
||||
'no_ssh_keys' => 'No SSH Keys',
|
||||
|
||||
@@ -17,6 +17,16 @@ return [
|
||||
'notification_failed' => 'Failed to invite user!',
|
||||
'permissions' => [
|
||||
'title' => 'Permissions',
|
||||
'control' => 'Control',
|
||||
'user' => 'User',
|
||||
'file' => 'File',
|
||||
'backup' => 'Backup',
|
||||
'schedule' => 'Schedule',
|
||||
'database' => 'Database',
|
||||
'allocation' => 'Allocation',
|
||||
'startup' => 'Startup',
|
||||
'settings' => 'Settings',
|
||||
'activity' => 'Activity',
|
||||
'activity_desc' => 'Permissions that control a user\'s access to the server activity logs.',
|
||||
'startup_desc' => 'Permissions that control a user\'s ability to view this server\'s startup parameters.',
|
||||
'settings_desc' => 'Permissions that control a user\'s ability to modify this server\'s settings.',
|
||||
|
||||
@@ -5,10 +5,6 @@
|
||||
|
||||
RewriteEngine On
|
||||
|
||||
# Handle X-Forwarded-Proto Header
|
||||
RewriteCond %{HTTP:X-Forwarded-Proto} =https [NC]
|
||||
RewriteRule .* - [E=HTTPS:on]
|
||||
|
||||
# Handle Authorization Header
|
||||
RewriteCond %{HTTP:Authorization} .
|
||||
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
(()=>{var n=({livewireId:e})=>({actionNestingIndex:null,init(){window.addEventListener("sync-action-modals",t=>{t.detail.id===e&&this.syncActionModals(t.detail.newActionNestingIndex,t.detail.shouldOverlayParentActions??!1)})},syncActionModals(t,i=!1){if(this.actionNestingIndex===t){this.actionNestingIndex!==null&&this.$nextTick(()=>this.openModal());return}let s=this.actionNestingIndex!==null&&t!==null&&t>this.actionNestingIndex;if(this.actionNestingIndex!==null&&!(i&&s)&&this.closeModal(),this.actionNestingIndex=t,this.actionNestingIndex!==null){if(!this.$el.querySelector(`#${this.generateModalId(t)}`)){this.$nextTick(()=>this.openModal());return}this.openModal()}},generateModalId(t){return`fi-${e}-action-`+t},openModal(){let t=this.generateModalId(this.actionNestingIndex);document.dispatchEvent(new CustomEvent("open-modal",{bubbles:!0,composed:!0,detail:{id:t}}))},closeModal(){let t=this.generateModalId(this.actionNestingIndex);document.dispatchEvent(new CustomEvent("close-modal-quietly",{bubbles:!0,composed:!0,detail:{id:t}}))}});document.addEventListener("alpine:init",()=>{window.Alpine.data("filamentActionModals",n)});})();
|
||||
(()=>{var n=({livewireId:e})=>({actionNestingIndex:null,init(){window.addEventListener("sync-action-modals",t=>{t.detail.id===e&&this.syncActionModals(t.detail.newActionNestingIndex)})},syncActionModals(t){if(this.actionNestingIndex===t){this.actionNestingIndex!==null&&this.$nextTick(()=>this.openModal());return}if(this.actionNestingIndex!==null&&this.closeModal(),this.actionNestingIndex=t,this.actionNestingIndex!==null){if(!this.$el.querySelector(`#${this.generateModalId(t)}`)){this.$nextTick(()=>this.openModal());return}this.openModal()}},generateModalId(t){return`fi-${e}-action-`+t},openModal(){let t=this.generateModalId(this.actionNestingIndex);document.dispatchEvent(new CustomEvent("open-modal",{bubbles:!0,composed:!0,detail:{id:t}}))},closeModal(){let t=this.generateModalId(this.actionNestingIndex);document.dispatchEvent(new CustomEvent("close-modal-quietly",{bubbles:!0,composed:!0,detail:{id:t}}))}});document.addEventListener("alpine:init",()=>{window.Alpine.data("filamentActionModals",n)});})();
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
function c({livewireId:s}){return{areAllCheckboxesChecked:!1,checkboxListOptions:[],search:"",unsubscribeLivewireHook:null,visibleCheckboxListOptions:[],init(){this.checkboxListOptions=Array.from(this.$root.querySelectorAll(".fi-fo-checkbox-list-option")),this.updateVisibleCheckboxListOptions(),this.$nextTick(()=>{this.checkIfAllCheckboxesAreChecked()}),this.unsubscribeLivewireHook=Livewire.hook("commit",({component:e,commit:t,succeed:i,fail:o,respond:h})=>{i(({snapshot:r,effect:l})=>{this.$nextTick(()=>{e.id===s&&(this.checkboxListOptions=Array.from(this.$root.querySelectorAll(".fi-fo-checkbox-list-option")),this.updateVisibleCheckboxListOptions(),this.checkIfAllCheckboxesAreChecked())})})}),this.$watch("search",()=>{this.updateVisibleCheckboxListOptions(),this.checkIfAllCheckboxesAreChecked()})},checkIfAllCheckboxesAreChecked(){this.areAllCheckboxesChecked=this.visibleCheckboxListOptions.length===this.visibleCheckboxListOptions.filter(e=>e.querySelector("input[type=checkbox]:checked, input[type=checkbox]:disabled")).length},toggleAllCheckboxes(){this.checkIfAllCheckboxesAreChecked();let e=!this.areAllCheckboxesChecked;this.visibleCheckboxListOptions.forEach(t=>{let i=t.querySelector("input[type=checkbox]");i.disabled||i.checked!==e&&(i.checked=e,i.dispatchEvent(new Event("change")))}),this.areAllCheckboxesChecked=e},updateVisibleCheckboxListOptions(){this.visibleCheckboxListOptions=this.checkboxListOptions.filter(e=>["",null,void 0].includes(this.search)||e.querySelector(".fi-fo-checkbox-list-option-label")?.innerText.toLowerCase().includes(this.search.toLowerCase())?!0:e.querySelector(".fi-fo-checkbox-list-option-description")?.innerText.toLowerCase().includes(this.search.toLowerCase()))},destroy(){this.unsubscribeLivewireHook?.()}}}export{c as default};
|
||||
function c({livewireId:s}){return{areAllCheckboxesChecked:!1,checkboxListOptions:[],search:"",visibleCheckboxListOptions:[],init(){this.checkboxListOptions=Array.from(this.$root.querySelectorAll(".fi-fo-checkbox-list-option")),this.updateVisibleCheckboxListOptions(),this.$nextTick(()=>{this.checkIfAllCheckboxesAreChecked()}),Livewire.hook("commit",({component:e,commit:t,succeed:i,fail:o,respond:h})=>{i(({snapshot:r,effect:l})=>{this.$nextTick(()=>{e.id===s&&(this.checkboxListOptions=Array.from(this.$root.querySelectorAll(".fi-fo-checkbox-list-option")),this.updateVisibleCheckboxListOptions(),this.checkIfAllCheckboxesAreChecked())})})}),this.$watch("search",()=>{this.updateVisibleCheckboxListOptions(),this.checkIfAllCheckboxesAreChecked()})},checkIfAllCheckboxesAreChecked(){this.areAllCheckboxesChecked=this.visibleCheckboxListOptions.length===this.visibleCheckboxListOptions.filter(e=>e.querySelector("input[type=checkbox]:checked, input[type=checkbox]:disabled")).length},toggleAllCheckboxes(){this.checkIfAllCheckboxesAreChecked();let e=!this.areAllCheckboxesChecked;this.visibleCheckboxListOptions.forEach(t=>{let i=t.querySelector("input[type=checkbox]");i.disabled||i.checked!==e&&(i.checked=e,i.dispatchEvent(new Event("change")))}),this.areAllCheckboxesChecked=e},updateVisibleCheckboxListOptions(){this.visibleCheckboxListOptions=this.checkboxListOptions.filter(e=>["",null,void 0].includes(this.search)||e.querySelector(".fi-fo-checkbox-list-option-label")?.innerText.toLowerCase().includes(this.search.toLowerCase())?!0:e.querySelector(".fi-fo-checkbox-list-option-description")?.innerText.toLowerCase().includes(this.search.toLowerCase()))}}}export{c as default};
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
function a({state:r}){return{state:r,rows:[],init(){this.updateRows(),this.rows.length<=0?this.rows.push({key:"",value:""}):this.updateState(),this.$watch("state",(e,t)=>{if(!Array.isArray(e))return;let s=i=>i===null?0:Array.isArray(i)?i.length:typeof i!="object"?0:Object.keys(i).length;s(e)===0&&s(t)===0||this.updateRows()})},addRow(){this.rows.push({key:"",value:""}),this.updateState()},deleteRow(e){this.rows.splice(e,1),this.rows.length<=0&&this.addRow(),this.updateState()},reorderRows(e){let t=Alpine.raw(this.rows);this.rows=[];let s=t.splice(e.oldIndex,1)[0];t.splice(e.newIndex,0,s),this.$nextTick(()=>{this.rows=t,this.updateState()})},updateRows(){let t=Alpine.raw(this.state).map(({key:s,value:i})=>({key:s,value:i}));this.rows.forEach(s=>{(s.key===""||s.key===null)&&t.push({key:"",value:s.value})}),this.rows=t},updateState(){let e=[];this.rows.forEach(t=>{t.key===""||t.key===null||e.push({key:t.key,value:t.value})}),JSON.stringify(this.state)!==JSON.stringify(e)&&(this.state=e)}}}export{a as default};
|
||||
function h({state:r}){return{state:r,rows:[],init(){this.updateRows(),this.rows.length<=0?this.rows.push({key:"",value:""}):this.updateState(),this.$watch("state",(e,t)=>{let s=i=>i===null?0:Array.isArray(i)?i.length:typeof i!="object"?0:Object.keys(i).length;s(e)===0&&s(t)===0||this.updateRows()})},addRow(){this.rows.push({key:"",value:""}),this.updateState()},deleteRow(e){this.rows.splice(e,1),this.rows.length<=0&&this.addRow(),this.updateState()},reorderRows(e){let t=Alpine.raw(this.rows);this.rows=[];let s=t.splice(e.oldIndex,1)[0];t.splice(e.newIndex,0,s),this.$nextTick(()=>{this.rows=t,this.updateState()})},updateRows(){let t=Alpine.raw(this.state).map(({key:s,value:i})=>({key:s,value:i}));this.rows.forEach(s=>{(s.key===""||s.key===null)&&t.push({key:"",value:s.value})}),this.rows=t},updateState(){let e=[];this.rows.forEach(t=>{t.key===""||t.key===null||e.push({key:t.key,value:t.value})}),JSON.stringify(this.state)!==JSON.stringify(e)&&(this.state=e)}}}export{h as default};
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
function I({activeTab:w,isScrollable:f,isTabPersistedInQueryString:m,livewireId:g,tab:T,tabQueryStringKey:c}){return{boundResizeHandler:null,isScrollable:f,resizeDebounceTimer:null,tab:T,unsubscribeLivewireHook:null,withinDropdownIndex:null,withinDropdownMounted:!1,init(){let t=this.getTabs(),e=new URLSearchParams(window.location.search);m&&e.has(c)&&t.includes(e.get(c))&&(this.tab=e.get(c)),this.$watch("tab",()=>this.updateQueryString()),(!this.tab||!t.includes(this.tab))&&(this.tab=t[w-1]),this.unsubscribeLivewireHook=Livewire.hook("commit",({component:n,commit:d,succeed:r,fail:h,respond:u})=>{r(({snapshot:b,effect:i})=>{this.$nextTick(()=>{if(n.id!==g)return;let a=this.getTabs();a.includes(this.tab)||(this.tab=a[w-1]??this.tab)})})}),f||(this.boundResizeHandler=this.debouncedUpdateTabsWithinDropdown.bind(this),window.addEventListener("resize",this.boundResizeHandler),this.updateTabsWithinDropdown())},calculateAvailableWidth(t){let e=window.getComputedStyle(t);return Math.floor(t.clientWidth)-Math.ceil(parseFloat(e.paddingLeft))*2},calculateContainerGap(t){let e=window.getComputedStyle(t);return Math.ceil(parseFloat(e.columnGap))},calculateDropdownIconWidth(t){let e=t.querySelector(".fi-icon");return Math.ceil(e.clientWidth)},calculateTabItemGap(t){let e=window.getComputedStyle(t);return Math.ceil(parseFloat(e.columnGap)||8)},calculateTabItemPadding(t){let e=window.getComputedStyle(t);return Math.ceil(parseFloat(e.paddingLeft))+Math.ceil(parseFloat(e.paddingRight))},findOverflowIndex(t,e,n,d,r,h){let u=t.map(i=>Math.ceil(i.clientWidth)),b=t.map(i=>{let a=i.querySelector(".fi-tabs-item-label"),s=i.querySelector(".fi-badge"),o=Math.ceil(a.clientWidth),l=s?Math.ceil(s.clientWidth):0;return{label:o,badge:l,total:o+(l>0?d+l:0)}});for(let i=0;i<t.length;i++){let a=u.slice(0,i+1).reduce((p,y)=>p+y,0),s=i*n,o=b.slice(i+1),l=o.length>0,W=l?Math.max(...o.map(p=>p.total)):0,D=l?r+W+d+h+n:0;if(a+s+D>e)return i}return-1},get isDropdownButtonVisible(){return this.withinDropdownMounted?this.withinDropdownIndex===null?!1:this.getTabs().findIndex(e=>e===this.tab)<this.withinDropdownIndex:!0},getTabs(){return this.$refs.tabsData?JSON.parse(this.$refs.tabsData.value):[]},updateQueryString(){if(!m)return;let t=new URL(window.location.href);t.searchParams.set(c,this.tab),history.replaceState(null,document.title,t.toString())},debouncedUpdateTabsWithinDropdown(){clearTimeout(this.resizeDebounceTimer),this.resizeDebounceTimer=setTimeout(()=>this.updateTabsWithinDropdown(),150)},async updateTabsWithinDropdown(){this.withinDropdownIndex=null,this.withinDropdownMounted=!1,await this.$nextTick();let t=this.$el.querySelector(".fi-tabs"),e=t.querySelector(".fi-tabs-item:last-child"),n=Array.from(t.children).slice(0,-1),d=n.map(s=>s.style.display);n.forEach(s=>s.style.display=""),t.offsetHeight;let r=this.calculateAvailableWidth(t),h=this.calculateContainerGap(t),u=this.calculateDropdownIconWidth(e),b=this.calculateTabItemGap(n[0]),i=this.calculateTabItemPadding(n[0]),a=this.findOverflowIndex(n,r,h,b,i,u);n.forEach((s,o)=>s.style.display=d[o]),a!==-1&&(this.withinDropdownIndex=a),this.withinDropdownMounted=!0},destroy(){this.unsubscribeLivewireHook?.(),this.boundResizeHandler&&window.removeEventListener("resize",this.boundResizeHandler),clearTimeout(this.resizeDebounceTimer)}}}export{I as default};
|
||||
function I({activeTab:w,isScrollable:f,isTabPersistedInQueryString:m,livewireId:g,tab:T,tabQueryStringKey:c}){return{boundResizeHandler:null,isScrollable:f,resizeDebounceTimer:null,tab:T,withinDropdownIndex:null,withinDropdownMounted:!1,init(){let t=this.getTabs(),e=new URLSearchParams(window.location.search);m&&e.has(c)&&t.includes(e.get(c))&&(this.tab=e.get(c)),this.$watch("tab",()=>this.updateQueryString()),(!this.tab||!t.includes(this.tab))&&(this.tab=t[w-1]),Livewire.hook("commit",({component:n,commit:d,succeed:r,fail:h,respond:u})=>{r(({snapshot:p,effect:i})=>{this.$nextTick(()=>{if(n.id!==g)return;let s=this.getTabs();s.includes(this.tab)||(this.tab=s[w-1]??this.tab)})})}),f||(this.boundResizeHandler=this.debouncedUpdateTabsWithinDropdown.bind(this),window.addEventListener("resize",this.boundResizeHandler),this.updateTabsWithinDropdown())},calculateAvailableWidth(t){let e=window.getComputedStyle(t);return Math.floor(t.clientWidth)-Math.ceil(parseFloat(e.paddingLeft))*2},calculateContainerGap(t){let e=window.getComputedStyle(t);return Math.ceil(parseFloat(e.columnGap))},calculateDropdownIconWidth(t){let e=t.querySelector(".fi-icon");return Math.ceil(e.clientWidth)},calculateTabItemGap(t){let e=window.getComputedStyle(t);return Math.ceil(parseFloat(e.columnGap)||8)},calculateTabItemPadding(t){let e=window.getComputedStyle(t);return Math.ceil(parseFloat(e.paddingLeft))+Math.ceil(parseFloat(e.paddingRight))},findOverflowIndex(t,e,n,d,r,h){let u=t.map(i=>Math.ceil(i.clientWidth)),p=t.map(i=>{let s=i.querySelector(".fi-tabs-item-label"),a=i.querySelector(".fi-badge"),o=Math.ceil(s.clientWidth),l=a?Math.ceil(a.clientWidth):0;return{label:o,badge:l,total:o+(l>0?d+l:0)}});for(let i=0;i<t.length;i++){let s=u.slice(0,i+1).reduce((b,y)=>b+y,0),a=i*n,o=p.slice(i+1),l=o.length>0,W=l?Math.max(...o.map(b=>b.total)):0,D=l?r+W+d+h+n:0;if(s+a+D>e)return i}return-1},get isDropdownButtonVisible(){return this.withinDropdownMounted?this.withinDropdownIndex===null?!1:this.getTabs().findIndex(e=>e===this.tab)<this.withinDropdownIndex:!0},getTabs(){return this.$refs.tabsData?JSON.parse(this.$refs.tabsData.value):[]},updateQueryString(){if(!m)return;let t=new URL(window.location.href);t.searchParams.set(c,this.tab),history.replaceState(null,document.title,t.toString())},debouncedUpdateTabsWithinDropdown(){clearTimeout(this.resizeDebounceTimer),this.resizeDebounceTimer=setTimeout(()=>this.updateTabsWithinDropdown(),150)},async updateTabsWithinDropdown(){this.withinDropdownIndex=null,this.withinDropdownMounted=!1,await this.$nextTick();let t=this.$el.querySelector(".fi-tabs"),e=t.querySelector(".fi-tabs-item:last-child"),n=Array.from(t.children).slice(0,-1),d=n.map(a=>a.style.display);n.forEach(a=>a.style.display=""),t.offsetHeight;let r=this.calculateAvailableWidth(t),h=this.calculateContainerGap(t),u=this.calculateDropdownIconWidth(e),p=this.calculateTabItemGap(n[0]),i=this.calculateTabItemPadding(n[0]),s=this.findOverflowIndex(n,r,h,p,i,u);n.forEach((a,o)=>a.style.display=d[o]),s!==-1&&(this.withinDropdownIndex=s),this.withinDropdownMounted=!0},destroy(){this.boundResizeHandler&&window.removeEventListener("resize",this.boundResizeHandler),clearTimeout(this.resizeDebounceTimer)}}}export{I as default};
|
||||
|
||||
@@ -1 +1 @@
|
||||
function o({name:r,recordKey:s,state:n}){return{error:void 0,isLoading:!1,state:n,unsubscribeLivewireHook:null,init(){this.unsubscribeLivewireHook=Livewire.hook("commit",({component:e,commit:i,succeed:a,fail:u,respond:h})=>{a(({snapshot:d,effect:f})=>{this.$nextTick(()=>{if(this.isLoading||e.id!==this.$root.closest("[wire\\:id]")?.attributes["wire:id"].value)return;let t=this.getServerState();t===void 0||Alpine.raw(this.state)===t||(this.state=t)})})}),this.$watch("state",async()=>{let e=this.getServerState();if(e===void 0||Alpine.raw(this.state)===e)return;this.isLoading=!0;let i=await this.$wire.updateTableColumnState(r,s,this.state);this.error=i?.error??void 0,!this.error&&this.$refs.serverState&&(this.$refs.serverState.value=this.state?"1":"0"),this.isLoading=!1})},getServerState(){if(this.$refs.serverState)return[1,"1"].includes(this.$refs.serverState.value)},destroy(){this.unsubscribeLivewireHook?.()}}}export{o as default};
|
||||
function o({name:i,recordKey:s,state:a}){return{error:void 0,isLoading:!1,state:a,init(){Livewire.hook("commit",({component:e,commit:r,succeed:n,fail:h,respond:u})=>{n(({snapshot:f,effect:d})=>{this.$nextTick(()=>{if(this.isLoading||e.id!==this.$root.closest("[wire\\:id]")?.attributes["wire:id"].value)return;let t=this.getServerState();t===void 0||Alpine.raw(this.state)===t||(this.state=t)})})}),this.$watch("state",async()=>{let e=this.getServerState();if(e===void 0||Alpine.raw(this.state)===e)return;this.isLoading=!0;let r=await this.$wire.updateTableColumnState(i,s,this.state);this.error=r?.error??void 0,!this.error&&this.$refs.serverState&&(this.$refs.serverState.value=this.state?"1":"0"),this.isLoading=!1})},getServerState(){if(this.$refs.serverState)return[1,"1"].includes(this.$refs.serverState.value)}}}export{o as default};
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
function o({name:i,recordKey:s,state:n}){return{error:void 0,isLoading:!1,state:n,unsubscribeLivewireHook:null,init(){this.unsubscribeLivewireHook=Livewire.hook("commit",({component:e,commit:r,succeed:a,fail:u,respond:d})=>{a(({snapshot:h,effect:l})=>{this.$nextTick(()=>{if(this.isLoading||e.id!==this.$root.closest("[wire\\:id]")?.attributes["wire:id"].value)return;let t=this.getServerState();t===void 0||this.getNormalizedState()===t||(this.state=t)})})}),this.$watch("state",async()=>{let e=this.getServerState();if(e===void 0||this.getNormalizedState()===e)return;this.isLoading=!0;let r=await this.$wire.updateTableColumnState(i,s,this.state);this.error=r?.error??void 0,!this.error&&this.$refs.serverState&&(this.$refs.serverState.value=this.getNormalizedState()),this.isLoading=!1})},getServerState(){if(this.$refs.serverState)return[null,void 0].includes(this.$refs.serverState.value)?"":this.$refs.serverState.value.replaceAll('\\"','"')},getNormalizedState(){let e=Alpine.raw(this.state);return[null,void 0].includes(e)?"":e},destroy(){this.unsubscribeLivewireHook?.()}}}export{o as default};
|
||||
function o({name:i,recordKey:s,state:a}){return{error:void 0,isLoading:!1,state:a,init(){Livewire.hook("commit",({component:e,commit:r,succeed:n,fail:d,respond:u})=>{n(({snapshot:f,effect:h})=>{this.$nextTick(()=>{if(this.isLoading||e.id!==this.$root.closest("[wire\\:id]")?.attributes["wire:id"].value)return;let t=this.getServerState();t===void 0||this.getNormalizedState()===t||(this.state=t)})})}),this.$watch("state",async()=>{let e=this.getServerState();if(e===void 0||this.getNormalizedState()===e)return;this.isLoading=!0;let r=await this.$wire.updateTableColumnState(i,s,this.state);this.error=r?.error??void 0,!this.error&&this.$refs.serverState&&(this.$refs.serverState.value=this.getNormalizedState()),this.isLoading=!1})},getServerState(){if(this.$refs.serverState)return[null,void 0].includes(this.$refs.serverState.value)?"":this.$refs.serverState.value.replaceAll('\\"','"')},getNormalizedState(){let e=Alpine.raw(this.state);return[null,void 0].includes(e)?"":e}}}export{o as default};
|
||||
|
||||
@@ -1 +1 @@
|
||||
function o({name:r,recordKey:s,state:n}){return{error:void 0,isLoading:!1,state:n,unsubscribeLivewireHook:null,init(){this.unsubscribeLivewireHook=Livewire.hook("commit",({component:e,commit:i,succeed:a,fail:u,respond:h})=>{a(({snapshot:d,effect:f})=>{this.$nextTick(()=>{if(this.isLoading||e.id!==this.$root.closest("[wire\\:id]")?.attributes["wire:id"].value)return;let t=this.getServerState();t===void 0||Alpine.raw(this.state)===t||(this.state=t)})})}),this.$watch("state",async()=>{let e=this.getServerState();if(e===void 0||Alpine.raw(this.state)===e)return;this.isLoading=!0;let i=await this.$wire.updateTableColumnState(r,s,this.state);this.error=i?.error??void 0,!this.error&&this.$refs.serverState&&(this.$refs.serverState.value=this.state?"1":"0"),this.isLoading=!1})},getServerState(){if(this.$refs.serverState)return[1,"1"].includes(this.$refs.serverState.value)},destroy(){this.unsubscribeLivewireHook?.()}}}export{o as default};
|
||||
function o({name:i,recordKey:s,state:a}){return{error:void 0,isLoading:!1,state:a,init(){Livewire.hook("commit",({component:e,commit:r,succeed:n,fail:h,respond:u})=>{n(({snapshot:f,effect:d})=>{this.$nextTick(()=>{if(this.isLoading||e.id!==this.$root.closest("[wire\\:id]")?.attributes["wire:id"].value)return;let t=this.getServerState();t===void 0||Alpine.raw(this.state)===t||(this.state=t)})})}),this.$watch("state",async()=>{let e=this.getServerState();if(e===void 0||Alpine.raw(this.state)===e)return;this.isLoading=!0;let r=await this.$wire.updateTableColumnState(i,s,this.state);this.error=r?.error??void 0,!this.error&&this.$refs.serverState&&(this.$refs.serverState.value=this.state?"1":"0"),this.isLoading=!1})},getServerState(){if(this.$refs.serverState)return[1,"1"].includes(this.$refs.serverState.value)}}}export{o as default};
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,16 @@
|
||||
<x-filament-panels::page>
|
||||
@once
|
||||
<style>
|
||||
.files-selection-merged .fi-ta-header-ctn {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
</style>
|
||||
@endonce
|
||||
|
||||
<div
|
||||
x-data="
|
||||
{
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
<div
|
||||
x-data="{
|
||||
status: 'loading',
|
||||
async check() {
|
||||
try {
|
||||
await fetch('{{ $httpUrl }}', { mode: 'no-cors', signal: AbortSignal.timeout(5000) });
|
||||
} catch (e) {
|
||||
this.status = 'offline';
|
||||
return;
|
||||
}
|
||||
|
||||
@if ($wsUrl && $wsToken)
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
ws.close();
|
||||
reject(new Error('timeout'));
|
||||
}, 10000);
|
||||
|
||||
const ws = new WebSocket('{{ $wsUrl }}');
|
||||
|
||||
ws.onerror = () => {
|
||||
clearTimeout(timeout);
|
||||
ws.close();
|
||||
reject(new Error('ws_error'));
|
||||
};
|
||||
|
||||
ws.onopen = () => {
|
||||
ws.send(JSON.stringify({ event: 'auth', args: ['{{ $wsToken }}'] }));
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.event === 'auth success') {
|
||||
clearTimeout(timeout);
|
||||
ws.close();
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
});
|
||||
this.status = 'online';
|
||||
} catch (e) {
|
||||
this.status = 'warning';
|
||||
}
|
||||
@else
|
||||
this.status = 'online-no-ws';
|
||||
@endif
|
||||
}
|
||||
}"
|
||||
x-init="check()"
|
||||
>
|
||||
<div x-show="status === 'loading'" x-cloak>
|
||||
{!! $loadingIcon !!}
|
||||
</div>
|
||||
<div x-show="status === 'offline'" x-cloak>
|
||||
{!! $offlineIcon !!}
|
||||
</div>
|
||||
<div x-show="status === 'online'" x-cloak>
|
||||
{!! $onlineIcon !!}
|
||||
</div>
|
||||
<div x-show="status === 'warning'" x-cloak>
|
||||
{!! $warningIcon !!}
|
||||
</div>
|
||||
<div x-show="status === 'online-no-ws'" x-cloak>
|
||||
{!! $onlineNoWsIcon !!}
|
||||
</div>
|
||||
</div>
|
||||
@@ -11,7 +11,6 @@ use App\Models\User;
|
||||
use App\Models\UserSSHKey;
|
||||
use App\Tests\Integration\IntegrationTestCase;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
class SftpAuthenticationControllerTest extends IntegrationTestCase
|
||||
{
|
||||
@@ -196,39 +195,6 @@ class SftpAuthenticationControllerTest extends IntegrationTestCase
|
||||
$this->post('/api/remote/sftp/auth', $data)->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_subuser_sftp_works_when_user_has_view_only_role(): void
|
||||
{
|
||||
[$user, $server] = $this->generateTestAccount([SubuserPermission::FileRead, SubuserPermission::FileSftp]);
|
||||
|
||||
$user->update(['password' => password_hash('foobar', PASSWORD_DEFAULT)]);
|
||||
|
||||
$this->setAuthorization($server->node);
|
||||
|
||||
$data = [
|
||||
'username' => $user->username . '.' . $server->uuid_short,
|
||||
'password' => 'foobar',
|
||||
];
|
||||
|
||||
// SFTP works as a plain subuser
|
||||
$this->postJson('/api/remote/sftp/auth', $data)
|
||||
->assertOk()
|
||||
->assertJsonPath('permissions', [SubuserPermission::FileRead->value, SubuserPermission::FileSftp->value]);
|
||||
|
||||
// Assign a role with only "view server" permission
|
||||
$role = Role::findOrCreate('view-only-test', 'web');
|
||||
$permission = Permission::findOrCreate('view server', 'web');
|
||||
$role->givePermissionTo($permission);
|
||||
$user->syncRoles($role);
|
||||
|
||||
// SFTP should still work — subuser permissions must be merged with admin permissions
|
||||
$response = $this->postJson('/api/remote/sftp/auth', $data)
|
||||
->assertOk();
|
||||
|
||||
$permissions = $response->json('permissions');
|
||||
$this->assertContains(SubuserPermission::FileSftp->value, $permissions);
|
||||
$this->assertContains(SubuserPermission::FileRead->value, $permissions);
|
||||
}
|
||||
|
||||
public static function authorizationTypeDataProvider(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Unit\Services\Eggs;
|
||||
|
||||
use App\Services\Eggs\Sharing\EggExporterService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class EggExporterServiceTest extends TestCase
|
||||
{
|
||||
private EggExporterService $service;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->service = new EggExporterService();
|
||||
}
|
||||
|
||||
public function test_yaml_export_preserves_literal_backslash_n_in_scripts(): void
|
||||
{
|
||||
$script = <<<'BASH'
|
||||
if [[ "${STEAM_USER}" == "" ]] || [[ "${STEAM_PASS}" == "" ]]; then
|
||||
echo -e "steam user is not set.\n"
|
||||
echo -e "Using anonymous user.\n"
|
||||
STEAM_USER=anonymous
|
||||
STEAM_PASS=""
|
||||
STEAM_AUTH=""
|
||||
else
|
||||
echo -e "user set to ${STEAM_USER}"
|
||||
fi
|
||||
BASH;
|
||||
|
||||
$result = $this->callYamlExport($script);
|
||||
|
||||
$this->assertStringContainsString('echo -e "steam user is not set.\n"', $result);
|
||||
$this->assertStringContainsString('echo -e "Using anonymous user.\n"', $result);
|
||||
}
|
||||
|
||||
public function test_yaml_export_preserves_literal_backslash_r_backslash_n(): void
|
||||
{
|
||||
$script = 'echo -e "line ending\\r\\n"';
|
||||
|
||||
$result = $this->callYamlExport($script);
|
||||
|
||||
$this->assertSame($script, $result);
|
||||
}
|
||||
|
||||
public function test_yaml_export_normalizes_real_crlf_to_lf(): void
|
||||
{
|
||||
$script = "line one\r\nline two\r\nline three";
|
||||
|
||||
$result = $this->callYamlExport($script);
|
||||
|
||||
$this->assertSame("line one\nline two\nline three", $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the protected yamlExport method via reflection.
|
||||
*/
|
||||
private function callYamlExport(mixed $data): mixed
|
||||
{
|
||||
$reflection = new \ReflectionMethod($this->service, 'yamlExport');
|
||||
|
||||
return $reflection->invoke($this->service, $data);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user