Compare commits

..

13 Commits

Author SHA1 Message Date
Lance Pioch
c784037ed4 Resolve real .env path and escape APP_URL for sed replacement 2026-02-07 23:21:04 -05:00
Lance Pioch
f4d572b0d3 Fix ASSET_URL not reaching PHP-FPM behind reverse proxy (#2026)
PHP-FPM's clear_env default strips shell environment variables, so the exported ASSET_URL was never visible to Laravel. This persists it to .env where Dotenv can read it.
2026-02-06 02:45:01 -05:00
Frogperson
a34bf9fd49 Add Daemon Base Directory field (#2151)
Co-authored-by: Boy132 <mail@boy132.de>
2026-02-05 08:00:25 -05:00
Boy132
7a9deba0e1 Fix notifications for DeleteAction on EditEgg page (#2165) 2026-02-04 22:20:22 +01:00
Charles
159bfe2210 exclude node actions (#2164) 2026-02-04 06:48:13 -05:00
stdpi
a821db8aae Improve file browser UI (#2086) 2026-02-04 05:40:45 -05:00
Lance Pioch
1556f8efb8 Allow all permissions to be toggled at once for api tokens (#2154) 2026-02-02 08:41:10 -05:00
Lance Pioch
57c2aa6f21 Fix the tabbing for username to password on login (#2155)
Co-authored-by: notCharles <charles@pelican.dev>
2026-02-02 08:40:55 -05:00
Charles
36de4c3786 composer update (#2161) 2026-01-31 19:37:54 -05:00
Boy132
26312e3897 Add basic api for plugins (#2146) 2026-02-01 00:10:57 +01:00
Boy132
a477c89025 Move username sanitization to model and make it less strict (#2150) 2026-02-01 00:07:26 +01:00
Boy132
93e81c26a9 Send server id to wings (#2157) 2026-01-31 22:54:06 +01:00
Boy132
23e91e8df3 Fix "visit website" button being invisible (#2158) 2026-01-31 22:53:56 +01:00
38 changed files with 586 additions and 1020 deletions

View File

@@ -22,6 +22,7 @@ use Filament\Forms\Components\ToggleButtons;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Fieldset;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
@@ -113,12 +114,44 @@ class ApiKeyResource extends Resource
*/
public static function defaultForm(Schema $schema): Schema
{
$permissionList = ApiKey::getPermissionList();
return $schema
->components([
Section::make(trans('admin/apikey.permissions.all'))
->description(trans('admin/apikey.permissions.all_description'))
->columnSpanFull()
->schema([
ToggleButtons::make('permissions_all')
->hiddenLabel()
->inline()
->options([
0 => trans('admin/apikey.permissions.none'),
1 => trans('admin/apikey.permissions.read'),
3 => trans('admin/apikey.permissions.read_write'),
])
->icons([
0 => TablerIcon::BookOff,
1 => TablerIcon::Book,
3 => TablerIcon::Writing,
])
->colors([
0 => 'success',
1 => 'warning',
3 => 'danger',
])
->live()
->afterStateUpdated(function ($state, callable $set) use ($permissionList) {
foreach ($permissionList as $resource) {
$set('permissions_' . $resource, $state);
}
})
->default(0),
]),
Fieldset::make('Permissions')
->columnSpanFull()
->schema(
collect(ApiKey::getPermissionList())->map(fn ($resource) => ToggleButtons::make('permissions_' . $resource)
collect($permissionList)->map(fn ($resource) => ToggleButtons::make('permissions_' . $resource)
->label(str($resource)->replace('_', ' ')->title())->inline()
->options([
0 => trans('admin/apikey.permissions.none'),

View File

@@ -450,17 +450,7 @@ class EditEgg extends EditRecord
return [
DeleteAction::make()
->disabled(fn (Egg $egg): bool => $egg->servers()->count() > 0)
->tooltip(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? trans('filament-actions::delete.single.label') : trans('admin/egg.in_use'))
->successNotification(fn (Egg $egg) => Notification::make()
->success()
->title(trans('admin/egg.delete_success'))
->body(trans('admin/egg.deleted', ['egg' => $egg->name]))
)
->failureNotification(fn (Egg $egg) => Notification::make()
->danger()
->title(trans('admin/egg.delete_failed'))
->body(trans('admin/egg.could_not_delete', ['egg' => $egg->name]))
),
->tooltip(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? trans('filament-actions::delete.single.label') : trans('admin/egg.in_use')),
ExportEggAction::make(),
ImportEggAction::make()
->multiple(false),

View File

@@ -82,9 +82,9 @@ class ListEggs extends ListRecords
->successRedirectUrl(fn (Egg $replica) => EditEgg::getUrl(['record' => $replica])),
])
->toolbarActions([
CreateAction::make(),
ImportEggAction::make()
->multiple(),
CreateAction::make(),
BulkActionGroup::make([
DeleteBulkAction::make()
->before(function (Collection &$records) {

View File

@@ -278,6 +278,14 @@ class CreateNode extends CreateRecord
->default(256)
->minValue(1)
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'),
TextInput::make('daemon_base')
->label(trans('admin/node.daemon_base'))
->placeholder('/var/lib/pelican/volumes')
->hintIcon(TablerIcon::QuestionMark, trans('admin/node.daemon_base_help'))
->columnSpan(1)
->required()
->default('/var/lib/pelican/volumes')
->rule('regex:/^([\/][\d\w.\-\/]+)$/'),
TextInput::make('daemon_sftp')
->columnSpan(1)
->label(trans('admin/node.sftp_port'))
@@ -287,7 +295,7 @@ class CreateNode extends CreateRecord
->required()
->integer(),
TextInput::make('daemon_sftp_alias')
->columnSpan(2)
->columnSpan(1)
->label(trans('admin/node.sftp_alias'))
->helperText(trans('admin/node.sftp_alias_help')),
Grid::make()

View File

@@ -314,7 +314,7 @@ class EditNode extends EditRecord
'default' => 1,
'sm' => 1,
'md' => 2,
'lg' => 2,
'lg' => 3,
]),
TextInput::make('upload_size')
->columnSpan([
@@ -329,12 +329,24 @@ class EditNode extends EditRecord
->required()
->minValue(1)
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'),
TextInput::make('daemon_base')
->label(trans('admin/node.daemon_base'))
->placeholder('/var/lib/pelican/volumes')
->hintIcon(TablerIcon::QuestionMark, trans('admin/node.daemon_base_help'))
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 2,
'lg' => 2,
])
->required()
->rule('regex:/^([\/][\d\w.\-\/]+)$/'),
TextInput::make('daemon_sftp')
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 3,
'md' => 2,
'lg' => 1,
])
->label(trans('admin/node.sftp_port'))
->minValue(1)
@@ -346,8 +358,8 @@ class EditNode extends EditRecord
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 3,
'md' => 2,
'lg' => 2,
])
->label(trans('admin/node.sftp_alias'))
->helperText(trans('admin/node.sftp_alias_help')),
@@ -356,7 +368,7 @@ class EditNode extends EditRecord
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 3,
'lg' => 2,
])
->label(trans('admin/node.use_for_deploy'))
->inline()
@@ -374,7 +386,7 @@ class EditNode extends EditRecord
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 3,
'lg' => 2,
])
->label(trans('admin/node.maintenance_mode'))
->inline()
@@ -572,7 +584,7 @@ class EditNode extends EditRecord
->columnSpanFull()
->schema([
Actions::make([
Action::make('autoDeploy')
Action::make('exclude_autoDeploy')
->label(trans('admin/node.auto_deploy'))
->color('primary')
->modalHeading(trans('admin/node.auto_deploy'))
@@ -610,7 +622,7 @@ class EditNode extends EditRecord
}),
])->fullWidth(),
Actions::make([
Action::make('resetKey')
Action::make('exclude_resetKey')
->label(trans('admin/node.reset_token'))
->color('danger')
->requiresConfirmation()

View File

@@ -91,7 +91,7 @@ class PluginResource extends Resource
->url(fn (Plugin $plugin) => !$plugin->getReadme() ? $plugin->url : null, true)
->slideOver(true)
->modalHeading('Readme')
->modalSubmitAction(fn (Plugin $plugin) => Action::make('visit_website')
->modalSubmitAction(fn (Plugin $plugin) => Action::make('exclude_visit_website')
->label(trans('admin/plugin.visit_website'))
->visible(!is_null($plugin->url))
->url($plugin->url, true)
@@ -240,7 +240,7 @@ class PluginResource extends Resource
}),
]),
])
->toolbarActions([
->headerActions([
Action::make('import_from_file')
->hiddenLabel()
->tooltip(trans('admin/plugin.import_from_file'))

View File

@@ -6,7 +6,6 @@ use App\Enums\SuspendAction;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Servers\ServerResource;
use App\Filament\Components\Actions\DeleteServerIcon;
use App\Filament\Components\Actions\ExportServerConfigAction;
use App\Filament\Components\Actions\PreviewStartupAction;
use App\Filament\Components\Forms\Fields\MonacoEditor;
use App\Filament\Components\Forms\Fields\StartupVariable;
@@ -1005,16 +1004,6 @@ class EditServer extends EditRecord
->hiddenLabel()
->hint(new HtmlString(trans('admin/server.transfer_help'))),
]),
Grid::make()
->columnSpan(3)
->schema([
Actions::make([
ExportServerConfigAction::make(),
])->fullWidth(),
ToggleButtons::make('export_help')
->hiddenLabel()
->hint(trans('admin/server.import_export.export_description')),
]),
Grid::make()
->columnSpan(3)
->schema([

View File

@@ -4,7 +4,6 @@ namespace App\Filament\Admin\Resources\Servers\Pages;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Servers\ServerResource;
use App\Filament\Components\Actions\ImportServerConfigAction;
use App\Filament\Server\Pages\Console;
use App\Models\Server;
use App\Traits\Filament\CanCustomizeHeaderActions;
@@ -100,7 +99,6 @@ class ListServers extends ListRecords
])
->toolbarActions([
CreateAction::make(),
ImportServerConfigAction::make(),
])
->searchable()
->emptyStateIcon(TablerIcon::BrandDocker)

View File

@@ -1,60 +0,0 @@
<?php
namespace App\Filament\Components\Actions;
use App\Models\Server;
use App\Services\Servers\Sharing\ServerConfigExporterService;
use Filament\Actions\Action;
use Filament\Forms\Components\Toggle;
use Filament\Support\Enums\Alignment;
use Filament\Support\Enums\IconSize;
class ExportServerConfigAction extends Action
{
public static function getDefaultName(): ?string
{
return 'export_config';
}
protected function setUp(): void
{
parent::setUp();
$this->label(trans('filament-actions::export.modal.actions.export.label'));
$this->iconSize(IconSize::ExtraLarge);
$this->authorize(fn () => user()?->can('view server'));
$this->modalHeading(fn (Server $server) => trans('admin/server.import_export.export_heading', ['name' => $server->name]));
$this->modalDescription(trans('admin/server.import_export.export_description'));
$this->modalFooterActionsAlignment(Alignment::Center);
$this->schema([
Toggle::make('include_description')
->label(trans('admin/server.import_export.include_description'))
->helperText(trans('admin/server.import_export.include_description_help'))
->default(true),
Toggle::make('include_allocations')
->label(trans('admin/server.import_export.include_allocations'))
->helperText(trans('admin/server.import_export.include_allocations_help'))
->default(true),
Toggle::make('include_variable_values')
->label(trans('admin/server.import_export.include_variables'))
->helperText(trans('admin/server.import_export.include_variables_help'))
->default(true),
]);
$this->action(fn (ServerConfigExporterService $service, Server $server, array $data) => response()->streamDownload(
function () use ($service, $server, $data) {
echo $service->handle($server, $data);
},
'server-' . str($server->name)->kebab()->lower()->trim() . '.yaml',
[
'Content-Type' => 'application/x-yaml',
]
));
}
}

View File

@@ -1,87 +0,0 @@
<?php
namespace App\Filament\Components\Actions;
use App\Exceptions\Service\InvalidFileUploadException;
use App\Services\Servers\Sharing\ServerConfigCreatorService;
use Filament\Actions\Action;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Select;
use Filament\Notifications\Notification;
use Illuminate\Http\UploadedFile;
class ImportServerConfigAction extends Action
{
public static function getDefaultName(): ?string
{
return 'import_config';
}
protected function setUp(): void
{
parent::setUp();
$this->hiddenLabel();
$this->icon('tabler-file-import');
$this->tooltip(trans('admin/server.import_export.import_tooltip'));
$this->authorize(fn () => user()?->can('create server'));
$this->modalHeading(trans('admin/server.import_export.import_heading'));
$this->modalDescription(trans('admin/server.import_export.import_description'));
$this->schema([
FileUpload::make('file')
->label(trans('admin/server.import_export.config_file'))
->hint(trans('admin/server.import_export.config_file_hint'))
->acceptedFileTypes(['application/x-yaml', 'text/yaml', 'text/x-yaml', '.yaml', '.yml'])
->preserveFilenames()
->previewable(false)
->storeFiles(false)
->required()
->maxSize(1024), // 1MB max
Select::make('node_id')
->label(trans('admin/server.import_export.node_select'))
->hint(trans('admin/server.import_export.node_select_hint'))
->options(fn () => user()?->accessibleNodes()->pluck('name', 'id') ?? [])
->searchable()
->required()
->visible(fn () => (user()?->accessibleNodes()->count() ?? 0) > 1),
]);
$this->action(function (ServerConfigCreatorService $createService, array $data): void {
/** @var UploadedFile $file */
$file = $data['file'];
$nodeId = $data['node_id'] ?? user()->accessibleNodes()->first()->id;
try {
$server = $createService->fromFile($file, $nodeId);
Notification::make()
->title(trans('admin/server.notifications.import_created'))
->body(trans('admin/server.notifications.import_created_body', ['name' => $server->name]))
->success()
->send();
redirect()->route('filament.admin.resources.servers.edit', ['record' => $server]);
} catch (InvalidFileUploadException $exception) {
Notification::make()
->title(trans('admin/server.notifications.import_failed'))
->body($exception->getMessage())
->danger()
->send();
} catch (\Exception $exception) {
Notification::make()
->title(trans('admin/server.notifications.import_failed'))
->body(trans('admin/server.notifications.import_failed_body', ['error' => $exception->getMessage()]))
->danger()
->send();
report($exception);
}
});
}
}

View File

@@ -66,6 +66,14 @@ class Login extends BaseLogin
->extraInputAttributes(['tabindex' => 1]);
}
protected function getPasswordFormComponent(): Component
{
/** @var TextInput $component */
$component = parent::getPasswordFormComponent();
return $component->extraInputAttributes(['tabindex' => 2]);
}
protected function getOAuthFormComponent(): Component
{
$actions = [];

View File

@@ -0,0 +1,203 @@
<?php
namespace App\Http\Controllers\Api\Application\Plugins;
use App\Enums\PluginStatus;
use App\Exceptions\PanelException;
use App\Http\Controllers\Api\Application\ApplicationApiController;
use App\Http\Requests\Api\Application\Plugins\ImportFilePluginRequest;
use App\Http\Requests\Api\Application\Plugins\ReadPluginRequest;
use App\Http\Requests\Api\Application\Plugins\UninstallPluginRequest;
use App\Http\Requests\Api\Application\Plugins\WritePluginRequest;
use App\Models\Plugin;
use App\Services\Helpers\PluginService;
use App\Transformers\Api\Application\PluginTransformer;
use Exception;
use Illuminate\Http\Response;
use Spatie\QueryBuilder\QueryBuilder;
class PluginController extends ApplicationApiController
{
/**
* PluginController constructor.
*/
public function __construct(private readonly PluginService $pluginService)
{
parent::__construct();
}
/**
* List plugins
*
* Return all plugins on the Panel.
*
* @return array<array-key, mixed>
*/
public function index(ReadPluginRequest $request): array
{
$plugins = QueryBuilder::for(Plugin::class)
->allowedFilters(['id', 'name', 'author', 'category'])
->allowedSorts(['id', 'name', 'author', 'category'])
->paginate($request->query('per_page') ?? 10);
return $this->fractal->collection($plugins)
->transformWith($this->getTransformer(PluginTransformer::class))
->toArray();
}
/**
* View plugin
*
* Return a single plugin.
*
* @return array<array-key, mixed>
*/
public function view(ReadPluginRequest $request, Plugin $plugin): array
{
return $this->fractal->item($plugin)
->transformWith($this->getTransformer(PluginTransformer::class))
->toArray();
}
/**
* Import plugin (file)
*
* Imports a new plugin file.
*
* @throws Exception
*/
public function importFile(WritePluginRequest $request): Response
{
if (!$request->hasFile('plugin')) {
throw new PanelException("No 'plugin' file in request");
}
$this->pluginService->downloadPluginFromFile($request->file('plugin'));
return new Response('', Response::HTTP_CREATED);
}
/**
* Import plugin (url)
*
* Imports a new plugin from an url.
*
* @throws Exception
*/
public function importUrl(ImportFilePluginRequest $request): Response
{
$this->pluginService->downloadPluginFromUrl($request->input('url'));
return new Response('', Response::HTTP_CREATED);
}
/**
* Install plugin
*
* Installs and enables a plugin.
*
* @return array<array-key, mixed>
*
* @throws Exception
*/
public function install(WritePluginRequest $request, Plugin $plugin): array
{
if ($plugin->status !== PluginStatus::NotInstalled) {
throw new PanelException('Plugin is already installed');
}
$this->pluginService->installPlugin($plugin);
return $this->fractal->item($plugin)
->transformWith($this->getTransformer(PluginTransformer::class))
->toArray();
}
/**
* Update plugin
*
* Downloads and installs an update for a plugin. Will throw if no update is available.
*
* @return array<array-key, mixed>
*
* @throws Exception
*/
public function update(WritePluginRequest $request, Plugin $plugin): array
{
if (!$plugin->isUpdateAvailable()) {
throw new PanelException("Plugin doesn't need updating");
}
$this->pluginService->updatePlugin($plugin);
return $this->fractal->item($plugin)
->transformWith($this->getTransformer(PluginTransformer::class))
->toArray();
}
/**
* Uninstall plugin
*
* Uninstalls a plugin. Optionally it will delete the plugin folder too.
*
* @return array<array-key, mixed>
*
* @throws Exception
*/
public function uninstall(UninstallPluginRequest $request, Plugin $plugin): array
{
if ($plugin->status === PluginStatus::NotInstalled) {
throw new PanelException('Plugin is not installed');
}
$this->pluginService->uninstallPlugin($plugin, $request->boolean('delete'));
return $this->fractal->item($plugin)
->transformWith($this->getTransformer(PluginTransformer::class))
->toArray();
}
/**
* Enable plugin
*
* Enables a plugin.
*
* @return array<array-key, mixed>
*
* @throws Exception
*/
public function enable(WritePluginRequest $request, Plugin $plugin): array
{
if (!$plugin->canEnable()) {
throw new PanelException("Plugin can't be enabled");
}
$this->pluginService->enablePlugin($plugin);
return $this->fractal->item($plugin)
->transformWith($this->getTransformer(PluginTransformer::class))
->toArray();
}
/**
* Disable plugin
*
* Disables a plugin.
*
* @return array<array-key, mixed>
*
* @throws Exception
*/
public function disable(WritePluginRequest $request, Plugin $plugin): array
{
if (!$plugin->canDisable()) {
throw new PanelException("Plugin can't be disabled");
}
$this->pluginService->disablePlugin($plugin);
return $this->fractal->item($plugin)
->transformWith($this->getTransformer(PluginTransformer::class))
->toArray();
}
}

View File

@@ -1,76 +0,0 @@
<?php
namespace App\Http\Controllers\Api\Application\Servers;
use App\Exceptions\Service\InvalidFileUploadException;
use App\Http\Controllers\Api\Application\ApplicationApiController;
use App\Http\Requests\Api\Application\Servers\GetServerRequest;
use App\Models\Server;
use App\Services\Servers\Sharing\ServerConfigCreatorService;
use App\Services\Servers\Sharing\ServerConfigExporterService;
use App\Transformers\Api\Application\ServerTransformer;
use Dedoc\Scramble\Attributes\Group;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
#[Group('Server Config', weight: 1)]
class ServerConfigController extends ApplicationApiController
{
public function __construct(
private ServerConfigExporterService $exporterService,
private ServerConfigCreatorService $creatorService
) {
parent::__construct();
}
/**
* Export server configuration
*
* Export a server's configuration to YAML format. Returns the configuration as a
* downloadable YAML file containing settings, limits, allocations, and variable values.
*/
public function export(GetServerRequest $request, Server $server): Response
{
$options = [
'include_description' => $request->boolean('include_description', true),
'include_allocations' => $request->boolean('include_allocations', true),
'include_variable_values' => $request->boolean('include_variable_values', true),
];
$yaml = $this->exporterService->handle($server, $options);
$filename = 'server-config-' . str($server->name)->kebab()->lower()->trim() . '.yaml';
return response($yaml, 200, [
'Content-Type' => 'application/x-yaml',
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
]);
}
/**
* Create server from configuration
*
* Create a new server from a YAML configuration file. The configuration must
* include a valid egg UUID that exists in the system. Optionally specify a
* node_id to create the server on a specific node.
*
* @throws InvalidFileUploadException
*/
public function create(Request $request): JsonResponse
{
$request->validate([
'file' => 'required|file|mimes:yaml,yml|max:1024',
'node_id' => 'required|integer|exists:nodes,id',
]);
$file = $request->file('file');
$nodeId = $request->input('node_id');
$server = $this->creatorService->fromFile($file, $nodeId);
return $this->fractal->item($server)
->transformWith($this->getTransformer(ServerTransformer::class))
->respond(201);
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Http\Requests\Api\Application\Plugins;
class ImportFilePluginRequest extends WritePluginRequest
{
public function rules(): array
{
return [
'url' => 'required|string',
];
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Http\Requests\Api\Application\Plugins;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Plugin;
use App\Services\Acl\Api\AdminAcl;
class ReadPluginRequest extends ApplicationApiRequest
{
protected ?string $resource = Plugin::RESOURCE_NAME;
protected int $permission = AdminAcl::READ;
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Http\Requests\Api\Application\Plugins;
class UninstallPluginRequest extends WritePluginRequest
{
/**
* @param array<array-key, string|string[]>|null $rules
* @return array<array-key, string|string[]>
*/
public function rules(?array $rules = null): array
{
return [
'delete' => 'boolean',
];
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Http\Requests\Api\Application\Plugins;
use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Plugin;
use App\Services\Acl\Api\AdminAcl;
class WritePluginRequest extends ApplicationApiRequest
{
protected ?string $resource = Plugin::RESOURCE_NAME;
protected int $permission = AdminAcl::WRITE;
}

View File

@@ -174,6 +174,7 @@ class ApiKey extends PersonalAccessToken
Database::RESOURCE_NAME,
Mount::RESOURCE_NAME,
Role::RESOURCE_NAME,
Plugin::RESOURCE_NAME,
];
/** @var string[] */

View File

@@ -38,6 +38,8 @@ class Plugin extends Model implements HasPluginSettings
{
use Sushi;
public const RESOURCE_NAME = 'plugin';
protected $primaryKey = 'id';
protected $keyType = 'string';

View File

@@ -198,7 +198,8 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
});
static::saving(function (self $user) {
$user->email = mb_strtolower($user->email);
$user->username = str($user->username)->lower()->toString();
$user->email = str($user->email)->lower()->toString();
});
static::deleting(function (self $user) {

View File

@@ -3,7 +3,6 @@
namespace App\Services\Servers;
use App\Extensions\Features\FeatureService;
use App\Models\Egg;
use App\Models\Mount;
use App\Models\Server;
@@ -39,6 +38,7 @@ class ServerConfigurationStructureService
* Returns the data format used for the daemon.
*
* @return array{
* id: int,
* uuid: string,
* meta: array{name: string, description: string},
* suspended: bool,
@@ -70,6 +70,7 @@ class ServerConfigurationStructureService
protected function returnFormat(Server $server): array
{
$response = [
'id' => $server->id,
'uuid' => $server->uuid,
'meta' => [
'name' => $server->name,

View File

@@ -1,267 +0,0 @@
<?php
namespace App\Services\Servers\Sharing;
use App\Exceptions\Service\InvalidFileUploadException;
use App\Models\Allocation;
use App\Models\Egg;
use App\Models\EggVariable;
use App\Models\Node;
use App\Models\Server;
use App\Models\ServerVariable;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Storage;
use Ramsey\Uuid\Uuid;
use Symfony\Component\Yaml\Yaml;
class ServerConfigCreatorService
{
/**
* @throws InvalidFileUploadException
*/
public function fromFile(UploadedFile $file, ?int $nodeId = null): Server
{
if ($file->getError() !== UPLOAD_ERR_OK) {
throw new InvalidFileUploadException(trans('admin/server.import_errors.file_error'));
}
try {
$parsed = Yaml::parse($file->getContent());
} catch (\Exception $exception) {
throw new InvalidFileUploadException(trans('admin/server.import_errors.parse_error_desc', ['error' => $exception->getMessage()]));
}
return $this->createServer($parsed, $nodeId);
}
/**
* Create a server from configuration array.
*
* @param array<string, mixed> $config
*
* @throws InvalidFileUploadException
*/
protected function createServer(array $config, ?int $nodeId = null): Server
{
$eggUuid = Arr::get($config, 'egg.uuid');
$eggName = Arr::get($config, 'egg.name');
if (!$eggUuid) {
throw new InvalidFileUploadException(trans('admin/server.import_errors.egg_uuid_required'));
}
$egg = Egg::where('uuid', $eggUuid)->first();
if (!$egg) {
throw new InvalidFileUploadException(
trans('admin/server.import_errors.egg_not_found_desc', [
'uuid' => $eggUuid,
'name' => $eggName ?: trans('admin/server.none'),
])
);
}
if ($nodeId) {
$node = Node::whereIn('id', user()?->accessibleNodes()->pluck('id'))
->where('id', $nodeId)
->first();
if (!$node) {
throw new InvalidFileUploadException(trans('admin/server.import_errors.node_not_accessible'));
}
} else {
$node = Node::whereIn('id', user()?->accessibleNodes()->pluck('id'))->first();
if (!$node) {
throw new InvalidFileUploadException(trans('admin/server.import_errors.no_nodes'));
}
}
$allocations = Arr::get($config, 'allocations', []);
$primaryAllocation = null;
$createdAllocations = [];
if (!empty($allocations)) {
foreach ($allocations as $allocationData) {
$ip = Arr::get($allocationData, 'ip');
$port = Arr::get($allocationData, 'port');
$isPrimary = Arr::get($allocationData, 'is_primary', false);
$allocation = Allocation::where('node_id', $node->id)
->where('ip', $ip)
->where('port', $port)
->whereNull('server_id')
->first();
if (!$allocation) {
$existingAllocation = Allocation::where('node_id', $node->id)
->where('ip', $ip)
->where('port', $port)
->first();
if ($existingAllocation) {
$port = $this->findNextAvailablePort($node->id, $ip, $port);
}
$allocation = Allocation::create([
'node_id' => $node->id,
'ip' => $ip,
'port' => $port,
]);
}
$createdAllocations[] = $allocation;
if ($isPrimary && !$primaryAllocation) {
$primaryAllocation = $allocation;
}
}
if (!$primaryAllocation && !empty($createdAllocations)) {
$primaryAllocation = $createdAllocations[0];
}
}
$owner = user();
if (!$owner) {
throw new InvalidFileUploadException(trans('admin/server.import_errors.no_user'));
}
$serverName = Arr::get($config, 'name', 'Imported Server');
$startupCommand = Arr::get($config, 'settings.startup');
if ($startupCommand === null) {
$startupCommand = array_values($egg->startup_commands)[0];
}
$dockerImage = Arr::get($config, 'settings.image');
if ($dockerImage === null) {
$dockerImage = array_values($egg->docker_images)[0];
}
$uuid = Uuid::uuid4()->toString();
$server = Server::create([
'uuid' => $uuid,
'uuid_short' => substr($uuid, 0, 8),
'name' => $serverName,
'description' => Arr::get($config, 'description', ''),
'owner_id' => $owner->id,
'node_id' => $node->id,
'allocation_id' => $primaryAllocation?->id,
'egg_id' => $egg->id,
'startup' => $startupCommand,
'image' => $dockerImage,
'skip_scripts' => Arr::get($config, 'settings.skip_scripts', false),
'memory' => Arr::get($config, 'limits.memory', 512),
'swap' => Arr::get($config, 'limits.swap', 0),
'disk' => Arr::get($config, 'limits.disk', 1024),
'io' => Arr::get($config, 'limits.io', 500),
'cpu' => Arr::get($config, 'limits.cpu', 0),
'threads' => Arr::get($config, 'limits.threads'),
'oom_killer' => Arr::get($config, 'limits.oom_killer', false),
'database_limit' => Arr::get($config, 'feature_limits.databases', 0),
'allocation_limit' => Arr::get($config, 'feature_limits.allocations', 0),
'backup_limit' => Arr::get($config, 'feature_limits.backups', 0),
]);
if ($primaryAllocation) {
$primaryAllocation->update(['server_id' => $server->id]);
}
foreach ($createdAllocations as $allocation) {
if ($allocation->id !== $primaryAllocation?->id) {
$allocation->update(['server_id' => $server->id]);
}
}
if (isset($config['variables'])) {
$this->importVariables($server, $config['variables']);
}
if (isset($config['icon'])) {
$this->importServerIcon($server, $config['icon']);
}
return $server;
}
/**
* Import server icon from base64 encoded data.
*
* @param array<string, string> $iconData
*/
protected function importServerIcon(Server $server, array $iconData): void
{
$base64Data = Arr::get($iconData, 'data');
$extension = Arr::get($iconData, 'extension');
if (!$base64Data || !$extension) {
return;
}
if (!array_key_exists($extension, Server::IMAGE_FORMATS)) {
return;
}
try {
$imageData = base64_decode($base64Data, true);
if ($imageData === false) {
return;
}
$path = Server::ICON_STORAGE_PATH . "/{$server->uuid}.{$extension}";
Storage::disk('public')->put($path, $imageData);
} catch (\Exception $e) {
// Log the error but do not fail the entire import process
report($e);
}
}
/**
* @param array<int, array{env_variable: string, value: string|null}> $variables
*/
protected function importVariables(Server $server, array $variables): void
{
foreach ($variables as $variable) {
$envVariable = Arr::get($variable, 'env_variable');
$value = Arr::get($variable, 'value');
/** @var EggVariable $eggVariable */
$eggVariable = $server->egg->variables()->where('env_variable', $envVariable)->first();
ServerVariable::create([
'server_id' => $server->id,
'variable_id' => $eggVariable->id,
'variable_value' => $value,
]);
}
}
/**
* @throws InvalidFileUploadException
*/
protected function findNextAvailablePort(int $nodeId, string $ip, int $startPort): int
{
$port = $startPort + 1;
$maxPort = 65535;
while ($port <= $maxPort) {
$exists = Allocation::where('node_id', $nodeId)
->where('ip', $ip)
->where('port', $port)
->exists();
if (!$exists) {
return $port;
}
$port++;
}
throw new InvalidFileUploadException(trans('admin/server.import_errors.port_exhausted_desc', ['ip' => $ip, 'port' => $startPort]));
}
}

View File

@@ -1,106 +0,0 @@
<?php
namespace App\Services\Servers\Sharing;
use App\Models\Server;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\Yaml\Yaml;
class ServerConfigExporterService
{
/**
* @param array<string, bool> $options
*/
public function handle(Server|int $server, array $options = []): string
{
if (!$server instanceof Server) {
$server = Server::with(['egg', 'allocations', 'serverVariables.variable'])->findOrFail($server);
}
$includeDescription = $options['include_description'] ?? true;
$includeAllocations = $options['include_allocations'] ?? true;
$includeVariableValues = $options['include_variable_values'] ?? true;
$data = [
'name' => $server->name,
'egg' => [
'uuid' => $server->egg->uuid,
'name' => $server->egg->name,
],
'settings' => [
'startup' => $server->startup,
'image' => $server->image,
'skip_scripts' => $server->skip_scripts,
],
'limits' => [
'memory' => $server->memory,
'swap' => $server->swap,
'disk' => $server->disk,
'io' => $server->io,
'cpu' => $server->cpu,
'threads' => $server->threads,
'oom_killer' => $server->oom_killer,
],
'feature_limits' => [
'databases' => $server->database_limit,
'allocations' => $server->allocation_limit,
'backups' => $server->backup_limit,
],
];
if ($includeDescription && !empty($server->description)) {
$data['description'] = $server->description;
}
// Export server icon if exists
$iconData = $this->exportServerIcon($server);
if ($iconData) {
$data['icon'] = $iconData;
}
if ($includeAllocations && $server->allocations->isNotEmpty()) {
$data['allocations'] = $server->allocations->map(function ($allocation) use ($server) {
return [
'ip' => $allocation->ip,
'port' => $allocation->port,
'is_primary' => $allocation->id === $server->allocation_id,
];
})->values()->all();
}
if ($includeVariableValues && $server->serverVariables->isNotEmpty()) {
$data['variables'] = $server->serverVariables->map(function ($serverVar) {
return [
'env_variable' => $serverVar->variable->env_variable,
'value' => $serverVar->variable_value,
];
})->values()->all();
}
return Yaml::dump($data, 4, 2);
}
/**
* Export server icon as base64 encoded string with mime type.
*
* @return array<string, string>|null
*/
protected function exportServerIcon(Server $server): ?array
{
foreach (array_keys(Server::IMAGE_FORMATS) as $ext) {
$path = Server::ICON_STORAGE_PATH . "/{$server->uuid}.{$ext}";
if (Storage::disk('public')->exists($path)) {
$contents = Storage::disk('public')->get($path);
$mimeType = Server::IMAGE_FORMATS[$ext];
return [
'data' => base64_encode($contents),
'mime_type' => $mimeType,
'extension' => $ext,
];
}
}
return null;
}
}

View File

@@ -1,194 +0,0 @@
<?php
namespace App\Services\Servers\Sharing;
use App\Exceptions\Service\InvalidFileUploadException;
use App\Models\Allocation;
use App\Models\Egg;
use App\Models\EggVariable;
use App\Models\Server;
use App\Models\ServerVariable;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Arr;
use Symfony\Component\Yaml\Yaml;
class ServerConfigImporterService
{
/**
* @throws InvalidFileUploadException
*/
public function fromFile(UploadedFile $file, Server $server): void
{
if ($file->getError() !== UPLOAD_ERR_OK) {
throw new InvalidFileUploadException('The selected file was not uploaded successfully');
}
try {
$parsed = Yaml::parse($file->getContent());
} catch (\Exception $exception) {
throw new InvalidFileUploadException('Could not parse YAML file: ' . $exception->getMessage());
}
$this->applyConfiguration($server, $parsed);
}
/**
* @param array{
* egg: array{uuid: string, name?: string},
* settings?: array<string, mixed>,
* limits?: array<string, mixed>,
* feature_limits?: array<string, mixed>,
* description?: string,
* variables?: array<int, array<string, mixed>>,
* allocations?: array<int, array<string, mixed>>
* } $config
*
* @throws InvalidFileUploadException
*/
public function applyConfiguration(Server $server, array $config): void
{
$eggUuid = Arr::get($config, 'egg.uuid');
$eggName = Arr::get($config, 'egg.name');
if (!$eggUuid) {
throw new InvalidFileUploadException('Egg UUID is required in the configuration file');
}
$egg = Egg::where('uuid', $eggUuid)->first();
if (!$egg) {
throw new InvalidFileUploadException(
"Egg with UUID '{$eggUuid}'" .
($eggName ? " (name: {$eggName})" : '') .
' does not exist in the system'
);
}
$server->update([
'egg_id' => $egg->id,
'startup' => Arr::get($config, 'settings.startup', $server->startup),
'image' => Arr::get($config, 'settings.image', $server->image),
'skip_scripts' => Arr::get($config, 'settings.skip_scripts', $server->skip_scripts),
'memory' => Arr::get($config, 'limits.memory', $server->memory),
'swap' => Arr::get($config, 'limits.swap', $server->swap),
'disk' => Arr::get($config, 'limits.disk', $server->disk),
'io' => Arr::get($config, 'limits.io', $server->io),
'cpu' => Arr::get($config, 'limits.cpu', $server->cpu),
'threads' => Arr::get($config, 'limits.threads', $server->threads),
'oom_killer' => Arr::get($config, 'limits.oom_killer', $server->oom_killer),
'database_limit' => Arr::get($config, 'feature_limits.databases', $server->database_limit),
'allocation_limit' => Arr::get($config, 'feature_limits.allocations', $server->allocation_limit),
'backup_limit' => Arr::get($config, 'feature_limits.backups', $server->backup_limit),
]);
if (isset($config['description'])) {
$server->update(['description' => $config['description']]);
}
if (isset($config['variables'])) {
$this->importVariables($server, $config['variables']);
}
if (isset($config['allocations'])) {
$this->importAllocations($server, $config['allocations']);
}
}
/**
* @param array<int, array{env_variable: string, value: string|null}> $variables
*/
protected function importVariables(Server $server, array $variables): void
{
foreach ($variables as $variable) {
$envVariable = Arr::get($variable, 'env_variable');
$value = Arr::get($variable, 'value');
$eggVariable = EggVariable::where('egg_id', $server->egg_id)
->where('env_variable', $envVariable)
->first();
if ($eggVariable) {
ServerVariable::updateOrCreate(
[
'server_id' => $server->id,
'variable_id' => $eggVariable->id,
],
[
'variable_value' => $value,
]
);
}
}
}
/**
* @param array<int, array<string, mixed>> $allocations
*
* @throws InvalidFileUploadException
*/
protected function importAllocations(Server $server, array $allocations): void
{
$nodeId = $server->node_id;
$primaryAllocationSet = false;
foreach ($allocations as $allocationData) {
$ip = Arr::get($allocationData, 'ip');
$port = Arr::get($allocationData, 'port');
$isPrimary = Arr::get($allocationData, 'is_primary', false);
$allocation = Allocation::where('node_id', $nodeId)
->where('ip', $ip)
->where('port', $port)
->first();
if (!$allocation) {
$allocation = Allocation::create([
'node_id' => $nodeId,
'ip' => $ip,
'port' => $port,
'server_id' => $server->id,
]);
} elseif ($allocation->server_id && $allocation->server_id !== $server->id) {
$newPort = $this->findNextAvailablePort($nodeId, $ip, $port);
$allocation = Allocation::create([
'node_id' => $nodeId,
'ip' => $ip,
'port' => $newPort,
'server_id' => $server->id,
]);
} elseif (!$allocation->server_id) {
$allocation->update(['server_id' => $server->id]);
}
if ($isPrimary && !$primaryAllocationSet) {
$server->update(['allocation_id' => $allocation->id]);
$primaryAllocationSet = true;
}
}
}
/**
* @throws InvalidFileUploadException
*/
protected function findNextAvailablePort(int $nodeId, string $ip, int $startPort): int
{
$port = $startPort + 1;
$maxPort = 65535;
while ($port <= $maxPort) {
$exists = Allocation::where('node_id', $nodeId)
->where('ip', $ip)
->where('port', $port)
->exists();
if (!$exists) {
return $port;
}
$port++;
}
throw new InvalidFileUploadException("Could not find an available port for IP {$ip} starting from port {$startPort}");
}
}

View File

@@ -49,12 +49,6 @@ class UserCreationService
$data['username'] = str($data['email'])->before('@')->toString() . Str::random(3);
}
$data['username'] = str($data['username'])
->replace(['.', '-'], '')
->ascii()
->substr(0, 64)
->toString();
/** @var User $user */
$user = User::query()->forceCreate(array_merge($data, [
'uuid' => Uuid::uuid4()->toString(),

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Transformers\Api\Application;
use App\Models\Plugin;
class PluginTransformer extends BaseTransformer
{
/**
* Return the resource name for the JSONAPI output.
*/
public function getResourceName(): string
{
return Plugin::RESOURCE_NAME;
}
/**
* @param Plugin $model
*/
public function transform($model): array
{
return [
'id' => $model->id,
'name' => $model->name,
'author' => $model->author,
'version' => $model->version,
'description' => $model->description,
'category' => $model->category,
'url' => $model->url,
'update_url' => $model->update_url,
'namespace' => $model->namespace,
'class' => $model->class,
'panels' => $model->panels ? explode(',', $model->panels) : null,
'panel_version' => $model->panel_version,
'composer_packages' => $model->composer_packages ? json_decode($model->composer_packages, true, 512, JSON_THROW_ON_ERROR) : null,
'meta' => [
'status' => $model->status,
'status_message' => $model->status_message,
'load_order' => $model->load_order,
'is_compatible' => $model->isCompatible(),
'update_available' => $model->isUpdateAvailable(),
'can_enable' => $model->canEnable(),
'can_disable' => $model->canDisable(),
],
];
}
}

274
composer.lock generated
View File

@@ -128,16 +128,16 @@
},
{
"name": "aws/aws-sdk-php",
"version": "3.369.21",
"version": "3.369.24",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "7076af00534135cbbf6cc19eb2521124a3549f0d"
"reference": "17f404a47879c1fb47175ac2b61881ab0dc2dc5c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/7076af00534135cbbf6cc19eb2521124a3549f0d",
"reference": "7076af00534135cbbf6cc19eb2521124a3549f0d",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/17f404a47879c1fb47175ac2b61881ab0dc2dc5c",
"reference": "17f404a47879c1fb47175ac2b61881ab0dc2dc5c",
"shasum": ""
},
"require": {
@@ -219,9 +219,9 @@
"support": {
"forum": "https://github.com/aws/aws-sdk-php/discussions",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.369.21"
"source": "https://github.com/aws/aws-sdk-php/tree/3.369.24"
},
"time": "2026-01-27T19:14:48+00:00"
"time": "2026-01-30T19:14:32+00:00"
},
{
"name": "blade-ui-kit/blade-heroicons",
@@ -375,16 +375,16 @@
},
{
"name": "brick/math",
"version": "0.14.1",
"version": "0.14.2",
"source": {
"type": "git",
"url": "https://github.com/brick/math.git",
"reference": "f05858549e5f9d7bb45875a75583240a38a281d0"
"reference": "55c950aa71a2cabc1d8f2bec1f8a7020bd244aa2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/brick/math/zipball/f05858549e5f9d7bb45875a75583240a38a281d0",
"reference": "f05858549e5f9d7bb45875a75583240a38a281d0",
"url": "https://api.github.com/repos/brick/math/zipball/55c950aa71a2cabc1d8f2bec1f8a7020bd244aa2",
"reference": "55c950aa71a2cabc1d8f2bec1f8a7020bd244aa2",
"shasum": ""
},
"require": {
@@ -423,7 +423,7 @@
],
"support": {
"issues": "https://github.com/brick/math/issues",
"source": "https://github.com/brick/math/tree/0.14.1"
"source": "https://github.com/brick/math/tree/0.14.2"
},
"funding": [
{
@@ -431,7 +431,7 @@
"type": "github"
}
],
"time": "2025-11-24T14:40:29+00:00"
"time": "2026-01-30T14:03:11+00:00"
},
{
"name": "calebporzio/sushi",
@@ -822,16 +822,16 @@
},
{
"name": "dedoc/scramble",
"version": "v0.13.10",
"version": "v0.13.11",
"source": {
"type": "git",
"url": "https://github.com/dedoc/scramble.git",
"reference": "fd73178629c0a5ddc59eeac4fd605d4820b60f11"
"reference": "871dbac4888e3d22d7f04c2c7b3d7bb810e87005"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dedoc/scramble/zipball/fd73178629c0a5ddc59eeac4fd605d4820b60f11",
"reference": "fd73178629c0a5ddc59eeac4fd605d4820b60f11",
"url": "https://api.github.com/repos/dedoc/scramble/zipball/871dbac4888e3d22d7f04c2c7b3d7bb810e87005",
"reference": "871dbac4888e3d22d7f04c2c7b3d7bb810e87005",
"shasum": ""
},
"require": {
@@ -890,7 +890,7 @@
],
"support": {
"issues": "https://github.com/dedoc/scramble/issues",
"source": "https://github.com/dedoc/scramble/tree/v0.13.10"
"source": "https://github.com/dedoc/scramble/tree/v0.13.11"
},
"funding": [
{
@@ -898,7 +898,7 @@
"type": "github"
}
],
"time": "2025-12-29T08:30:07+00:00"
"time": "2026-01-28T14:02:15+00:00"
},
{
"name": "dflydev/dot-access-data",
@@ -1323,16 +1323,16 @@
},
{
"name": "filament/actions",
"version": "v4.6.1",
"version": "v4.6.3",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/actions.git",
"reference": "e9936617c3f74b18bf24a0645a6314a719c74a33"
"reference": "4a3896cd956fcb7d132a51453bc8110d47023433"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/actions/zipball/e9936617c3f74b18bf24a0645a6314a719c74a33",
"reference": "e9936617c3f74b18bf24a0645a6314a719c74a33",
"url": "https://api.github.com/repos/filamentphp/actions/zipball/4a3896cd956fcb7d132a51453bc8110d47023433",
"reference": "4a3896cd956fcb7d132a51453bc8110d47023433",
"shasum": ""
},
"require": {
@@ -1368,20 +1368,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2026-01-27T12:10:55+00:00"
"time": "2026-01-30T12:51:27+00:00"
},
{
"name": "filament/filament",
"version": "v4.6.1",
"version": "v4.6.3",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/panels.git",
"reference": "2fe37fa96447cc61a70354b43f854b3632e388cc"
"reference": "24aababf2777f82014887199f9cd029b5e6251d5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/panels/zipball/2fe37fa96447cc61a70354b43f854b3632e388cc",
"reference": "2fe37fa96447cc61a70354b43f854b3632e388cc",
"url": "https://api.github.com/repos/filamentphp/panels/zipball/24aababf2777f82014887199f9cd029b5e6251d5",
"reference": "24aababf2777f82014887199f9cd029b5e6251d5",
"shasum": ""
},
"require": {
@@ -1425,20 +1425,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2026-01-27T12:10:56+00:00"
"time": "2026-01-30T12:47:54+00:00"
},
{
"name": "filament/forms",
"version": "v4.6.1",
"version": "v4.6.3",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/forms.git",
"reference": "63dfeb69b3823e0effde1cefcdaae21c1ddf0814"
"reference": "137090f3d2b1dc6e4234ad2325c9acb34276d5b5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/forms/zipball/63dfeb69b3823e0effde1cefcdaae21c1ddf0814",
"reference": "63dfeb69b3823e0effde1cefcdaae21c1ddf0814",
"url": "https://api.github.com/repos/filamentphp/forms/zipball/137090f3d2b1dc6e4234ad2325c9acb34276d5b5",
"reference": "137090f3d2b1dc6e4234ad2325c9acb34276d5b5",
"shasum": ""
},
"require": {
@@ -1475,20 +1475,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2026-01-27T12:10:56+00:00"
"time": "2026-01-30T12:47:37+00:00"
},
{
"name": "filament/infolists",
"version": "v4.6.1",
"version": "v4.6.3",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/infolists.git",
"reference": "841a0520637b98822f09d008509e040ba9ef3faa"
"reference": "03228d5cf9310598712299d49ac38b75d61d7c1c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/infolists/zipball/841a0520637b98822f09d008509e040ba9ef3faa",
"reference": "841a0520637b98822f09d008509e040ba9ef3faa",
"url": "https://api.github.com/repos/filamentphp/infolists/zipball/03228d5cf9310598712299d49ac38b75d61d7c1c",
"reference": "03228d5cf9310598712299d49ac38b75d61d7c1c",
"shasum": ""
},
"require": {
@@ -1520,11 +1520,11 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2026-01-23T10:58:05+00:00"
"time": "2026-01-29T21:00:53+00:00"
},
{
"name": "filament/notifications",
"version": "v4.6.1",
"version": "v4.6.3",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/notifications.git",
@@ -1571,16 +1571,16 @@
},
{
"name": "filament/query-builder",
"version": "v4.6.1",
"version": "v4.6.3",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/query-builder.git",
"reference": "bcafe9bb71ec34292440c4bded5ee395f037b2a5"
"reference": "132ad55f85eaa427bb786b4a8184ac6e24a7e676"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/query-builder/zipball/bcafe9bb71ec34292440c4bded5ee395f037b2a5",
"reference": "bcafe9bb71ec34292440c4bded5ee395f037b2a5",
"url": "https://api.github.com/repos/filamentphp/query-builder/zipball/132ad55f85eaa427bb786b4a8184ac6e24a7e676",
"reference": "132ad55f85eaa427bb786b4a8184ac6e24a7e676",
"shasum": ""
},
"require": {
@@ -1613,20 +1613,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2026-01-23T10:52:35+00:00"
"time": "2026-01-30T12:51:18+00:00"
},
{
"name": "filament/schemas",
"version": "v4.6.1",
"version": "v4.6.3",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/schemas.git",
"reference": "1638d2b16f7684b7925d9c6253592ecd308d7f12"
"reference": "e1bdda85e9fb5d65774df153e0357eb41c7c7a3d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/schemas/zipball/1638d2b16f7684b7925d9c6253592ecd308d7f12",
"reference": "1638d2b16f7684b7925d9c6253592ecd308d7f12",
"url": "https://api.github.com/repos/filamentphp/schemas/zipball/e1bdda85e9fb5d65774df153e0357eb41c7c7a3d",
"reference": "e1bdda85e9fb5d65774df153e0357eb41c7c7a3d",
"shasum": ""
},
"require": {
@@ -1658,20 +1658,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2026-01-27T12:11:14+00:00"
"time": "2026-01-29T21:01:33+00:00"
},
{
"name": "filament/support",
"version": "v4.6.1",
"version": "v4.6.3",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/support.git",
"reference": "26894b15b989f89c83b004b38e513b9b2843a0c4"
"reference": "67a017956b2fe3d37425a2df0d1f8e18c30b42e2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/support/zipball/26894b15b989f89c83b004b38e513b9b2843a0c4",
"reference": "26894b15b989f89c83b004b38e513b9b2843a0c4",
"url": "https://api.github.com/repos/filamentphp/support/zipball/67a017956b2fe3d37425a2df0d1f8e18c30b42e2",
"reference": "67a017956b2fe3d37425a2df0d1f8e18c30b42e2",
"shasum": ""
},
"require": {
@@ -1716,20 +1716,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2026-01-27T12:11:14+00:00"
"time": "2026-01-29T21:00:17+00:00"
},
{
"name": "filament/tables",
"version": "v4.6.1",
"version": "v4.6.3",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/tables.git",
"reference": "ae5a17dfc442b9ac9ff6d3f91a96538c3771ee3c"
"reference": "e3cb9c71e150a6a2b7e8601c363e7731ddfc9518"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/tables/zipball/ae5a17dfc442b9ac9ff6d3f91a96538c3771ee3c",
"reference": "ae5a17dfc442b9ac9ff6d3f91a96538c3771ee3c",
"url": "https://api.github.com/repos/filamentphp/tables/zipball/e3cb9c71e150a6a2b7e8601c363e7731ddfc9518",
"reference": "e3cb9c71e150a6a2b7e8601c363e7731ddfc9518",
"shasum": ""
},
"require": {
@@ -1762,20 +1762,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2026-01-27T12:10:54+00:00"
"time": "2026-01-30T12:51:42+00:00"
},
{
"name": "filament/widgets",
"version": "v4.6.1",
"version": "v4.6.3",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/widgets.git",
"reference": "2dfe6ea8d6a491cdf1eb7056561ec9db4b8cfeb2"
"reference": "d9b5a35a04048bea86563223d2505d11aea5a7e0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/widgets/zipball/2dfe6ea8d6a491cdf1eb7056561ec9db4b8cfeb2",
"reference": "2dfe6ea8d6a491cdf1eb7056561ec9db4b8cfeb2",
"url": "https://api.github.com/repos/filamentphp/widgets/zipball/d9b5a35a04048bea86563223d2505d11aea5a7e0",
"reference": "d9b5a35a04048bea86563223d2505d11aea5a7e0",
"shasum": ""
},
"require": {
@@ -1806,7 +1806,7 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2026-01-23T10:56:26+00:00"
"time": "2026-01-29T20:48:44+00:00"
},
{
"name": "firebase/php-jwt",
@@ -4635,16 +4635,16 @@
},
{
"name": "nesbot/carbon",
"version": "3.11.0",
"version": "3.11.1",
"source": {
"type": "git",
"url": "https://github.com/CarbonPHP/carbon.git",
"reference": "bdb375400dcd162624531666db4799b36b64e4a1"
"reference": "f438fcc98f92babee98381d399c65336f3a3827f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/bdb375400dcd162624531666db4799b36b64e4a1",
"reference": "bdb375400dcd162624531666db4799b36b64e4a1",
"url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/f438fcc98f92babee98381d399c65336f3a3827f",
"reference": "f438fcc98f92babee98381d399c65336f3a3827f",
"shasum": ""
},
"require": {
@@ -4668,7 +4668,7 @@
"phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan": "^2.1.22",
"phpunit/phpunit": "^10.5.53",
"squizlabs/php_codesniffer": "^3.13.4"
"squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0"
},
"bin": [
"bin/carbon"
@@ -4711,14 +4711,14 @@
}
],
"description": "An API extension for DateTime that supports 281 different languages.",
"homepage": "https://carbon.nesbot.com",
"homepage": "https://carbonphp.github.io/carbon/",
"keywords": [
"date",
"datetime",
"time"
],
"support": {
"docs": "https://carbon.nesbot.com/docs",
"docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html",
"issues": "https://github.com/CarbonPHP/carbon/issues",
"source": "https://github.com/CarbonPHP/carbon"
},
@@ -4736,7 +4736,7 @@
"type": "tidelift"
}
],
"time": "2025-12-02T21:04:28+00:00"
"time": "2026-01-29T09:26:29+00:00"
},
{
"name": "nette/php-generator",
@@ -6526,16 +6526,16 @@
},
{
"name": "psy/psysh",
"version": "v0.12.18",
"version": "v0.12.19",
"source": {
"type": "git",
"url": "https://github.com/bobthecow/psysh.git",
"reference": "ddff0ac01beddc251786fe70367cd8bbdb258196"
"reference": "a4f766e5c5b6773d8399711019bb7d90875a50ee"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/ddff0ac01beddc251786fe70367cd8bbdb258196",
"reference": "ddff0ac01beddc251786fe70367cd8bbdb258196",
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/a4f766e5c5b6773d8399711019bb7d90875a50ee",
"reference": "a4f766e5c5b6773d8399711019bb7d90875a50ee",
"shasum": ""
},
"require": {
@@ -6599,9 +6599,9 @@
],
"support": {
"issues": "https://github.com/bobthecow/psysh/issues",
"source": "https://github.com/bobthecow/psysh/tree/v0.12.18"
"source": "https://github.com/bobthecow/psysh/tree/v0.12.19"
},
"time": "2025-12-17T14:35:46+00:00"
"time": "2026-01-30T17:33:13+00:00"
},
{
"name": "ralouphie/getallheaders",
@@ -7494,16 +7494,16 @@
},
{
"name": "spatie/laravel-data",
"version": "4.19.0",
"version": "4.19.1",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-data.git",
"reference": "33ea9c6359015415bc46138eb256051ceff24a2e"
"reference": "41ed0472250676f19440fb24d7b62a8d43abdb89"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-data/zipball/33ea9c6359015415bc46138eb256051ceff24a2e",
"reference": "33ea9c6359015415bc46138eb256051ceff24a2e",
"url": "https://api.github.com/repos/spatie/laravel-data/zipball/41ed0472250676f19440fb24d7b62a8d43abdb89",
"reference": "41ed0472250676f19440fb24d7b62a8d43abdb89",
"shasum": ""
},
"require": {
@@ -7564,7 +7564,7 @@
],
"support": {
"issues": "https://github.com/spatie/laravel-data/issues",
"source": "https://github.com/spatie/laravel-data/tree/4.19.0"
"source": "https://github.com/spatie/laravel-data/tree/4.19.1"
},
"funding": [
{
@@ -7572,7 +7572,7 @@
"type": "github"
}
],
"time": "2026-01-19T09:47:31+00:00"
"time": "2026-01-28T13:10:20+00:00"
},
{
"name": "spatie/laravel-fractal",
@@ -8863,16 +8863,16 @@
},
{
"name": "symfony/finder",
"version": "v7.4.4",
"version": "v7.4.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "01b24a145bbeaa7141e75887ec904c34a6728a5f"
"reference": "ad4daa7c38668dcb031e63bc99ea9bd42196a2cb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/01b24a145bbeaa7141e75887ec904c34a6728a5f",
"reference": "01b24a145bbeaa7141e75887ec904c34a6728a5f",
"url": "https://api.github.com/repos/symfony/finder/zipball/ad4daa7c38668dcb031e63bc99ea9bd42196a2cb",
"reference": "ad4daa7c38668dcb031e63bc99ea9bd42196a2cb",
"shasum": ""
},
"require": {
@@ -8907,7 +8907,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/finder/tree/v7.4.4"
"source": "https://github.com/symfony/finder/tree/v7.4.5"
},
"funding": [
{
@@ -8927,7 +8927,7 @@
"type": "tidelift"
}
],
"time": "2026-01-12T12:19:02+00:00"
"time": "2026-01-26T15:07:59+00:00"
},
{
"name": "symfony/html-sanitizer",
@@ -9005,16 +9005,16 @@
},
{
"name": "symfony/http-client",
"version": "v7.4.4",
"version": "v7.4.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
"reference": "d63c23357d74715a589454c141c843f0172bec6c"
"reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/d63c23357d74715a589454c141c843f0172bec6c",
"reference": "d63c23357d74715a589454c141c843f0172bec6c",
"url": "https://api.github.com/repos/symfony/http-client/zipball/84bb634857a893cc146cceb467e31b3f02c5fe9f",
"reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f",
"shasum": ""
},
"require": {
@@ -9082,7 +9082,7 @@
"http"
],
"support": {
"source": "https://github.com/symfony/http-client/tree/v7.4.4"
"source": "https://github.com/symfony/http-client/tree/v7.4.5"
},
"funding": [
{
@@ -9102,7 +9102,7 @@
"type": "tidelift"
}
],
"time": "2026-01-23T16:34:22+00:00"
"time": "2026-01-27T16:16:02+00:00"
},
{
"name": "symfony/http-client-contracts",
@@ -9184,16 +9184,16 @@
},
{
"name": "symfony/http-foundation",
"version": "v7.4.4",
"version": "v7.4.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
"reference": "977a554a34cf8edc95ca351fbecb1bb1ad05cc94"
"reference": "446d0db2b1f21575f1284b74533e425096abdfb6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/977a554a34cf8edc95ca351fbecb1bb1ad05cc94",
"reference": "977a554a34cf8edc95ca351fbecb1bb1ad05cc94",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/446d0db2b1f21575f1284b74533e425096abdfb6",
"reference": "446d0db2b1f21575f1284b74533e425096abdfb6",
"shasum": ""
},
"require": {
@@ -9242,7 +9242,7 @@
"description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/http-foundation/tree/v7.4.4"
"source": "https://github.com/symfony/http-foundation/tree/v7.4.5"
},
"funding": [
{
@@ -9262,20 +9262,20 @@
"type": "tidelift"
}
],
"time": "2026-01-09T12:14:21+00:00"
"time": "2026-01-27T16:16:02+00:00"
},
{
"name": "symfony/http-kernel",
"version": "v7.4.4",
"version": "v7.4.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
"reference": "48b067768859f7b68acf41dfb857a5a4be00acdd"
"reference": "229eda477017f92bd2ce7615d06222ec0c19e82a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/48b067768859f7b68acf41dfb857a5a4be00acdd",
"reference": "48b067768859f7b68acf41dfb857a5a4be00acdd",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/229eda477017f92bd2ce7615d06222ec0c19e82a",
"reference": "229eda477017f92bd2ce7615d06222ec0c19e82a",
"shasum": ""
},
"require": {
@@ -9361,7 +9361,7 @@
"description": "Provides a structured process for converting a Request into a Response",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/http-kernel/tree/v7.4.4"
"source": "https://github.com/symfony/http-kernel/tree/v7.4.5"
},
"funding": [
{
@@ -9381,7 +9381,7 @@
"type": "tidelift"
}
],
"time": "2026-01-24T22:13:01+00:00"
"time": "2026-01-28T10:33:42+00:00"
},
{
"name": "symfony/mailer",
@@ -9542,16 +9542,16 @@
},
{
"name": "symfony/mime",
"version": "v7.4.4",
"version": "v7.4.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
"reference": "40945014c0a9471ccfe19673c54738fa19367a3c"
"reference": "b18c7e6e9eee1e19958138df10412f3c4c316148"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mime/zipball/40945014c0a9471ccfe19673c54738fa19367a3c",
"reference": "40945014c0a9471ccfe19673c54738fa19367a3c",
"url": "https://api.github.com/repos/symfony/mime/zipball/b18c7e6e9eee1e19958138df10412f3c4c316148",
"reference": "b18c7e6e9eee1e19958138df10412f3c4c316148",
"shasum": ""
},
"require": {
@@ -9562,15 +9562,15 @@
},
"conflict": {
"egulias/email-validator": "~3.0.0",
"phpdocumentor/reflection-docblock": "<3.2.2",
"phpdocumentor/type-resolver": "<1.4.0",
"phpdocumentor/reflection-docblock": "<5.2|>=6",
"phpdocumentor/type-resolver": "<1.5.1",
"symfony/mailer": "<6.4",
"symfony/serializer": "<6.4.3|>7.0,<7.0.3"
},
"require-dev": {
"egulias/email-validator": "^2.1.10|^3.1|^4",
"league/html-to-markdown": "^5.0",
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
"phpdocumentor/reflection-docblock": "^5.2",
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
"symfony/process": "^6.4|^7.0|^8.0",
"symfony/property-access": "^6.4|^7.0|^8.0",
@@ -9607,7 +9607,7 @@
"mime-type"
],
"support": {
"source": "https://github.com/symfony/mime/tree/v7.4.4"
"source": "https://github.com/symfony/mime/tree/v7.4.5"
},
"funding": [
{
@@ -9627,7 +9627,7 @@
"type": "tidelift"
}
],
"time": "2026-01-08T16:12:55+00:00"
"time": "2026-01-27T08:59:58+00:00"
},
{
"name": "symfony/polyfill-ctype",
@@ -10534,16 +10534,16 @@
},
{
"name": "symfony/process",
"version": "v7.4.4",
"version": "v7.4.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "626f07a53f4b4e2f00e11824cc29f928d797783b"
"reference": "608476f4604102976d687c483ac63a79ba18cc97"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/626f07a53f4b4e2f00e11824cc29f928d797783b",
"reference": "626f07a53f4b4e2f00e11824cc29f928d797783b",
"url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97",
"reference": "608476f4604102976d687c483ac63a79ba18cc97",
"shasum": ""
},
"require": {
@@ -10575,7 +10575,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v7.4.4"
"source": "https://github.com/symfony/process/tree/v7.4.5"
},
"funding": [
{
@@ -10595,7 +10595,7 @@
"type": "tidelift"
}
],
"time": "2026-01-20T09:23:51+00:00"
"time": "2026-01-26T15:07:59+00:00"
},
{
"name": "symfony/routing",
@@ -13415,11 +13415,11 @@
},
{
"name": "phpstan/phpstan",
"version": "2.1.37",
"version": "2.1.38",
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/28cd424c5ea984128c95cfa7ea658808e8954e49",
"reference": "28cd424c5ea984128c95cfa7ea658808e8954e49",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/dfaf1f530e1663aa167bc3e52197adb221582629",
"reference": "dfaf1f530e1663aa167bc3e52197adb221582629",
"shasum": ""
},
"require": {
@@ -13464,7 +13464,7 @@
"type": "github"
}
],
"time": "2026-01-24T08:21:55+00:00"
"time": "2026-01-30T17:12:46+00:00"
},
{
"name": "phpunit/php-code-coverage",
@@ -15332,24 +15332,24 @@
},
{
"name": "ta-tikoma/phpunit-architecture-test",
"version": "0.8.5",
"version": "0.8.6",
"source": {
"type": "git",
"url": "https://github.com/ta-tikoma/phpunit-architecture-test.git",
"reference": "cf6fb197b676ba716837c886baca842e4db29005"
"reference": "ad48430b92901fd7d003fdaf2d7b139f96c0906e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/cf6fb197b676ba716837c886baca842e4db29005",
"reference": "cf6fb197b676ba716837c886baca842e4db29005",
"url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/ad48430b92901fd7d003fdaf2d7b139f96c0906e",
"reference": "ad48430b92901fd7d003fdaf2d7b139f96c0906e",
"shasum": ""
},
"require": {
"nikic/php-parser": "^4.18.0 || ^5.0.0",
"php": "^8.1.0",
"phpdocumentor/reflection-docblock": "^5.3.0",
"phpunit/phpunit": "^10.5.5 || ^11.0.0 || ^12.0.0",
"symfony/finder": "^6.4.0 || ^7.0.0"
"phpdocumentor/reflection-docblock": "^5.3.0 || ^6.0.0",
"phpunit/phpunit": "^10.5.5 || ^11.0.0 || ^12.0.0",
"symfony/finder": "^6.4.0 || ^7.0.0 || ^8.0.0"
},
"require-dev": {
"laravel/pint": "^1.13.7",
@@ -15385,9 +15385,9 @@
],
"support": {
"issues": "https://github.com/ta-tikoma/phpunit-architecture-test/issues",
"source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.5"
"source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.6"
},
"time": "2025-04-20T20:23:40+00:00"
"time": "2026-01-30T07:16:00+00:00"
},
{
"name": "theseer/tokenizer",
@@ -15457,5 +15457,5 @@
"platform-overrides": {
"php": "8.2"
},
"plugin-api-version": "2.6.0"
"plugin-api-version": "2.9.0"
}

View File

@@ -65,6 +65,20 @@ if [ "${BEHIND_PROXY}" == "true" ]; then
export PARSED_APP_URL=":80"
export PARSED_AUTO_HTTPS="auto_https off"
export ASSET_URL=${APP_URL}
# Write ASSET_URL to .env so PHP-FPM workers can read it (clear_env = yes by default)
ENV_FILE="/var/www/html/.env"
if [ -e "$ENV_FILE" ]; then
ENV_FILE="$(readlink -f "$ENV_FILE")"
else
ENV_FILE="/pelican-data/.env"
fi
ESCAPED_APP_URL="$(printf '%s' "$APP_URL" | sed 's/[&|]/\\&/g')"
if grep -q "^ASSET_URL=" "$ENV_FILE"; then
sed -i "s|^ASSET_URL=.*|ASSET_URL=${ESCAPED_APP_URL}|" "$ENV_FILE"
else
echo "ASSET_URL=${APP_URL}" >> "$ENV_FILE"
fi
fi
# disable caddy if SKIP_CADDY is set

View File

@@ -20,6 +20,8 @@ return [
'never_used' => 'Never Used',
],
'permissions' => [
'all' => 'Set All Permissions',
'all_description' => 'Quickly set all permissions below to the same level.',
'none' => 'None',
'read' => 'Read',
'read_write' => 'Read & Write',

View File

@@ -115,10 +115,6 @@ return [
'no_update_url' => 'The following eggs do not have a working update URL set: :eggs',
'cannot_delete' => 'Cannot delete :count egg(s)',
'eggs_have_servers' => 'The following eggs have servers and cannot be deleted: :eggs',
'delete_success' => 'Egg deleted successfully',
'deleted' => 'Deleted: :egg',
'delete_failed' => 'Failed to delete egg',
'could_not_delete' => 'Could not delete: :egg',
'updated_from' => 'Successfully updated from: :url',
'update_error' => 'Error: :error',
'updated_eggs' => 'Updated: :eggs',

View File

@@ -65,6 +65,8 @@ return [
'sftp_port' => 'SFTP Port',
'sftp_alias' => 'SFTP Alias',
'sftp_alias_help' => 'Display alias for the SFTP address. Leave empty to use the Node FQDN.',
'daemon_base' => 'Daemon Base Directory',
'daemon_base_help' => 'The directory where server data will be stored.',
'use_for_deploy' => 'Use for Deployments?',
'maintenance_mode' => 'Maintenance Mode',
'maintenance_mode_help' => 'If the node is marked \'Under Maintenance\' users won\'t be able to access servers that are on that node',

View File

@@ -147,44 +147,8 @@ return [
'transfer_failed' => 'Transfer failed',
'already_transfering' => 'Server is currently being transferred.',
'backup_transfer_failed' => 'Backup Transfer Failed',
'import_created' => 'Server Created',
'import_created_body' => 'Server \':name\' has been successfully created from configuration.',
'import_failed' => 'Import Failed',
'import_failed_body' => 'An unexpected error occurred: :error',
],
'notes' => 'Notes',
'no_notes' => 'No Notes',
'none' => 'None',
'import_export' => [
'import_tooltip' => 'Import server configuration from YAML file',
'import_heading' => 'Import Server Configuration',
'import_description' => 'Import server configuration from a YAML file to create a new server.',
'export_tooltip' => 'Export server configuration to YAML file',
'export_heading' => 'Export Configuration: :name',
'export_description' => 'Export the server configuration, settings, limits, allocations, and variable values to a YAML file.',
'config_file' => 'Configuration File',
'config_file_hint' => 'Upload a YAML file exported from another panel',
'node_select' => 'Node',
'node_select_hint' => 'Select the node where the server will be created',
'include_description' => 'Include Description?',
'include_description_help' => 'Export the server description',
'include_allocations' => 'Include Allocations?',
'include_allocations_help' => 'Export IP addresses and ports assigned to the server',
'include_variables' => 'Include Variable Values?',
'include_variables_help' => 'Export environment variable values',
],
'import_errors' => [
'file_error' => 'The selected file was not uploaded successfully',
'file_error_desc' => 'Please check the file and try again',
'parse_error' => 'Could not parse YAML file',
'parse_error_desc' => 'The uploaded file is not valid YAML: :error',
'egg_uuid_required' => 'Egg UUID is required in the configuration file',
'egg_not_found' => 'Egg does not exist in the panel',
'egg_not_found_desc' => 'Egg with UUID \':uuid\' (name: :name) does not exist in the panel',
'node_not_accessible' => 'Selected node is not accessible or does not exist',
'no_nodes' => 'No accessible nodes found',
'no_user' => 'No authenticated user found',
'port_exhausted' => 'Could not find an available port',
'port_exhausted_desc' => 'Could not find an available port for IP :ip starting from port :port',
],
];

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
function o({isSkippable:s,isStepPersistedInQueryString:i,key:r,startStep:h,stepQueryStringKey:n}){return{step:null,init(){this.$watch("step",()=>this.updateQueryString()),this.step=this.getSteps().at(h-1),this.autofocusFields()},async requestNextStep(){await this.$wire.callSchemaComponentMethod(r,"nextStep",{currentStepIndex:this.getStepIndex(this.step)})},goToNextStep(){let t=this.getStepIndex(this.step)+1;t>=this.getSteps().length||(this.step=this.getSteps()[t],this.autofocusFields(),this.scroll())},goToPreviousStep(){let t=this.getStepIndex(this.step)-1;t<0||(this.step=this.getSteps()[t],this.autofocusFields(),this.scroll())},scroll(){this.$nextTick(()=>{this.$refs.header?.children[this.getStepIndex(this.step)].scrollIntoView({behavior:"smooth",block:"start"})})},autofocusFields(){this.$nextTick(()=>this.$refs[`step-${this.step}`].querySelector("[autofocus]")?.focus())},getStepIndex(t){let e=this.getSteps().findIndex(p=>p===t);return e===-1?0:e},getSteps(){return JSON.parse(this.$refs.stepsData.value)},isFirstStep(){return this.getStepIndex(this.step)<=0},isLastStep(){return this.getStepIndex(this.step)+1>=this.getSteps().length},isStepAccessible(t){return s||this.getStepIndex(this.step)>this.getStepIndex(t)},updateQueryString(){if(!i)return;let t=new URL(window.location.href);t.searchParams.set(n,this.step),history.replaceState(null,document.title,t.toString())}}}export{o as default};
function o({isSkippable:s,isStepPersistedInQueryString:i,key:r,startStep:h,stepQueryStringKey:n}){return{step:null,init(){this.$watch("step",()=>this.updateQueryString()),this.step=this.getSteps().at(h-1),this.autofocusFields()},async requestNextStep(){await this.$wire.callSchemaComponentMethod(r,"nextStep",{currentStepIndex:this.getStepIndex(this.step)})},goToNextStep(){let t=this.getStepIndex(this.step)+1;t>=this.getSteps().length||(this.step=this.getSteps()[t],this.autofocusFields(),this.scroll())},goToPreviousStep(){let t=this.getStepIndex(this.step)-1;t<0||(this.step=this.getSteps()[t],this.autofocusFields(),this.scroll())},goToStep(t){let e=this.getStepIndex(t);e<=-1||!s&&e>this.getStepIndex(this.step)||(this.step=t,this.autofocusFields(),this.scroll())},scroll(){this.$nextTick(()=>{this.$refs.header?.children[this.getStepIndex(this.step)].scrollIntoView({behavior:"smooth",block:"start"})})},autofocusFields(){this.$nextTick(()=>this.$refs[`step-${this.step}`].querySelector("[autofocus]")?.focus())},getStepIndex(t){let e=this.getSteps().findIndex(p=>p===t);return e===-1?0:e},getSteps(){return JSON.parse(this.$refs.stepsData.value)},isFirstStep(){return this.getStepIndex(this.step)<=0},isLastStep(){return this.getStepIndex(this.step)+1>=this.getSteps().length},isStepAccessible(t){return s||this.getStepIndex(this.step)>this.getStepIndex(t)},updateQueryString(){if(!i)return;let t=new URL(window.location.href);t.searchParams.set(n,this.step),history.replaceState(null,document.title,t.toString())}}}export{o as default};

File diff suppressed because one or more lines are too long

View File

@@ -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="
{

View File

@@ -75,12 +75,10 @@ Route::prefix('/servers')->group(function () {
Route::post('/{server:id}/transfer', [Application\Servers\ServerManagementController::class, 'startTransfer'])->name('api.application.servers.transfer');
Route::post('/{server:id}/transfer/cancel', [Application\Servers\ServerManagementController::class, 'cancelTransfer'])->name('api.application.servers.transfer.cancel');
Route::get('/{server:id}/config/export', [Application\Servers\ServerConfigController::class, 'export'])->name('api.application.servers.config.export');
Route::post('/config/create', [Application\Servers\ServerConfigController::class, 'create'])->name('api.application.servers.config.create');
Route::delete('/{server:id}', [Application\Servers\ServerController::class, 'delete']);
Route::delete('/{server:id}/{force?}', [Application\Servers\ServerController::class, 'delete']);
// Database Management Endpoint
Route::prefix('/{server:id}/databases')->group(function () {
Route::get('/', [Application\Servers\DatabaseController::class, 'index'])->name('api.application.servers.databases');
Route::get('/{database:id}', [Application\Servers\DatabaseController::class, 'view'])->name('api.application.servers.databases.view');
@@ -174,3 +172,26 @@ Route::prefix('/roles')->group(function () {
Route::delete('/{role:id}', [Application\Roles\RoleController::class, 'delete']);
});
/*
|--------------------------------------------------------------------------
| Plugin Controller Routes
|--------------------------------------------------------------------------
|
| Endpoint: /api/application/plugins
|
*/
Route::prefix('/plugins')->group(function () {
Route::get('/', [Application\Plugins\PluginController::class, 'index'])->name('api.application.plugins');
Route::get('/{plugin:id}', [Application\Plugins\PluginController::class, 'view'])->name('api.application.plugins.view');
Route::post('/import/file', [Application\Plugins\PluginController::class, 'importFile']);
Route::post('/import/url', [Application\Plugins\PluginController::class, 'importUrl']);
Route::post('/{plugin:id}/install', [Application\Plugins\PluginController::class, 'install']);
Route::post('/{plugin:id}/update', [Application\Plugins\PluginController::class, 'update']);
Route::post('/{plugin:id}/uninstall', [Application\Plugins\PluginController::class, 'uninstall']);
Route::post('/{plugin:id}/enable', [Application\Plugins\PluginController::class, 'enable']);
Route::post('/{plugin:id}/disable', [Application\Plugins\PluginController::class, 'disable']);
});