Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot]
27c5167bfe ci(release): bump version 2025-03-15 20:22:50 +00:00
214 changed files with 2041 additions and 2903 deletions

View File

@@ -3,4 +3,5 @@ APP_DEBUG=false
APP_KEY=
APP_URL=http://panel.test
APP_INSTALLED=false
APP_TIMEZONE=UTC
APP_LOCALE=en

15
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,15 @@
# Lines starting with '#' are comments.
# Each line is a file pattern followed by one or more owners.
# More details are here: https://help.github.com/articles/about-codeowners/
# The '*' pattern is global owners.
# Order is important. The last matching pattern has the most precedence.
# The folders are ordered as follows:
# In each subsection folders are ordered first by depth, then alphabetically.
# This should make it easy to add new rules without breaking existing ones.
# Global
* @pelican-dev/panel

View File

@@ -213,79 +213,3 @@ jobs:
- name: Integration tests
run: vendor/bin/pest tests/Integration
postgresql:
name: PostgreSQL
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: [8.2, 8.3, 8.4]
database: ["postgres:14"]
services:
database:
image: ${{ matrix.database }}
env:
POSTGRES_DB: testing
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
APP_ENV: testing
APP_DEBUG: "false"
APP_KEY: ThisIsARandomStringForTests12345
APP_TIMEZONE: UTC
APP_URL: http://localhost/
CACHE_DRIVER: array
MAIL_MAILER: array
SESSION_DRIVER: array
QUEUE_CONNECTION: sync
DB_CONNECTION: pgsql
DB_HOST: 127.0.0.1
DB_DATABASE: testing
DB_USERNAME: postgres
DB_PASSWORD: postgres
GUZZLE_TIMEOUT: 60
GUZZLE_CONNECT_TIMEOUT: 60
steps:
- name: Code Checkout
uses: actions/checkout@v4
- name: Get cache directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
restore-keys: |
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none
- name: Install dependencies
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
- name: Unit tests
run: vendor/bin/pest tests/Unit
env:
DB_HOST: UNIT_NO_DB
SKIP_MIGRATIONS: true
- name: Integration tests
run: vendor/bin/pest tests/Integration

View File

@@ -35,7 +35,7 @@ class RedisSetupCommand extends Command
{
$this->variables['CACHE_STORE'] = 'redis';
$this->variables['QUEUE_CONNECTION'] = 'redis';
$this->variables['SESSION_DRIVER'] = 'redis';
$this->variables['SESSION_DRIVERS'] = 'redis';
$this->requestRedisSettings();

View File

@@ -6,7 +6,6 @@ use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory;
use SplFileInfo;
class CleanServiceBackupFilesCommand extends Command
{
@@ -33,10 +32,9 @@ class CleanServiceBackupFilesCommand extends Command
*/
public function handle(): void
{
/** @var SplFileInfo[] */
$files = $this->disk->files('services/.bak');
collect($files)->each(function ($file) {
collect($files)->each(function (\SplFileInfo $file) {
$lastModified = Carbon::createFromTimestamp($this->disk->lastModified($file->getPath()));
if ($lastModified->diffInMinutes(Carbon::now()) > self::BACKUP_THRESHOLD_MINUTES) {
$this->disk->delete($file->getPath());

View File

@@ -2,8 +2,8 @@
namespace App\Console\Commands\Node;
use App\Models\Node;
use Illuminate\Console\Command;
use App\Services\Nodes\NodeCreationService;
class MakeNodeCommand extends Command
{
@@ -30,6 +30,14 @@ class MakeNodeCommand extends Command
protected $description = 'Creates a new node on the system via the CLI.';
/**
* MakeNodeCommand constructor.
*/
public function __construct(private NodeCreationService $creationService)
{
parent::__construct();
}
/**
* Handle the command execution process.
*
@@ -61,7 +69,7 @@ class MakeNodeCommand extends Command
$data['daemon_sftp_alias'] = $this->option('daemonSFTPAlias') ?? $this->ask(trans('commands.make_node.daemonSFTPAlias'), '');
$data['daemon_base'] = $this->option('daemonBase') ?? $this->ask(trans('commands.make_node.daemonBase'), '/var/lib/pelican/volumes');
$node = Node::create($data);
$node = $this->creationService->handle($data);
$this->line(trans('commands.make_node.success', ['name' => $data['name'], 'id' => $node->id]));
}
}

View File

@@ -1,11 +0,0 @@
<?php
namespace App\Enums;
enum ConsoleWidgetPosition: string
{
case Top = 'top';
case AboveConsole = 'above_console';
case BelowConsole = 'below_console';
case Bottom = 'bottom';
}

View File

@@ -62,7 +62,7 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
self::Removing => 'warning',
self::Missing => 'danger',
self::Stopping => 'warning',
self::Offline => 'danger',
self::Offline => 'gray',
};
}

View File

@@ -1,7 +0,0 @@
<?php
namespace App\Exceptions\Repository;
use Exception;
class FileNotEditableException extends Exception {}

View File

@@ -1,40 +0,0 @@
<?php
namespace App\Extensions\Avatar;
use Filament\AvatarProviders\Contracts\AvatarProvider as AvatarProviderContract;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
abstract class AvatarProvider implements AvatarProviderContract
{
/**
* @var array<string, static>
*/
protected static array $providers = [];
public static function getProvider(string $id): ?self
{
return Arr::get(static::$providers, $id);
}
/**
* @return array<string, static>
*/
public static function getAll(): array
{
return static::$providers;
}
public function __construct()
{
static::$providers[$this->getId()] = $this;
}
abstract public function getId(): string;
public function getName(): string
{
return Str::title($this->getId());
}
}

View File

@@ -1,27 +0,0 @@
<?php
namespace App\Extensions\Avatar\Providers;
use App\Extensions\Avatar\AvatarProvider;
use App\Models\User;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
class GravatarProvider extends AvatarProvider
{
public function getId(): string
{
return 'gravatar';
}
public function get(Model|Authenticatable $record): string
{
/** @var User $record */
return 'https://gravatar.com/avatar/' . md5($record->email);
}
public static function register(): self
{
return new self();
}
}

View File

@@ -1,29 +0,0 @@
<?php
namespace App\Extensions\Avatar\Providers;
use App\Extensions\Avatar\AvatarProvider;
use Filament\AvatarProviders\UiAvatarsProvider as FilamentUiAvatarsProvider;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
class LocalAvatarProvider extends AvatarProvider
{
public function getId(): string
{
return 'local';
}
public function get(Model|Authenticatable $record): string
{
$path = 'avatars/' . $record->getKey() . '.png';
return Storage::disk('public')->exists($path) ? Storage::url($path) : (new FilamentUiAvatarsProvider())->get($record);
}
public static function register(): self
{
return new self();
}
}

View File

@@ -1,31 +0,0 @@
<?php
namespace App\Extensions\Avatar\Providers;
use App\Extensions\Avatar\AvatarProvider;
use Filament\AvatarProviders\UiAvatarsProvider as FilamentUiAvatarsProvider;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
class UiAvatarsProvider extends AvatarProvider
{
public function getId(): string
{
return 'uiavatars';
}
public function getName(): string
{
return 'UI Avatars';
}
public function get(Model|Authenticatable $record): string
{
return (new FilamentUiAvatarsProvider())->get($record);
}
public static function register(): self
{
return new self();
}
}

View File

@@ -6,8 +6,8 @@ use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
use Illuminate\Support\Str;
use SocialiteProviders\Discord\Provider;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
@@ -34,15 +34,15 @@ final class DiscordProvider extends OAuthProvider
Step::make('Register new Discord OAuth App')
->schema([
Placeholder::make('')
->content(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://discord.com/developers/applications" target="_blank">Discord Developer Portal</x-filament::link> and click on <b>New Application</b>. Enter a <b>Name</b> (e.g. your panel name) and click on <b>Create</b>.</p><p>Copy the <b>Client ID</b> and the <b>Client Secret</b> from the OAuth2 tab, you will need them in the final step.</p>'))),
->content(new HtmlString('<p>Visit the <u><a href="https://discord.com/developers/applications" target="_blank">Discord Developer Portal</a></u> and click on <b>New Application</b>. Enter a <b>Name</b> (e.g. your panel name) and click on <b>Create</b>.</p><p>Copy the <b>Client ID</b> and the <b>Client Secret</b>, you will need them in the final step.</p>')),
Placeholder::make('')
->content(new HtmlString('<p>Under <b>Redirects</b> add the below URL.</p>')),
TextInput::make('_noenv_callback')
->label('Redirect URL')
->dehydrated()
->disabled()
->hintAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
->formatStateUsing(fn () => url('/auth/oauth/callback/discord')),
->hintAction(fn () => request()->isSecure() ? CopyAction::make() : null)
->formatStateUsing(fn () => config('app.url') . (Str::endsWith(config('app.url'), '/') ? '' : '/') . 'auth/oauth/callback/discord'),
]),
], parent::getSetupSteps());
}

View File

@@ -6,8 +6,8 @@ use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
use Illuminate\Support\Str;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
final class GithubProvider extends OAuthProvider
@@ -28,13 +28,13 @@ final class GithubProvider extends OAuthProvider
Step::make('Register new Github OAuth App')
->schema([
Placeholder::make('')
->content(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://github.com/settings/developers" target="_blank">Github Developer Dashboard</x-filament::link>, go to <b>OAuth Apps</b> and click on <b>New OAuth App</b>.</p><p>Enter an <b>Application name</b> (e.g. your panel name), set <b>Homepage URL</b> to your panel url and enter the below url as <b>Authorization callback URL</b>.</p>'))),
->content(new HtmlString('<p>Visit the <u><a href="https://github.com/settings/developers" target="_blank">Github Developer Dashboard</a></u>, go to <b>OAuth Apps</b> and click on <b>New OAuth App</b>.</p><p>Enter an <b>Application name</b> (e.g. your panel name), set <b>Homepage URL</b> to your panel url and enter the below url as <b>Authorization callback URL</b>.</p>')),
TextInput::make('_noenv_callback')
->label('Authorization callback URL')
->dehydrated()
->disabled()
->hintAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
->default(fn () => url('/auth/oauth/callback/github')),
->hintAction(fn () => request()->isSecure() ? CopyAction::make() : null)
->default(fn () => config('app.url') . (Str::endsWith(config('app.url'), '/') ? '' : '/') . 'auth/oauth/callback/github'),
Placeholder::make('')
->content(new HtmlString('<p>When you filled all fields click on <b>Register application</b>.</p>')),
]),

View File

@@ -1,76 +0,0 @@
<?php
namespace App\Extensions\OAuth\Providers;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
final class GitlabProvider extends OAuthProvider
{
public function __construct(protected Application $app)
{
parent::__construct($app);
}
public function getId(): string
{
return 'gitlab';
}
public function getServiceConfig(): array
{
return array_merge(parent::getServiceConfig(), [
'host' => env('OAUTH_GITLAB_HOST'),
]);
}
public function getSettingsForm(): array
{
return array_merge(parent::getSettingsForm(), [
TextInput::make('OAUTH_GITLAB_HOST')
->label('Custom Host')
->placeholder('Only set a custom host if you are self hosting gitlab')
->columnSpan(2)
->url()
->autocomplete(false)
->default(env('OAUTH_GITLAB_HOST')),
]);
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new Gitlab OAuth App')
->schema([
Placeholder::make('')
->content(new HtmlString(Blade::render('Check out the <x-filament::link href="https://docs.gitlab.com/integration/oauth_provider/" target="_blank">Gitlab docs</x-filament::link> on how to create the oauth app.'))),
TextInput::make('_noenv_callback')
->label('Redirect URI')
->dehydrated()
->disabled()
->hintAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
->default(fn () => url('/auth/oauth/callback/gitlab')),
]),
], parent::getSetupSteps());
}
public function getIcon(): string
{
return 'tabler-brand-gitlab';
}
public function getHexColor(): string
{
return '#fca326';
}
public static function register(Application $app): self
{
return new self($app);
}
}

View File

@@ -6,7 +6,6 @@ use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
use SocialiteProviders\Steam\Provider;
@@ -59,7 +58,7 @@ final class SteamProvider extends OAuthProvider
Step::make('Create API Key')
->schema([
Placeholder::make('')
->content(new HtmlString(Blade::render('Visit <x-filament::link href="https://steamcommunity.com/dev/apikey" target="_blank">https://steamcommunity.com/dev/apikey</x-filament::link> to generate an API key.'))),
->content(new HtmlString('Visit <u><a href="https://steamcommunity.com/dev/apikey" target="_blank">https://steamcommunity.com/dev/apikey</a></u> to generate an API key.')),
]),
], parent::getSetupSteps());
}

View File

@@ -2,13 +2,36 @@
namespace App\Filament\Admin\Pages;
use App\Filament\Admin\Resources\NodeResource\Pages\CreateNode;
use App\Filament\Admin\Resources\NodeResource\Pages\ListNodes;
use App\Models\Egg;
use App\Models\Node;
use App\Models\Server;
use App\Models\User;
use App\Services\Helpers\SoftwareVersionService;
use Filament\Pages\Dashboard as BaseDashboard;
use Filament\Actions\CreateAction;
use Filament\Pages\Page;
class Dashboard extends BaseDashboard
class Dashboard extends Page
{
protected static ?string $navigationIcon = 'tabler-layout-dashboard';
protected static string $view = 'filament.pages.dashboard';
protected ?string $heading = '';
public function getTitle(): string
{
return trans('admin/dashboard.title');
}
public static function getNavigationLabel(): string
{
return trans('admin/dashboard.title');
}
protected static ?string $slug = '/';
private SoftwareVersionService $softwareVersionService;
public function mount(SoftwareVersionService $softwareVersionService): void
@@ -16,18 +39,51 @@ class Dashboard extends BaseDashboard
$this->softwareVersionService = $softwareVersionService;
}
public function getColumns(): int
public function getViewData(): array
{
return 1;
}
return [
'inDevelopment' => config('app.version') === 'canary',
'version' => $this->softwareVersionService->currentPanelVersion(),
'latestVersion' => $this->softwareVersionService->latestPanelVersion(),
'isLatest' => $this->softwareVersionService->isLatestPanel(),
'eggsCount' => Egg::query()->count(),
'nodesList' => ListNodes::getUrl(),
'nodesCount' => Node::query()->count(),
'serversCount' => Server::query()->count(),
'usersCount' => User::query()->count(),
public function getHeading(): string
{
return trans('admin/dashboard.heading');
}
public function getSubheading(): string
{
return trans('admin/dashboard.version', ['version' => $this->softwareVersionService->currentPanelVersion()]);
'devActions' => [
CreateAction::make()
->label(trans('admin/dashboard.sections.intro-developers.button_issues'))
->icon('tabler-brand-github')
->url('https://github.com/pelican-dev/panel/issues', true),
],
'updateActions' => [
CreateAction::make()
->label(trans('admin/dashboard.sections.intro-update-available.heading'))
->icon('tabler-clipboard-text')
->url('https://pelican.dev/docs/panel/update', true)
->color('warning'),
],
'nodeActions' => [
CreateAction::make()
->label(trans('admin/dashboard.sections.intro-first-node.button_label'))
->icon('tabler-server-2')
->url(CreateNode::getUrl()),
],
'supportActions' => [
CreateAction::make()
->label(trans('admin/dashboard.sections.intro-support.button_donate'))
->icon('tabler-cash')
->url('https://pelican.dev/donate', true)
->color('success'),
],
'helpActions' => [
CreateAction::make()
->label(trans('admin/dashboard.sections.intro-help.button_docs'))
->icon('tabler-speedboat')
->url('https://pelican.dev/docs', true),
],
];
}
}

View File

@@ -2,7 +2,6 @@
namespace App\Filament\Admin\Pages;
use App\Extensions\Avatar\AvatarProvider;
use App\Extensions\Captcha\Providers\CaptchaProvider;
use App\Extensions\OAuth\Providers\OAuthProvider;
use App\Models\Backup;
@@ -118,55 +117,32 @@ class Settings extends Page implements HasForms
->label(trans('admin/setting.general.app_name'))
->required()
->default(env('APP_NAME', 'Pelican')),
Group::make()
->columns(2)
->schema([
TextInput::make('APP_LOGO')
->label(trans('admin/setting.general.app_logo'))
->hintIcon('tabler-question-mark')
->hintIconTooltip(trans('admin/setting.general.app_logo_help'))
->default(env('APP_LOGO'))
->placeholder('/pelican.svg'),
TextInput::make('APP_FAVICON')
->label(trans('admin/setting.general.app_favicon'))
->hintIcon('tabler-question-mark')
->hintIconTooltip(trans('admin/setting.general.app_favicon_help'))
->required()
->default(env('APP_FAVICON', '/pelican.ico'))
->placeholder('/pelican.ico'),
]),
Group::make()
->columnSpan(2)
->columns(4)
->schema([
Toggle::make('APP_DEBUG')
->label(trans('admin/setting.general.debug_mode'))
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('APP_DEBUG', (bool) $state))
->default(env('APP_DEBUG', config('app.debug'))),
ToggleButtons::make('FILAMENT_TOP_NAVIGATION')
->label(trans('admin/setting.general.navigation'))
->inline()
->options([
false => trans('admin/setting.general.sidebar'),
true => trans('admin/setting.general.topbar'),
])
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('FILAMENT_TOP_NAVIGATION', (bool) $state))
->default(env('FILAMENT_TOP_NAVIGATION', config('panel.filament.top-navigation'))),
Select::make('FILAMENT_AVATAR_PROVIDER')
->label(trans('admin/setting.general.avatar_provider'))
->columnSpan(2)
->native(false)
->options(collect(AvatarProvider::getAll())->mapWithKeys(fn ($provider) => [$provider->getId() => $provider->getName()]))
->selectablePlaceholder(false)
->default(env('FILAMENT_AVATAR_PROVIDER', config('panel.filament.avatar-provider'))),
]),
TextInput::make('APP_FAVICON')
->label(trans('admin/setting.general.app_favicon'))
->hintIcon('tabler-question-mark')
->hintIconTooltip(trans('admin/setting.general.app_favicon_help'))
->required()
->default(env('APP_FAVICON', '/pelican.ico')),
Toggle::make('APP_DEBUG')
->label(trans('admin/setting.general.debug_mode'))
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('APP_DEBUG', (bool) $state))
->default(env('APP_DEBUG', config('app.debug'))),
ToggleButtons::make('FILAMENT_TOP_NAVIGATION')
->label(trans('admin/setting.general.navigation'))
->inline()
->options([
false => trans('admin/setting.general.sidebar'),
true => trans('admin/setting.general.topbar'),
])
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('FILAMENT_TOP_NAVIGATION', (bool) $state))
->default(env('FILAMENT_TOP_NAVIGATION', config('panel.filament.top-navigation'))),
ToggleButtons::make('PANEL_USE_BINARY_PREFIX')
->label(trans('admin/setting.general.unit_prefix'))
->inline()
@@ -321,7 +297,7 @@ class Settings extends Page implements HasForms
'mail.mailers.smtp.port' => config('mail.mailers.smtp.port'),
'mail.mailers.smtp.username' => config('mail.mailers.smtp.username'),
'mail.mailers.smtp.password' => config('mail.mailers.smtp.password'),
'mail.mailers.smtp.scheme' => config('mail.mailers.smtp.scheme'),
'mail.mailers.smtp.encryption' => config('mail.mailers.smtp.encryption'),
'mail.from.address' => config('mail.from.address'),
'mail.from.name' => config('mail.from.name'),
'services.mailgun.domain' => config('services.mailgun.domain'),
@@ -337,7 +313,7 @@ class Settings extends Page implements HasForms
'mail.mailers.smtp.port' => $get('MAIL_PORT'),
'mail.mailers.smtp.username' => $get('MAIL_USERNAME'),
'mail.mailers.smtp.password' => $get('MAIL_PASSWORD'),
'mail.mailers.smtp.scheme' => $get('MAIL_SCHEME'),
'mail.mailers.smtp.encryption' => $get('MAIL_SCHEME'),
'mail.from.address' => $get('MAIL_FROM_ADDRESS'),
'mail.from.name' => $get('MAIL_FROM_NAME'),
'services.mailgun.domain' => $get('MAILGUN_DOMAIN'),
@@ -401,16 +377,22 @@ class Settings extends Page implements HasForms
->revealable()
->default(env('MAIL_PASSWORD')),
ToggleButtons::make('MAIL_SCHEME')
->label(trans('admin/setting.mail.smtp.scheme'))
->label(trans('admin/setting.mail.smtp.encryption'))
->inline()
->options([
'smtp' => 'SMTP',
'smtps' => 'SMTPS',
'tls' => trans('admin/setting.mail.smtp.tls'),
'ssl' => trans('admin/setting.mail.smtp.ssl'),
'' => trans('admin/setting.mail.smtp.none'),
])
->default(env('MAIL_SCHEME', config('mail.mailers.smtp.scheme')))
->default(env('MAIL_SCHEME', config('mail.mailers.smtp.encryption', 'tls')))
->live()
->afterStateUpdated(function ($state, Set $set) {
$set('MAIL_PORT', $state === 'smtps' ? 587 : 2525);
$port = match ($state) {
'tls' => 587,
'ssl' => 465,
default => 25,
};
$set('MAIL_PORT', $port);
}),
]),
Section::make(trans('admin/setting.mail.mailgun.mailgun_title'))

View File

@@ -4,29 +4,14 @@ namespace App\Filament\Admin\Resources\DatabaseHostResource\Pages;
use App\Filament\Admin\Resources\DatabaseHostResource;
use App\Services\Databases\Hosts\HostCreationService;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\Wizard\Step;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\CreateRecord;
use Filament\Resources\Pages\CreateRecord\Concerns\HasWizard;
use Filament\Support\Exceptions\Halt;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\HtmlString;
use Illuminate\Support\Str;
use PDOException;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
class CreateDatabaseHost extends CreateRecord
{
use HasWizard;
protected static string $resource = DatabaseHostResource::class;
protected static bool $canCreateAnother = false;
@@ -38,118 +23,18 @@ class CreateDatabaseHost extends CreateRecord
$this->service = $service;
}
/** @return Step[] */
public function getSteps(): array
protected function getHeaderActions(): array
{
return [
Step::make(trans('admin/databasehost.setup.preparations'))
->columns()
->schema([
Placeholder::make('')
->content(trans('admin/databasehost.setup.note')),
Toggle::make('different_server')
->label(new HtmlString(trans('admin/databasehost.setup.different_server')))
->dehydrated(false)
->live()
->columnSpanFull()
->afterStateUpdated(fn ($state, Set $set) => $state ? $set('panel_ip', gethostbyname(str(config('app.url'))->replace(['http:', 'https:', '/'], ''))) : '127.0.0.1'),
Hidden::make('panel_ip')
->default('127.0.0.1')
->dehydrated(false),
TextInput::make('username')
->label(trans('admin/databasehost.username'))
->helperText(trans('admin/databasehost.username_help'))
->required()
->default('pelicanuser')
->maxLength(255),
TextInput::make('password')
->label(trans('admin/databasehost.password'))
->helperText(trans('admin/databasehost.password_help'))
->required()
->default(Str::password(16))
->password()
->revealable()
->maxLength(255),
])
->afterValidation(function (Get $get, Set $set) {
$set('create_user', "CREATE USER '{$get('username')}'@'{$get('panel_ip')}' IDENTIFIED BY '{$get('password')}';");
$set('assign_permissions', "GRANT ALL PRIVILEGES ON *.* TO '{$get('username')}'@'{$get('panel_ip')}' WITH GRANT OPTION;");
}),
Step::make(trans('admin/databasehost.setup.database_setup'))
->schema([
Fieldset::make(trans('admin/databasehost.setup.database_user'))
->schema([
Placeholder::make('')
->content(new HtmlString(trans('admin/databasehost.setup.cli_login')))
->columnSpanFull(),
TextInput::make('create_user')
->label(trans('admin/databasehost.setup.command_create_user'))
->default(fn (Get $get) => "CREATE USER '{$get('username')}'@'{$get('panel_ip')}' IDENTIFIED BY '{$get('password')}';")
->disabled()
->dehydrated(false)
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
->columnSpanFull(),
TextInput::make('assign_permissions')
->label(trans('admin/databasehost.setup.command_assign_permissions'))
->default(fn (Get $get) => "GRANT ALL PRIVILEGES ON *.* TO '{$get('username')}'@'{$get('panel_ip')}' WITH GRANT OPTION;")
->disabled()
->dehydrated(false)
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
->columnSpanFull(),
Placeholder::make('')
->content(new HtmlString(trans('admin/databasehost.setup.cli_exit')))
->columnSpanFull(),
]),
Fieldset::make(trans('admin/databasehost.setup.external_access'))
->schema([
Placeholder::make('')
->content(new HtmlString(trans('admin/databasehost.setup.allow_external_access')))
->columnSpanFull(),
]),
]),
Step::make(trans('admin/databasehost.setup.panel_setup'))
->columns([
'default' => 2,
'lg' => 3,
])
->schema([
TextInput::make('host')
->columnSpan(2)
->label(trans('admin/databasehost.host'))
->helperText(trans('admin/databasehost.host_help'))
->required()
->live(onBlur: true)
->afterStateUpdated(fn ($state, Set $set) => $set('name', $state))
->maxLength(255),
TextInput::make('port')
->label(trans('admin/databasehost.port'))
->helperText(trans('admin/databasehost.port_help'))
->required()
->numeric()
->default(3306)
->minValue(0)
->maxValue(65535),
TextInput::make('max_databases')
->label(trans('admin/databasehost.max_database'))
->helpertext(trans('admin/databasehost.max_databases_help'))
->placeholder(trans('admin/databasehost.unlimited'))
->numeric(),
TextInput::make('name')
->label(trans('admin/databasehost.display_name'))
->helperText(trans('admin/databasehost.display_name_help'))
->required()
->maxLength(60),
Select::make('node_ids')
->multiple()
->searchable()
->preload()
->helperText(trans('admin/databasehost.linked_nodes_help'))
->label(trans('admin/databasehost.linked_nodes'))
->relationship('nodes', 'name'),
]),
$this->getCreateFormAction()->formId('form'),
];
}
protected function getFormActions(): array
{
return [];
}
protected function handleRecordCreation(array $data): Model
{
try {

View File

@@ -243,7 +243,6 @@ class CreateEgg extends CreateRecord
->default('ghcr.io/pelican-eggs/installers:debian'),
Select::make('script_entry')
->label(trans('admin/egg.script_entry'))
->native(false)
->selectablePlaceholder(false)
->default('bash')
->options(['bash', 'ash', '/bin/bash'])

View File

@@ -235,7 +235,6 @@ class EditEgg extends EditRecord
->placeholder('ghcr.io/pelican-eggs/installers:debian'),
Select::make('script_entry')
->label(trans('admin/egg.script_entry'))
->native(false)
->selectablePlaceholder(false)
->options(['bash', 'ash', '/bin/bash'])
->required(),

View File

@@ -7,8 +7,6 @@ use App\Filament\Components\Actions\ImportEggAction as ImportEggHeaderAction;
use App\Filament\Components\Tables\Actions\ExportEggAction;
use App\Filament\Components\Tables\Actions\ImportEggAction;
use App\Filament\Components\Tables\Actions\UpdateEggAction;
use App\Filament\Components\Tables\Actions\UpdateEggBulkAction;
use App\Filament\Components\Tables\Filters\TagsFilter;
use App\Models\Egg;
use Filament\Actions\CreateAction as CreateHeaderAction;
use Filament\Resources\Pages\ListRecords;
@@ -18,7 +16,6 @@ use Filament\Tables\Actions\EditAction;
use Filament\Tables\Actions\ReplicateAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
class ListEggs extends ListRecords
@@ -30,6 +27,7 @@ class ListEggs extends ListRecords
return $table
->searchable(true)
->defaultPaginationPageOption(25)
->checkIfRecordIsSelectableUsing(fn (Egg $egg) => $egg->servers_count <= 0)
->columns([
TextColumn::make('id')
->label('Id')
@@ -70,16 +68,7 @@ class ListEggs extends ListRecords
->successRedirectUrl(fn (Egg $replica) => EditEgg::getUrl(['record' => $replica])),
])
->groupedBulkActions([
DeleteBulkAction::make()
->before(fn (DeleteBulkAction $action, Collection $records) => $action->records($records->filter(function ($egg) {
/** @var Egg $egg */
return $egg->servers_count <= 0;
}))),
UpdateEggBulkAction::make()
->before(fn (UpdateEggBulkAction $action, Collection $records) => $action->records($records->filter(function ($egg) {
/** @var Egg $egg */
return cache()->get("eggs.$egg->uuid.update", false);
}))),
DeleteBulkAction::make(),
])
->emptyStateIcon('tabler-eggs')
->emptyStateDescription('')
@@ -88,10 +77,6 @@ class ListEggs extends ListRecords
CreateAction::make(),
ImportEggAction::make()
->multiple(),
])
->filters([
TagsFilter::make()
->model(Egg::class),
]);
}

View File

@@ -3,7 +3,6 @@
namespace App\Filament\Admin\Resources\NodeResource\Pages;
use App\Filament\Admin\Resources\NodeResource;
use App\Models\Node;
use Filament\Forms;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Grid;
@@ -45,8 +44,7 @@ class CreateNode extends CreateRecord
->required()
->autofocus()
->live(debounce: 1500)
->rules(Node::getRulesForField('fqdn'))
->prohibited(fn ($state) => is_ip($state) && request()->isSecure())
->rule('prohibited', fn ($state) => is_ip($state) && request()->isSecure())
->label(fn ($state) => is_ip($state) ? trans('admin/node.ip_address') : trans('admin/node.domain'))
->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
->helperText(function ($state) {

View File

@@ -4,7 +4,6 @@ namespace App\Filament\Admin\Resources\NodeResource\Pages;
use App\Filament\Admin\Resources\NodeResource;
use App\Models\Node;
use App\Repositories\Daemon\DaemonConfigurationRepository;
use App\Services\Helpers\SoftwareVersionService;
use App\Services\Nodes\NodeAutoDeployService;
use App\Services\Nodes\NodeUpdateService;
@@ -27,7 +26,7 @@ use Filament\Forms\Set;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use Filament\Support\Enums\Alignment;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\HtmlString;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
@@ -35,13 +34,12 @@ class EditNode extends EditRecord
{
protected static string $resource = NodeResource::class;
private DaemonConfigurationRepository $daemonConfigurationRepository;
private bool $errored = false;
private NodeUpdateService $nodeUpdateService;
public function boot(DaemonConfigurationRepository $daemonConfigurationRepository, NodeUpdateService $nodeUpdateService): void
public function boot(NodeUpdateService $nodeUpdateService): void
{
$this->daemonConfigurationRepository = $daemonConfigurationRepository;
$this->nodeUpdateService = $nodeUpdateService;
}
@@ -110,8 +108,7 @@ class EditNode extends EditRecord
->required()
->autofocus()
->live(debounce: 1500)
->rules(Node::getRulesForField('fqdn'))
->prohibited(fn ($state) => is_ip($state) && request()->isSecure())
->rule('prohibited', fn ($state) => is_ip($state) && request()->isSecure())
->label(fn ($state) => is_ip($state) ? trans('admin/node.ip_address') : trans('admin/node.domain'))
->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
->helperText(function ($state) {
@@ -558,18 +555,7 @@ class EditNode extends EditRecord
->modalHeading(trans('admin/node.reset_token'))
->modalDescription(trans('admin/node.reset_help'))
->action(function (Node $node) {
try {
$this->nodeUpdateService->handle($node, [], true);
} catch (Exception) {
Notification::make()
->title(trans('admin/node.error_connecting', ['node' => $node->name]))
->body(trans('admin/node.error_connecting_description'))
->color('warning')
->icon('tabler-database')
->warning()
->send();
}
$this->nodeUpdateService->handle($node, [], true);
Notification::make()->success()->title(trans('admin/node.token_reset'))->send();
$this->fillForm();
}),
@@ -599,6 +585,39 @@ class EditNode extends EditRecord
return $data;
}
protected function handleRecordUpdate(Model $record, array $data): Model
{
if (!$record instanceof Node) {
return $record;
}
try {
$record = $this->nodeUpdateService->handle($record, $data);
} catch (Exception $exception) {
$this->errored = true;
Notification::make()
->title(trans('admin/node.error_connecting', ['node' => $record->name]))
->body(trans('admin/node.error_connecting_description'))
->color('warning')
->icon('tabler-database')
->warning()
->send();
}
return parent::handleRecordUpdate($record, $data);
}
protected function getSavedNotification(): ?Notification
{
if ($this->errored) {
return null;
}
return parent::getSavedNotification();
}
protected function getFormActions(): array
{
return [];
@@ -617,31 +636,6 @@ class EditNode extends EditRecord
protected function afterSave(): void
{
$this->fillForm();
/** @var Node $node */
$node = $this->record;
$changed = collect($node->getChanges())->except(['updated_at', 'name', 'tags', 'public', 'maintenance_mode', 'memory', 'memory_overallocate', 'disk', 'disk_overallocate', 'cpu', 'cpu_overallocate'])->all();
try {
if ($changed) {
$this->daemonConfigurationRepository->setNode($node)->update($node);
}
parent::getSavedNotification()?->send();
} catch (ConnectionException) {
Notification::make()
->title(trans('admin/node.error_connecting', ['node' => $node->name]))
->body(trans('admin/node.error_connecting_description'))
->color('warning')
->icon('tabler-database')
->warning()
->send();
}
}
protected function getSavedNotification(): ?Notification
{
return null;
}
protected function getColumnSpan(): ?int

View File

@@ -4,7 +4,6 @@ namespace App\Filament\Admin\Resources\NodeResource\Pages;
use App\Filament\Admin\Resources\NodeResource;
use App\Filament\Components\Tables\Columns\NodeHealthColumn;
use App\Filament\Components\Tables\Filters\TagsFilter;
use App\Models\Node;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
@@ -66,10 +65,6 @@ class ListNodes extends ListRecords
->emptyStateHeading(trans('admin/node.no_nodes'))
->emptyStateActions([
CreateAction::make(),
])
->filters([
TagsFilter::make()
->model(Node::class),
]);
}

View File

@@ -2,6 +2,8 @@
namespace App\Filament\Admin\Resources;
use App\Enums\RolePermissionModels;
use App\Enums\RolePermissionPrefixes;
use App\Filament\Admin\Resources\RoleResource\Pages;
use App\Models\Role;
use Filament\Forms\Components\Actions\Action;
@@ -93,16 +95,32 @@ class RoleResource extends Resource
public static function form(Form $form): Form
{
$permissionSections = [];
$permissions = [];
foreach (Role::getPermissionList() as $model => $permissions) {
foreach (RolePermissionModels::cases() as $model) {
$options = [];
foreach ($permissions as $permission) {
$options[$permission . ' ' . strtolower($model)] = Str::headline($permission);
foreach (RolePermissionPrefixes::cases() as $prefix) {
$options[$prefix->value . ' ' . strtolower($model->value)] = Str::headline($prefix->value);
}
$permissionSections[] = self::makeSection($model, $options);
if (array_key_exists($model->value, Role::MODEL_SPECIFIC_PERMISSIONS)) {
foreach (Role::MODEL_SPECIFIC_PERMISSIONS[$model->value] as $permission) {
$options[$permission . ' ' . strtolower($model->value)] = Str::headline($permission);
}
}
$permissions[] = self::makeSection($model->value, $options);
}
foreach (Role::SPECIAL_PERMISSIONS as $model => $prefixes) {
$options = [];
foreach ($prefixes as $prefix) {
$options[$prefix . ' ' . strtolower($model)] = Str::headline($prefix);
}
$permissions[] = self::makeSection($model, $options);
}
return $form
@@ -119,7 +137,7 @@ class RoleResource extends Resource
->hidden(),
Fieldset::make(trans('admin/role.permissions'))
->columns(3)
->schema($permissionSections)
->schema($permissions)
->hidden(fn (Get $get) => $get('name') === Role::ROOT_ADMIN),
Placeholder::make('permissions')
->label(trans('admin/role.permissions'))

View File

@@ -2,19 +2,16 @@
namespace App\Filament\Admin\Resources\ServerResource\Pages;
use App\Enums\ServerState;
use App\Enums\SuspendAction;
use App\Filament\Admin\Resources\ServerResource;
use App\Filament\Admin\Resources\ServerResource\RelationManagers\AllocationsRelationManager;
use App\Filament\Components\Forms\Actions\PreviewStartupAction;
use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction;
use App\Filament\Server\Pages\Console;
use App\Models\Allocation;
use App\Models\Database;
use App\Models\DatabaseHost;
use App\Models\Egg;
use App\Models\Mount;
use App\Models\Node;
use App\Models\Server;
use App\Models\ServerVariable;
use App\Models\User;
@@ -31,10 +28,8 @@ use Closure;
use Exception;
use Filament\Actions;
use Filament\Forms;
use Filament\Forms\Components\Actions as FormActions;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Hidden;
@@ -54,10 +49,9 @@ use Filament\Forms\Set;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\HtmlString;
use LogicException;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
@@ -65,6 +59,8 @@ class EditServer extends EditRecord
{
protected static string $resource = ServerResource::class;
private bool $errored = false;
private DaemonServerRepository $daemonServerRepository;
public function boot(DaemonServerRepository $daemonServerRepository): void
@@ -595,9 +591,7 @@ class EditServer extends EditRecord
]);
}
return $query
->join('egg_variables', 'server_variables.variable_id', '=', 'egg_variables.id')
->orderBy('egg_variables.sort');
return $query;
})
->grid()
->mutateRelationshipDataBeforeSaveUsing(function (array &$data): array {
@@ -737,7 +731,7 @@ class EditServer extends EditRecord
->deletable(false)
->addable(false)
->columnSpan(4),
FormActions::make([
Forms\Components\Actions::make([
Action::make('createDatabase')
->authorize(fn () => auth()->user()->can('create database'))
->disabled(fn () => DatabaseHost::query()->count() < 1)
@@ -805,50 +799,14 @@ class EditServer extends EditRecord
Grid::make()
->columnSpan(3)
->schema([
FormActions::make([
Forms\Components\Actions::make([
Action::make('toggleInstall')
->label(trans('admin/server.toggle_install'))
->disabled(fn (Server $server) => $server->isSuspended())
->modal(fn (Server $server) => $server->status === ServerState::InstallFailed)
->modalHeading(trans('admin/server.toggle_install_failed_header'))
->modalDescription(trans('admin/server.toggle_install_failed_desc'))
->modalSubmitActionLabel(trans('admin/server.reinstall'))
->action(function (ToggleInstallService $toggleService, ReinstallServerService $reinstallService, Server $server) {
if ($server->status === ServerState::InstallFailed) {
try {
$reinstallService->handle($server);
->action(function (ToggleInstallService $service, Server $server) {
$service->handle($server);
Notification::make()
->title(trans('admin/server.notifications.reinstall_started'))
->success()
->send();
$this->refreshFormData(['status', 'docker']);
} catch (Exception) {
Notification::make()
->title(trans('admin/server.notifications.reinstall_failed'))
->body(trans('admin/server.error_connecting', ['node' => $server->node->name]))
->danger()
->send();
}
} else {
try {
$toggleService->handle($server);
Notification::make()
->title(trans('admin/server.notifications.install_toggled'))
->success()
->send();
$this->refreshFormData(['status', 'docker']);
} catch (Exception $exception) {
Notification::make()
->title(trans('admin/server.notifications.install_toggle_failed'))
->body($exception->getMessage())
->danger()
->send();
}
}
$this->refreshFormData(['status', 'docker']);
}),
])->fullWidth(),
ToggleButtons::make('')
@@ -857,7 +815,7 @@ class EditServer extends EditRecord
Grid::make()
->columnSpan(3)
->schema([
FormActions::make([
Forms\Components\Actions::make([
Action::make('toggleSuspend')
->label(trans('admin/server.suspend'))
->color('warning')
@@ -865,20 +823,12 @@ class EditServer extends EditRecord
->action(function (SuspensionService $suspensionService, Server $server) {
try {
$suspensionService->handle($server, SuspendAction::Suspend);
Notification::make()
->success()
->title(trans('admin/server.notifications.server_suspended'))
->send();
$this->refreshFormData(['status', 'docker']);
} catch (Exception) {
Notification::make()
->warning()
->title(trans('admin/server.notifications.server_suspension'))
->body(trans('admin/server.error_connecting', ['node' => $server->node->name]))
->send();
} catch (\Exception $exception) {
Notification::make()->warning()->title(trans('admin/server.notifications.server_suspension'))->body($exception->getMessage())->send();
}
Notification::make()->success()->title(trans('admin/server.notifications.server_suspended'))->send();
$this->refreshFormData(['status', 'docker']);
}),
Action::make('toggleUnsuspend')
->label(trans('admin/server.unsuspend'))
@@ -887,20 +837,12 @@ class EditServer extends EditRecord
->action(function (SuspensionService $suspensionService, Server $server) {
try {
$suspensionService->handle($server, SuspendAction::Unsuspend);
Notification::make()
->success()
->title(trans('admin/server.notifications.server_unsuspended'))
->send();
$this->refreshFormData(['status', 'docker']);
} catch (Exception) {
Notification::make()
->warning()
->title(trans('admin/server.notifications.server_suspension'))
->body(trans('admin/server.error_connecting', ['node' => $server->node->name]))
->send();
} catch (\Exception $exception) {
Notification::make()->warning()->title(trans('admin/server.notifications.server_suspension'))->body($exception->getMessage())->send();
}
Notification::make()->success()->title(trans('admin/server.notifications.server_unsuspended'))->send();
$this->refreshFormData(['status', 'docker']);
}),
])->fullWidth(),
ToggleButtons::make('')
@@ -913,36 +855,42 @@ class EditServer extends EditRecord
Grid::make()
->columnSpan(3)
->schema([
FormActions::make([
Forms\Components\Actions::make([
Action::make('transfer')
->label(trans('admin/server.transfer'))
->disabled(fn (Server $server) => Node::count() <= 1 || $server->isInConflictState())
->modalheading(trans('admin/server.transfer'))
->form($this->transferServer())
->action(function (TransferServerService $transfer, Server $server, $data) {
try {
$transfer->handle($server, Arr::get($data, 'node_id'), Arr::get($data, 'allocation_id'), Arr::get($data, 'allocation_additional', []));
Notification::make()
->title('Transfer started')
->success()
->send();
} catch (Exception $exception) {
Notification::make()
->title('Transfer failed')
->body($exception->getMessage())
->danger()
->send();
}
}),
// ->action(fn (TransferServerService $transfer, Server $server) => $transfer->handle($server, []))
->disabled() //TODO!
->form([ //TODO!
Select::make('newNode')
->label('New Node')
->required()
->options([
true => 'on',
false => 'off',
]),
Select::make('newMainAllocation')
->label('New Main Allocation')
->required()
->options([
true => 'on',
false => 'off',
]),
Select::make('newAdditionalAllocation')
->label('New Additional Allocations')
->options([
true => 'on',
false => 'off',
]),
])
->modalheading(trans('admin/server.transfer')),
])->fullWidth(),
ToggleButtons::make('')
->hint(new HtmlString(trans('admin/server.transfer_help'))),
->hint(trans('admin/server.transfer_help')),
]),
Grid::make()
->columnSpan(3)
->schema([
FormActions::make([
Forms\Components\Actions::make([
Action::make('reinstall')
->label(trans('admin/server.reinstall'))
->color('danger')
@@ -950,24 +898,7 @@ class EditServer extends EditRecord
->modalHeading(trans('admin/server.reinstall_modal_heading'))
->modalDescription(trans('admin/server.reinstall_modal_description'))
->disabled(fn (Server $server) => $server->isSuspended())
->action(function (ReinstallServerService $service, Server $server) {
try {
$service->handle($server);
Notification::make()
->title(trans('admin/server.notifications.reinstall_started'))
->success()
->send();
$this->refreshFormData(['status', 'docker']);
} catch (Exception) {
Notification::make()
->title(trans('admin/server.notifications.reinstall_failed'))
->body(trans('admin/server.error_connecting', ['node' => $server->node->name]))
->danger()
->send();
}
}),
->action(fn (ReinstallServerService $service, Server $server) => $service->handle($server)),
])->fullWidth(),
ToggleButtons::make('')
->hint(trans('admin/server.reinstall_help')),
@@ -978,86 +909,32 @@ class EditServer extends EditRecord
]);
}
/** @return Component[] */
protected function transferServer(): array
protected function transferServer(Form $form): Form
{
return [
Select::make('node_id')
->label(trans('admin/server.node'))
->prefixIcon('tabler-server-2')
->selectablePlaceholder(false)
->default(fn (Server $server) => Node::whereNot('id', $server->node->id)->first()?->id)
->required()
->live()
->options(fn (Server $server) => Node::whereNot('id', $server->node->id)->pluck('name', 'id')->all()),
Select::make('allocation_id')
->label(trans('admin/server.primary_allocation'))
->required()
->prefixIcon('tabler-network')
->disabled(fn (Get $get) => !$get('node_id'))
->options(fn (Get $get) => Allocation::where('node_id', $get('node_id'))->whereNull('server_id')->get()->mapWithKeys(fn (Allocation $allocation) => [$allocation->id => $allocation->address]))
->searchable(['ip', 'port', 'ip_alias'])
->placeholder(trans('admin/server.select_allocation')),
Select::make('allocation_additional')
->label(trans('admin/server.additional_allocations'))
->multiple()
->prefixIcon('tabler-network')
->disabled(fn (Get $get) => !$get('node_id'))
->options(fn (Get $get) => Allocation::where('node_id', $get('node_id'))->whereNull('server_id')->when($get('allocation_id'), fn ($query) => $query->whereNot('id', $get('allocation_id')))->get()->mapWithKeys(fn (Allocation $allocation) => [$allocation->id => $allocation->address]))
->searchable(['ip', 'port', 'ip_alias'])
->placeholder(trans('admin/server.select_additional')),
];
return $form
->columns()
->schema([
Select::make('toNode')
->label('New Node'),
TextInput::make('newAllocation')
->label('Allocation'),
]);
}
protected function getHeaderActions(): array
{
/** @var Server $server */
$server = $this->getRecord();
$canForceDelete = cache()->get("servers.$server->uuid.canForceDelete", false);
return [
Actions\Action::make('Delete')
->successRedirectUrl(route('filament.admin.resources.servers.index'))
->color('danger')
->label(trans('filament-actions::delete.single.label'))
->modalHeading(trans('filament-actions::delete.single.modal.heading', ['label' => $this->getRecordTitle()]))
->modalSubmitActionLabel(trans('filament-actions::delete.single.label'))
->label(trans('filament-actions::delete.single.modal.actions.delete.label'))
->requiresConfirmation()
->action(function (Server $server, ServerDeletionService $service) {
try {
$service->handle($server);
$service->handle($server);
return redirect(ListServers::getUrl(panel: 'admin'));
} catch (ConnectionException) {
cache()->put("servers.$server->uuid.canForceDelete", true, now()->addMinutes(5));
Notification::make()
->title(trans('admin/server.notifications.error_server_delete'))
->body(trans('admin/server.notifications.error_server_delete_body'))
->color('warning')
->icon('tabler-database')
->warning()
->send();
}
return redirect(ListServers::getUrl(panel: 'admin'));
})
->hidden(fn () => $canForceDelete)
->authorize(fn (Server $server) => auth()->user()->can('delete server', $server)),
Actions\Action::make('ForceDelete')
->color('danger')
->label(trans('filament-actions::force-delete.single.label'))
->modalHeading(trans('filament-actions::force-delete.single.modal.heading', ['label' => $this->getRecordTitle()]))
->modalSubmitActionLabel(trans('filament-actions::force-delete.single.label'))
->requiresConfirmation()
->action(function (Server $server, ServerDeletionService $service) {
try {
$service->withForce()->handle($server);
return redirect(ListServers::getUrl(panel: 'admin'));
} catch (ConnectionException) {
cache()->forget("servers.$server->uuid.canForceDelete");
}
})
->visible(fn () => $canForceDelete)
->authorize(fn (Server $server) => auth()->user()->can('delete server', $server)),
Actions\Action::make('console')
->label(trans('admin/server.console'))
@@ -1084,32 +961,39 @@ class EditServer extends EditRecord
return $data;
}
protected function afterSave(): void
protected function handleRecordUpdate(Model $record, array $data): Model
{
/** @var Server $server */
$server = $this->record;
if (!$record instanceof Server) {
return $record;
}
$changed = collect($server->getChanges())->except(['updated_at', 'name', 'owner_id', 'condition', 'description', 'external_id', 'tags', 'cpu_pinning', 'allocation_limit', 'database_limit', 'backup_limit', 'skip_scripts'])->all();
/** @var Server $record */
$record = parent::handleRecordUpdate($record, $data);
try {
if ($changed) {
$this->daemonServerRepository->setServer($server)->sync();
}
parent::getSavedNotification()?->send();
$this->daemonServerRepository->setServer($record)->sync();
} catch (ConnectionException) {
$this->errored = true;
Notification::make()
->title(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
->title(trans('admin/server.notifications.error_connecting', ['node' => $record->node->name]))
->body(trans('admin/server.notifications.error_connecting_description'))
->color('warning')
->icon('tabler-database')
->warning()
->send();
}
return $record;
}
protected function getSavedNotification(): ?Notification
{
return null;
if ($this->errored) {
return null;
}
return parent::getSavedNotification();
}
public function getRelationManagers(): array

View File

@@ -12,16 +12,14 @@ use Filament\Forms\Components\TextInput;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Support\Exceptions\Halt;
use Filament\Tables;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\AssociateAction;
use Filament\Tables\Actions\CreateAction;
use Filament\Tables\Actions\DissociateBulkAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\TextInputColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Collection;
/**
* @method Server getOwnerRecord()
@@ -98,21 +96,10 @@ class AllocationsRelationManager extends RelationManager
->recordSelectSearchColumns(['ip', 'port'])
->label(trans('admin/server.add_allocation')),
])
->groupedBulkActions([
DissociateBulkAction::make()
->before(function (DissociateBulkAction $action, Collection $records) {
$records = $records->filter(function ($allocation) {
/** @var Allocation $allocation */
return $allocation->id !== $this->getOwnerRecord()->allocation_id;
});
if ($records->isEmpty()) {
$action->failureNotificationTitle(trans('admin/server.notifications.dissociate_primary'))->failure();
throw new Halt();
}
return $records;
}),
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DissociateBulkAction::make(),
]),
]);
}
}

View File

@@ -6,7 +6,6 @@ use App\Filament\Admin\Resources\UserResource\Pages;
use App\Filament\Admin\Resources\UserResource\RelationManagers;
use App\Models\Role;
use App\Models\User;
use Filament\Facades\Filament;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
@@ -18,7 +17,6 @@ use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
class UserResource extends Resource
{
@@ -60,9 +58,8 @@ class UserResource extends Resource
ImageColumn::make('picture')
->visibleFrom('lg')
->label('')
->circular()
->alignCenter()
->defaultImageUrl(fn (User $user) => Filament::getUserAvatarUrl($user)),
->extraImgAttributes(['class' => 'rounded-full'])
->defaultImageUrl(fn (User $user) => 'https://gravatar.com/avatar/' . md5(strtolower($user->email))),
TextColumn::make('username')
->label(trans('admin/user.username')),
TextColumn::make('email')
@@ -123,26 +120,12 @@ class UserResource extends Resource
->hintIconTooltip(fn ($operation) => $operation === 'create' ? trans('admin/user.password_help') : null)
->password(),
CheckboxList::make('roles')
->hidden(fn (User $user) => $user->isRootAdmin())
->relationship('roles', 'name', fn (Builder $query) => $query->whereNot('id', Role::getRootAdmin()->id))
->saveRelationshipsUsing(fn (User $user, array $state) => $user->syncRoles(collect($state)->map(fn ($role) => Role::findById($role))))
->disableOptionWhen(fn (string $value): bool => $value == Role::getRootAdmin()->id)
->relationship('roles', 'name')
->dehydrated()
->label(trans('admin/user.admin_roles'))
->columnSpanFull()
->bulkToggleable(false),
CheckboxList::make('root_admin_role')
->visible(fn (User $user) => $user->isRootAdmin())
->disabled()
->options([
'root_admin' => Role::ROOT_ADMIN,
])
->descriptions([
'root_admin' => trans('admin/role.root_admin', ['role' => Role::ROOT_ADMIN]),
])
->formatStateUsing(fn () => ['root_admin'])
->dehydrated(false)
->label(trans('admin/user.admin_roles'))
->columnSpanFull(),
]);
}

View File

@@ -33,13 +33,6 @@ class CreateUser extends CreateRecord
return [];
}
protected function prepareForValidation($attributes): array
{
$attributes['data']['email'] = mb_strtolower($attributes['data']['email']);
return $attributes;
}
protected function handleRecordCreation(array $data): Model
{
$data['root_admin'] = false;

View File

@@ -1,32 +0,0 @@
<?php
namespace App\Filament\Admin\Widgets;
use Filament\Actions\CreateAction;
use Filament\Widgets\Widget;
class CanaryWidget extends Widget
{
protected static string $view = 'filament.admin.widgets.canary-widget';
protected static bool $isLazy = false;
protected static ?int $sort = 1;
public static function canView(): bool
{
return config('app.version') === 'canary';
}
public function getViewData(): array
{
return [
'actions' => [
CreateAction::make()
->label(trans('admin/dashboard.sections.intro-developers.button_issues'))
->icon('tabler-brand-github')
->url('https://github.com/pelican-dev/panel/issues', true),
],
];
}
}

View File

@@ -1,27 +0,0 @@
<?php
namespace App\Filament\Admin\Widgets;
use Filament\Actions\CreateAction;
use Filament\Widgets\Widget;
class HelpWidget extends Widget
{
protected static string $view = 'filament.admin.widgets.help-widget';
protected static bool $isLazy = false;
protected static ?int $sort = 4;
public function getViewData(): array
{
return [
'actions' => [
CreateAction::make()
->label(trans('admin/dashboard.sections.intro-help.button_docs'))
->icon('tabler-speedboat')
->url('https://pelican.dev/docs', true),
],
];
}
}

View File

@@ -1,34 +0,0 @@
<?php
namespace App\Filament\Admin\Widgets;
use App\Filament\Admin\Resources\NodeResource\Pages\CreateNode;
use App\Models\Node;
use Filament\Actions\CreateAction;
use Filament\Widgets\Widget;
class NoNodesWidget extends Widget
{
protected static string $view = 'filament.admin.widgets.no-nodes-widget';
protected static bool $isLazy = false;
protected static ?int $sort = 2;
public static function canView(): bool
{
return Node::count() <= 0;
}
public function getViewData(): array
{
return [
'actions' => [
CreateAction::make()
->label(trans('admin/dashboard.sections.intro-first-node.button_label'))
->icon('tabler-server-2')
->url(CreateNode::getUrl()),
],
];
}
}

View File

@@ -1,28 +0,0 @@
<?php
namespace App\Filament\Admin\Widgets;
use Filament\Actions\CreateAction;
use Filament\Widgets\Widget;
class SupportWidget extends Widget
{
protected static string $view = 'filament.admin.widgets.support-widget';
protected static bool $isLazy = false;
protected static ?int $sort = 3;
public function getViewData(): array
{
return [
'actions' => [
CreateAction::make()
->label(trans('admin/dashboard.sections.intro-support.button_donate'))
->icon('tabler-cash')
->url('https://pelican.dev/donate', true)
->color('success'),
],
];
}
}

View File

@@ -1,39 +0,0 @@
<?php
namespace App\Filament\Admin\Widgets;
use App\Services\Helpers\SoftwareVersionService;
use Filament\Actions\CreateAction;
use Filament\Widgets\Widget;
class UpdateWidget extends Widget
{
protected static string $view = 'filament.admin.widgets.update-widget';
protected static bool $isLazy = false;
protected static ?int $sort = 0;
private SoftwareVersionService $softwareVersionService;
public function mount(SoftwareVersionService $softwareVersionService): void
{
$this->softwareVersionService = $softwareVersionService;
}
public function getViewData(): array
{
return [
'version' => $this->softwareVersionService->currentPanelVersion(),
'latestVersion' => $this->softwareVersionService->latestPanelVersion(),
'isLatest' => $this->softwareVersionService->isLatestPanel(),
'actions' => [
CreateAction::make()
->label(trans('admin/dashboard.sections.intro-update-available.heading'))
->icon('tabler-clipboard-text')
->url('https://pelican.dev/docs/panel/update', true)
->color('warning'),
],
];
}
}

View File

@@ -2,16 +2,13 @@
namespace App\Filament\App\Resources\ServerResource\Pages;
use App\Enums\ServerResourceType;
use App\Filament\App\Resources\ServerResource;
use App\Filament\Components\Tables\Columns\ServerEntryColumn;
use App\Filament\Server\Pages\Console;
use App\Models\Server;
use Filament\Resources\Components\Tab;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Columns\ColumnGroup;
use Filament\Tables\Columns\Layout\Stack;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
@@ -20,111 +17,37 @@ class ListServers extends ListRecords
{
protected static string $resource = ServerResource::class;
public const DANGER_THRESHOLD = 0.9;
public const WARNING_THRESHOLD = 0.7;
public function table(Table $table): Table
{
$baseQuery = auth()->user()->accessibleServers();
$viewOne = [
TextColumn::make('condition')
->label('')
->default('unknown')
->wrap()
->badge()
->alignCenter()
->tooltip(fn (Server $server) => $server->formatResource('uptime', type: ServerResourceType::Time))
->icon(fn (Server $server) => $server->condition->getIcon())
->color(fn (Server $server) => $server->condition->getColor()),
];
$viewTwo = [
TextColumn::make('name')
->label('')
->size('md')
->searchable(),
TextColumn::make('')
->label('')
->badge()
->copyable(request()->isSecure())
->copyMessage(fn (Server $server, string $state) => 'Copied ' . $server->allocation->address)
->state(fn (Server $server) => $server->allocation->address),
];
$viewThree = [
TextColumn::make('cpuUsage')
->label('')
->icon('tabler-cpu')
->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('cpu', limit: true, type: ServerResourceType::Percentage, precision: 0))
->state(fn (Server $server) => $server->formatResource('cpu_absolute', type: ServerResourceType::Percentage))
->color(fn (Server $server) => $this->getResourceColor($server, 'cpu')),
TextColumn::make('memoryUsage')
->label('')
->icon('tabler-memory')
->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('memory', limit: true))
->state(fn (Server $server) => $server->formatResource('memory_bytes'))
->color(fn (Server $server) => $this->getResourceColor($server, 'memory')),
TextColumn::make('diskUsage')
->label('')
->icon('tabler-device-floppy')
->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('disk', limit: true))
->state(fn (Server $server) => $server->formatResource('disk_bytes'))
->color(fn (Server $server) => $this->getResourceColor($server, 'disk')),
];
return $table
->paginated(false)
->query(fn () => $baseQuery)
->poll('15s')
->columns(
(auth()->user()->getCustomization()['dashboard_layout'] ?? 'grid') === 'grid'
? [
Stack::make([
ServerEntryColumn::make('server_entry')
->searchable(['name']),
]),
]
: [
ColumnGroup::make('Status')
->label('Status')
->columns($viewOne),
ColumnGroup::make('Server')
->label('Servers')
->columns($viewTwo),
ColumnGroup::make('Resources')
->label('Resources')
->columns($viewThree),
]
)
->recordUrl(fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server))
->columns([
Stack::make([
ServerEntryColumn::make('server_entry')
->searchable(['name']),
]),
])
->contentGrid([
'default' => 1,
'md' => 2,
])
->recordUrl(fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server))
->emptyStateIcon('tabler-brand-docker')
->emptyStateDescription('')
->emptyStateHeading(fn () => $this->activeTab === 'my' ? 'You don\'t own any servers!' : 'You don\'t have access to any servers!')
->emptyStateHeading('You don\'t have access to any servers!')
->persistFiltersInSession()
->filters([
SelectFilter::make('egg')
->relationship('egg', 'name', fn (Builder $query) => $query->whereIn('id', $baseQuery->pluck('egg_id')))
->searchable()
->preload(),
SelectFilter::make('owner')
->relationship('user', 'username', fn (Builder $query) => $query->whereIn('id', $baseQuery->pluck('owner_id')))
->searchable()
->hidden(fn () => $this->activeTab === 'my')
->preload(),
]);
}
public function updatedActiveTab(): void
{
$this->resetTable();
}
public function getTabs(): array
{
$all = auth()->user()->accessibleServers();
@@ -144,50 +67,4 @@ class ListServers extends ListRecords
->badge($all->count()),
];
}
public function getResourceColor(Server $server, string $resource): ?string
{
$current = null;
$limit = null;
switch ($resource) {
case 'cpu':
$current = $server->resources()['cpu_absolute'] ?? 0;
$limit = $server->cpu;
if ($server->cpu === 0) {
return null;
}
break;
case 'memory':
$current = $server->resources()['memory_bytes'] ?? 0;
$limit = $server->memory * 2 ** 20;
if ($server->memory === 0) {
return null;
}
break;
case 'disk':
$current = $server->resources()['disk_bytes'] ?? 0;
$limit = $server->disk * 2 ** 20;
if ($server->disk === 0) {
return null;
}
break;
default:
return null;
}
if ($current >= $limit * self::DANGER_THRESHOLD) {
return 'danger';
}
if ($current >= $limit * self::WARNING_THRESHOLD) {
return 'warning';
}
return null;
}
}

View File

@@ -13,7 +13,6 @@ use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Illuminate\Support\Arr;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
class ImportEggAction extends Action
@@ -32,7 +31,7 @@ class ImportEggAction extends Action
$this->authorize(fn () => auth()->user()->can('import egg'));
$this->action(function (array $data, EggImporterService $eggImportService): void {
$eggs = array_merge(collect($data['urls'])->flatten()->whereNotNull()->unique()->all(), Arr::wrap($data['files']));
$eggs = array_merge($data['files'], collect($data['urls'])->flatten()->whereNotNull()->unique()->all());
if (empty($eggs)) {
return;
}

View File

@@ -17,12 +17,6 @@ class CopyFrom extends Select
$this->placeholder(trans('admin/egg.none'));
$this->preload();
$this->searchable();
$this->native(false);
$this->live();
}

View File

@@ -13,7 +13,6 @@ use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Filament\Tables\Actions\Action;
use Illuminate\Support\Arr;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
class ImportEggAction extends Action
@@ -32,7 +31,7 @@ class ImportEggAction extends Action
$this->authorize(fn () => auth()->user()->can('import egg'));
$this->action(function (array $data, EggImporterService $eggImportService): void {
$eggs = array_merge(collect($data['urls'])->flatten()->whereNotNull()->unique()->all(), Arr::wrap($data['files']));
$eggs = array_merge($data['files'], collect($data['urls'])->flatten()->whereNotNull()->unique()->all());
if (empty($eggs)) {
return;
}

View File

@@ -20,7 +20,7 @@ class UpdateEggAction extends Action
{
parent::setUp();
$this->label(trans_choice('admin/egg.update', 1));
$this->label(trans('admin/egg.update'));
$this->icon('tabler-cloud-download');
@@ -28,9 +28,9 @@ class UpdateEggAction extends Action
$this->requiresConfirmation();
$this->modalHeading(trans_choice('admin/egg.update_question', 1));
$this->modalHeading(trans('admin/egg.update_question'));
$this->modalDescription(trans_choice('admin/egg.update_description', 1));
$this->modalDescription(trans('admin/egg.update_description'));
$this->modalIconColor('danger');
@@ -54,7 +54,7 @@ class UpdateEggAction extends Action
}
Notification::make()
->title(trans_choice('admin/egg.updated', 1))
->title(trans('admin/egg.updated'))
->body($egg->name)
->success()
->send();

View File

@@ -1,80 +0,0 @@
<?php
namespace App\Filament\Components\Tables\Actions;
use App\Models\Egg;
use App\Services\Eggs\Sharing\EggImporterService;
use Exception;
use Filament\Actions\StaticAction;
use Filament\Notifications\Notification;
use Filament\Tables\Actions\BulkAction;
use Illuminate\Database\Eloquent\Collection;
class UpdateEggBulkAction extends BulkAction
{
public static function getDefaultName(): ?string
{
return 'update';
}
protected function setUp(): void
{
parent::setUp();
$this->label(trans_choice('admin/egg.update', 2));
$this->icon('tabler-cloud-download');
$this->color('success');
$this->requiresConfirmation();
$this->modalHeading(trans_choice('admin/egg.update_question', 2));
$this->modalDescription(trans_choice('admin/egg.update_description', 2));
$this->modalIconColor('danger');
$this->modalSubmitAction(fn (StaticAction $action) => $action->color('danger'));
$this->action(function (Collection $records, EggImporterService $eggImporterService) {
if ($records->count() === 0) {
Notification::make()
->title(trans('admin/egg.no_updates'))
->warning()
->send();
return;
}
$success = 0;
$failed = 0;
/** @var Egg $egg */
foreach ($records as $egg) {
try {
$eggImporterService->fromUrl($egg->update_url, $egg);
$success++;
cache()->forget("eggs.$egg->uuid.update");
} catch (Exception $exception) {
$failed++;
report($exception);
}
}
Notification::make()
->title(trans_choice('admin/egg.updated', 2, ['count' => $success, 'total' => $records->count()]))
->body($failed > 0 ? trans('admin/egg.updated_failed', ['count' => $failed]) : null)
->status($failed > 0 ? 'warning' : 'success')
->persistent()
->send();
});
$this->authorize(fn () => auth()->user()->can('import egg'));
$this->deselectRecordsAfterCompletion();
}
}

View File

@@ -1,57 +0,0 @@
<?php
namespace App\Filament\Components\Tables\Filters;
use Filament\Forms\Components\Field;
use Filament\Forms\Components\Select;
use Filament\Tables\Filters\BaseFilter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
class TagsFilter extends BaseFilter
{
protected string $model;
public static function getDefaultName(): ?string
{
return 'tags';
}
protected function setUp(): void
{
parent::setUp();
$this->query(fn (Builder $query, array $data) => $query->when($data['tag'], fn (Builder $query, $tag) => $query->whereJsonContains('tags', $tag)));
$this->indicateUsing(fn (array $data) => $data['tag'] ? 'Tag: ' . $data['tag'] : null);
$this->resetState(['tag' => null]);
$this->visible(fn () => $this->getTags()->count() > 0);
}
private function getTags(): Collection
{
return $this->getModel()::query()->pluck('tags')->flatten()->unique();
}
public function getFormField(): Field
{
return Select::make('tag')
->preload()
->searchable()
->options(fn () => $this->getTags()->mapWithKeys(fn ($tag) => [$tag => $tag]));
}
public function model(string $model): static
{
$this->model = $model;
return $this;
}
public function getModel(): string
{
return $this->model;
}
}

View File

@@ -19,7 +19,6 @@ use chillerlan\QRCode\QROptions;
use DateTimeZone;
use Filament\Forms\Components\Actions;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Repeater;
@@ -30,7 +29,6 @@ use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Get;
use Filament\Notifications\Notification;
use Filament\Pages\Auth\EditProfile as BaseEditProfile;
@@ -127,11 +125,6 @@ class EditProfile extends BaseEditProfile
->helperText(fn ($state, LanguageService $languageService) => new HtmlString($languageService->isLanguageTranslated($state) ? '' : trans('profile.language_help', ['state' => $state])))
->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages())
->native(false),
FileUpload::make('avatar')
->visible(fn () => config('panel.filament.avatar-provider') === 'local')
->avatar()
->directory('avatars')
->getUploadedFileNameForStorageUsing(fn () => $this->getUser()->id . '.png'),
]),
Tab::make(trans('profile.tabs.oauth'))
@@ -249,7 +242,6 @@ class EditProfile extends BaseEditProfile
->password(),
];
}),
Tab::make(trans('profile.tabs.api_keys'))
->icon('tabler-key')
->schema([
@@ -269,7 +261,7 @@ class EditProfile extends BaseEditProfile
Action::make('Create')
->label(trans('filament-actions::create.single.modal.actions.create.label'))
->disabled(fn (Get $get) => $get('description') === null)
->successRedirectUrl(self::getUrl(['tab' => '-api-keys-tab'], panel: 'app'))
->successRedirectUrl(self::getUrl(['tab' => '-api-keys-tab']))
->action(function (Get $get, Action $action, User $user) {
$token = $user->createToken(
$get('description'),
@@ -316,11 +308,9 @@ class EditProfile extends BaseEditProfile
]),
]),
]),
Tab::make(trans('profile.tabs.ssh_keys'))
->icon('tabler-lock-code')
->hidden(),
Tab::make(trans('profile.tabs.activity'))
->icon('tabler-history')
->schema([
@@ -335,47 +325,6 @@ class EditProfile extends BaseEditProfile
Placeholder::make('activity!')->label('')->content(fn (ActivityLog $log) => new HtmlString($log->htmlable())),
]),
]),
Tab::make(trans('profile.tabs.customization'))
->icon('tabler-adjustments')
->schema([
Section::make(trans('profile.dashboard'))
->collapsible()
->icon('tabler-dashboard')
->schema([
ToggleButtons::make('dashboard_layout')
->label(trans('profile.dashboard_layout'))
->inline()
->required()
->options([
'grid' => trans('profile.grid'),
'table' => trans('profile.table'),
]),
]),
Section::make(trans('profile.console'))
->collapsible()
->icon('tabler-brand-tabler')
->schema([
TextInput::make('console_rows')
->label(trans('profile.rows'))
->minValue(1)
->numeric()
->required()
->columnSpan(1)
->default(30),
// Select::make('console_font')
// ->label(trans('profile.font'))
// ->hidden() //TODO
// ->columnSpan(1),
TextInput::make('console_font_size')
->label(trans('profile.font_size'))
->columnSpan(1)
->minValue(1)
->numeric()
->required()
->default(14),
]),
]),
]),
])
->operation('edit')
@@ -396,7 +345,7 @@ class EditProfile extends BaseEditProfile
$tokens = $this->toggleTwoFactorService->handle($record, $token, true);
cache()->put("users.$record->id.2fa.tokens", implode("\n", $tokens), now()->addSeconds(15));
$this->redirect(self::getUrl(['tab' => '-2fa-tab'], panel: 'app'));
$this->redirectRoute('filament.admin.auth.profile', ['tab' => '-2fa-tab']);
}
if ($token = $data['2fa-disable-code'] ?? null) {
@@ -432,29 +381,4 @@ class EditProfile extends BaseEditProfile
];
}
protected function mutateFormDataBeforeSave(array $data): array
{
$moarbetterdata = [
'console_font_size' => $data['console_font_size'],
'console_rows' => $data['console_rows'],
'dashboard_layout' => $data['dashboard_layout'],
];
unset($data['dashboard_layout'], $data['console_font_size'], $data['console_rows']);
$data['customization'] = json_encode($moarbetterdata);
return $data;
}
protected function mutateFormDataBeforeFill(array $data): array
{
$moarbetterdata = json_decode($data['customization'], true);
$data['console_font_size'] = $moarbetterdata['console_font_size'] ?? 14;
$data['console_rows'] = $moarbetterdata['console_rows'] ?? 30;
$data['dashboard_layout'] = $moarbetterdata['dashboard_layout'] ?? 'grid';
return $data;
}
}

View File

@@ -2,7 +2,6 @@
namespace App\Filament\Server\Pages;
use App\Enums\ConsoleWidgetPosition;
use App\Enums\ContainerStatus;
use App\Exceptions\Http\Server\ServerStateConflictException;
use App\Filament\Server\Widgets\ServerConsole;
@@ -39,10 +38,10 @@ class Console extends Page
try {
$server->validateCurrentState();
} catch (ServerStateConflictException $exception) {
AlertBanner::make('server_conflict')
AlertBanner::make()
->warning()
->title('Warning')
->body($exception->getMessage())
->warning()
->send();
}
}
@@ -55,41 +54,18 @@ class Console extends Page
];
}
/** @var array<string, array<class-string<Widget>>> */
protected static array $customWidgets = [];
/** @param class-string<Widget>[] $customWidgets */
public static function registerCustomWidgets(ConsoleWidgetPosition $position, array $customWidgets): void
{
static::$customWidgets[$position->value] = array_unique(array_merge(static::$customWidgets[$position->value] ?? [], $customWidgets));
}
/**
* @return class-string<Widget>[]
*/
public function getWidgets(): array
{
$allWidgets = [];
$allWidgets = array_merge($allWidgets, static::$customWidgets[ConsoleWidgetPosition::Top->value] ?? []);
$allWidgets[] = ServerOverview::class;
$allWidgets = array_merge($allWidgets, static::$customWidgets[ConsoleWidgetPosition::AboveConsole->value] ?? []);
$allWidgets[] = ServerConsole::class;
$allWidgets = array_merge($allWidgets, static::$customWidgets[ConsoleWidgetPosition::BelowConsole->value] ?? []);
$allWidgets = array_merge($allWidgets, [
return [
ServerOverview::class,
ServerConsole::class,
ServerCpuChart::class,
ServerMemoryChart::class,
//ServerNetworkChart::class, TODO: convert units.
]);
$allWidgets = array_merge($allWidgets, static::$customWidgets[ConsoleWidgetPosition::Bottom->value] ?? []);
return array_unique($allWidgets);
];
}
/**
@@ -128,23 +104,20 @@ class Console extends Page
->size(ActionSize::ExtraLarge)
->action(fn () => $this->dispatch('setServerState', state: 'start', uuid: $server->uuid))
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_START, $server))
->disabled(fn () => $server->isInConflictState() || !$this->status->isStartable())
->icon('tabler-player-play-filled'),
->disabled(fn () => $server->isInConflictState() || !$this->status->isStartable()),
Action::make('restart')
->color('gray')
->size(ActionSize::ExtraLarge)
->action(fn () => $this->dispatch('setServerState', state: 'restart', uuid: $server->uuid))
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_RESTART, $server))
->disabled(fn () => $server->isInConflictState() || !$this->status->isRestartable())
->icon('tabler-reload'),
->disabled(fn () => $server->isInConflictState() || !$this->status->isRestartable()),
Action::make('stop')
->color('danger')
->size(ActionSize::ExtraLarge)
->action(fn () => $this->dispatch('setServerState', state: 'stop', uuid: $server->uuid))
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
->hidden(fn () => $this->status->isStartingOrStopping() || $this->status->isKillable())
->disabled(fn () => $server->isInConflictState() || !$this->status->isStoppable())
->icon('tabler-player-stop-filled'),
->disabled(fn () => $server->isInConflictState() || !$this->status->isStoppable()),
Action::make('kill')
->color('danger')
->requiresConfirmation()
@@ -154,8 +127,7 @@ class Console extends Page
->size(ActionSize::ExtraLarge)
->action(fn () => $this->dispatch('setServerState', state: 'kill', uuid: $server->uuid))
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
->hidden(fn () => $server->isInConflictState() || !$this->status->isKillable())
->icon('tabler-alert-square'),
->hidden(fn () => $server->isInConflictState() || !$this->status->isKillable()),
];
}
}

View File

@@ -192,7 +192,7 @@ class Settings extends ServerFormPage
]),
Section::make('Reinstall Server')
->hidden(fn () => !auth()->user()->can(Permission::ACTION_SETTINGS_REINSTALL, $server))
->collapsible()
->collapsible()->collapsed()
->footerActions([
Action::make('reinstall')
->color('danger')
@@ -226,8 +226,6 @@ class Settings extends ServerFormPage
->success()
->title('Server Reinstall started')
->send();
redirect(Console::getUrl());
}),
])
->footerActionsAlignment(Alignment::Right)
@@ -259,6 +257,7 @@ class Settings extends ServerFormPage
Notification::make()
->success()
->duration(5000) // 5 seconds
->title('Updated Server Name')
->body(fn () => $original . ' -> ' . $name)
->send();
@@ -290,6 +289,7 @@ class Settings extends ServerFormPage
Notification::make()
->success()
->duration(5000) // 5 seconds
->title('Updated Server Description')
->body(fn () => $original . ' -> ' . $description)
->send();

View File

@@ -31,7 +31,7 @@ class ListActivities extends ListRecords
$server = Filament::getTenant();
return $table
->paginated([25, 50])
->paginated([25, 50, 100, 250])
->defaultPaginationPageOption(25)
->columns([
TextColumn::make('event')

View File

@@ -31,7 +31,6 @@ use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\HttpException;
class ListBackups extends ListRecords
{
@@ -167,27 +166,18 @@ class ListBackups extends ListRecords
$action->setIsLocked((bool) $data['is_locked']);
}
try {
$backup = $action->handle($server, $data['name']);
$backup = $action->handle($server, $data['name']);
Activity::event('server:backup.start')
->subject($backup)
->property(['name' => $backup->name, 'locked' => (bool) $data['is_locked']])
->log();
Activity::event('server:backup.start')
->subject($backup)
->property(['name' => $backup->name, 'locked' => (bool) $data['is_locked']])
->log();
return Notification::make()
->title('Backup Created')
->body($backup->name . ' created.')
->success()
->send();
} catch (HttpException $e) {
return Notification::make()
->danger()
->title('Backup Failed')
->body($e->getMessage() . ' Try again' . ($e->getHeaders()['Retry-After'] ? ' in ' . $e->getHeaders()['Retry-After'] . ' seconds.' : ''))
->send();
}
return Notification::make()
->title('Backup Created')
->body($backup->name . ' created.')
->success()
->send();
}),
];
}

View File

@@ -4,8 +4,6 @@ namespace App\Filament\Server\Resources\FileResource\Pages;
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
use App\Enums\EditorLanguages;
use App\Exceptions\Http\Server\FileSizeTooLargeException;
use App\Exceptions\Repository\FileNotEditableException;
use App\Facades\Activity;
use App\Filament\Server\Resources\FileResource;
use App\Livewire\AlertBanner;
@@ -47,8 +45,6 @@ class EditFiles extends Page
#[Locked]
public string $path;
private DaemonFileRepository $fileRepository;
/** @var array<mixed> */
public ?array $data = [];
@@ -70,8 +66,12 @@ class EditFiles extends Page
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->icon('tabler-device-floppy')
->keyBindings('mod+shift+s')
->action(function () {
$this->getDaemonFileRepository()->putContent($this->path, $this->data['editor'] ?? '');
->action(function (DaemonFileRepository $fileRepository) use ($server) {
$data = $this->form->getState();
$fileRepository
->setServer($server)
->putContent($this->path, $data['editor'] ?? '');
Activity::event('server:file.write')
->property('file', $this->path)
@@ -90,8 +90,12 @@ class EditFiles extends Page
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->icon('tabler-device-floppy')
->keyBindings('mod+s')
->action(function () {
$this->getDaemonFileRepository()->putContent($this->path, $this->data['editor'] ?? '');
->action(function (DaemonFileRepository $fileRepository) use ($server) {
$data = $this->form->getState();
$fileRepository
->setServer($server)
->putContent($this->path, $data['editor'] ?? '');
Activity::event('server:file.write')
->property('file', $this->path)
@@ -113,46 +117,21 @@ class EditFiles extends Page
->schema([
Select::make('lang')
->label('Syntax Highlighting')
->searchable()
->native(false)
->live()
->options(EditorLanguages::class)
->selectablePlaceholder(false)
->afterStateUpdated(fn ($state) => $this->dispatch('setLanguage', lang: $state))
->default(fn () => EditorLanguages::fromWithAlias(pathinfo($this->path, PATHINFO_EXTENSION))),
MonacoEditor::make('editor')
->hiddenLabel()
->showPlaceholder(false)
->default(function () {
->label('')
->placeholderText('')
->default(function (DaemonFileRepository $fileRepository) use ($server) {
try {
return $this->getDaemonFileRepository()->getContent($this->path, config('panel.files.max_edit_size'));
} catch (FileSizeTooLargeException) {
AlertBanner::make()
->title('File too large!')
->body('<code>' . $this->path . '</code> Max is ' . convert_bytes_to_readable(config('panel.files.max_edit_size')))
->danger()
->closable()
->send();
$this->redirect(ListFiles::getUrl());
return $fileRepository
->setServer($server)
->getContent($this->path, config('panel.files.max_edit_size'));
} catch (FileNotFoundException) {
AlertBanner::make()
->title('File Not found!')
->body('<code>' . $this->path . '</code>')
->danger()
->closable()
->send();
$this->redirect(ListFiles::getUrl());
} catch (FileNotEditableException) {
AlertBanner::make()
->title('Could not edit directory!')
->body('<code>' . $this->path . '</code>')
->danger()
->closable()
->send();
$this->redirect(ListFiles::getUrl());
abort(404, $this->path . ' not found.');
}
})
->language(fn (Get $get) => $get('lang'))
@@ -170,7 +149,7 @@ class EditFiles extends Page
$this->form->fill();
if (str($path)->endsWith('.pelicanignore')) {
AlertBanner::make('.pelicanignore_info')
AlertBanner::make()
->title('You\'re editing a <code>.pelicanignore</code> file!')
->body('Any files or directories listed in here will be excluded from backups. Wildcards are supported by using an asterisk (<code>*</code>).<br>You can negate a prior rule by prepending an exclamation point (<code>!</code>).')
->info()
@@ -221,15 +200,6 @@ class EditFiles extends Page
return $breadcrumbs;
}
private function getDaemonFileRepository(): DaemonFileRepository
{
/** @var Server $server */
$server = Filament::getTenant();
$this->fileRepository ??= (new DaemonFileRepository())->setServer($server);
return $this->fileRepository;
}
public static function route(string $path): PageRegistration
{
return new PageRegistration(

View File

@@ -12,7 +12,6 @@ use App\Models\Server;
use App\Repositories\Daemon\DaemonFileRepository;
use App\Filament\Components\Tables\Columns\BytesColumn;
use App\Filament\Components\Tables\Columns\DateTimeColumn;
use App\Livewire\AlertBanner;
use Filament\Actions\Action as HeaderAction;
use Filament\Facades\Filament;
use Filament\Forms\Components\CheckboxList;
@@ -37,10 +36,8 @@ use Filament\Tables\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\UploadedFile;
use Illuminate\Routing\Route;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Route as RouteFacade;
use Livewire\Attributes\Locked;
@@ -51,26 +48,10 @@ class ListFiles extends ListRecords
#[Locked]
public string $path;
private DaemonFileRepository $fileRepository;
private bool $isDisabled = false;
public function mount(?string $path = null): void
{
parent::mount();
$this->path = $path ?? '/';
try {
$this->getDaemonFileRepository()->getDirectory('/');
} catch (ConnectionException) {
$this->isDisabled = true;
AlertBanner::make('node_connection_error')
->title('Could not connect to the node!')
->danger()
->send();
}
}
public function getBreadcrumbs(): array
@@ -95,12 +76,10 @@ class ListFiles extends ListRecords
/** @var Server $server */
$server = Filament::getTenant();
$files = File::get($server, $this->path);
return $table
->paginated([25, 50])
->defaultPaginationPageOption(25)
->query(fn () => $files->orderByDesc('is_directory'))
->paginated([25, 50, 100, 250])
->defaultPaginationPageOption(50)
->query(fn () => File::get($server, $this->path)->orderByDesc('is_directory'))
->defaultSort('name')
->columns([
TextColumn::make('name')
@@ -109,7 +88,6 @@ class ListFiles extends ListRecords
->icon(fn (File $file) => $file->getIcon()),
BytesColumn::make('size')
->visibleFrom('md')
->state(fn (File $file) => $file->is_directory ? null : $file->size)
->sortable(),
DateTimeColumn::make('modified_at')
->visibleFrom('md')
@@ -130,21 +108,18 @@ class ListFiles extends ListRecords
->actions([
Action::make('view')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server))
->disabled($this->isDisabled)
->label('Open')
->icon('tabler-eye')
->visible(fn (File $file) => $file->is_directory)
->url(fn (File $file) => self::getUrl(['path' => join_paths($this->path, $file->name)])),
EditAction::make('edit')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server))
->disabled($this->isDisabled)
->icon('tabler-edit')
->visible(fn (File $file) => $file->canEdit())
->url(fn (File $file) => EditFiles::getUrl(['path' => join_paths($this->path, $file->name)])),
ActionGroup::make([
Action::make('rename')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->disabled($this->isDisabled)
->label('Rename')
->icon('tabler-forms')
->form([
@@ -153,10 +128,12 @@ class ListFiles extends ListRecords
->default(fn (File $file) => $file->name)
->required(),
])
->action(function ($data, File $file) {
->action(function ($data, File $file, DaemonFileRepository $fileRepository) use ($server) {
$files = [['to' => $data['name'], 'from' => $file->name]];
$this->getDaemonFileRepository()->renameFiles($this->path, $files);
$fileRepository
->setServer($server)
->renameFiles($this->path, $files);
Activity::event('server:file.rename')
->property('directory', $this->path)
@@ -173,12 +150,13 @@ class ListFiles extends ListRecords
}),
Action::make('copy')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
->disabled($this->isDisabled)
->label('Copy')
->icon('tabler-copy')
->visible(fn (File $file) => $file->is_file)
->action(function (File $file) {
$this->getDaemonFileRepository()->copyFile(join_paths($this->path, $file->name));
->action(function (File $file, DaemonFileRepository $fileRepository) use ($server) {
$fileRepository
->setServer($server)
->copyFile(join_paths($this->path, $file->name));
Activity::event('server:file.copy')
->property('file', join_paths($this->path, $file->name))
@@ -193,50 +171,47 @@ class ListFiles extends ListRecords
}),
Action::make('download')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server))
->disabled($this->isDisabled)
->label('Download')
->icon('tabler-download')
->visible(fn (File $file) => $file->is_file)
->url(fn (File $file) => DownloadFiles::getUrl(['path' => join_paths($this->path, $file->name)]), true),
Action::make('move')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->disabled($this->isDisabled)
->label('Move')
->icon('tabler-replace')
->form([
TextInput::make('location')
->label('New location')
->hint('Enter the location of this file or folder, relative to the current directory.')
->label('File name')
->hint('Enter the new name and directory of this file or folder, relative to the current directory.')
->default(fn (File $file) => $file->name)
->required()
->live(),
Placeholder::make('new_location')
->content(fn (Get $get, File $file) => resolve_path('./' . join_paths($this->path, $get('location') ?? '/', $file->name))),
->content(fn (Get $get) => resolve_path('./' . join_paths($this->path, $get('location')))),
])
->action(function ($data, File $file) {
$location = rtrim($data['location'], '/');
$files = [['to' => join_paths($location, $file->name), 'from' => $file->name]];
->action(function ($data, File $file, DaemonFileRepository $fileRepository) use ($server) {
$location = resolve_path(join_paths($this->path, $data['location']));
$this->getDaemonFileRepository()->renameFiles($this->path, $files);
$files = [['to' => $location, 'from' => $file->name]];
$oldLocation = join_paths($this->path, $file->name);
$newLocation = resolve_path(join_paths($this->path, $location, $file->name));
$fileRepository
->setServer($server)
->renameFiles($this->path, $files);
Activity::event('server:file.rename')
->property('directory', $this->path)
->property('files', $files)
->property('to', $newLocation)
->property('from', $oldLocation)
->property('to', $location)
->property('from', $file->name)
->log();
Notification::make()
->title('File Moved')
->body($oldLocation . ' -> ' . $newLocation)
->title(join_paths($this->path, $file->name) . ' was moved to ' . $location)
->success()
->send();
}),
Action::make('permissions')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->disabled($this->isDisabled)
->label('Permissions')
->icon('tabler-license')
->form([
@@ -277,14 +252,16 @@ class ListFiles extends ListRecords
return $this->getPermissionsFromModeBit($mode);
}),
])
->action(function ($data, File $file) {
->action(function ($data, File $file, DaemonFileRepository $fileRepository) use ($server) {
$owner = (in_array('read', $data['owner']) ? 4 : 0) | (in_array('write', $data['owner']) ? 2 : 0) | (in_array('execute', $data['owner']) ? 1 : 0);
$group = (in_array('read', $data['group']) ? 4 : 0) | (in_array('write', $data['group']) ? 2 : 0) | (in_array('execute', $data['group']) ? 1 : 0);
$public = (in_array('read', $data['public']) ? 4 : 0) | (in_array('write', $data['public']) ? 2 : 0) | (in_array('execute', $data['public']) ? 1 : 0);
$mode = $owner . $group . $public;
$this->getDaemonFileRepository()->chmodFiles($this->path, [['file' => $file->name, 'mode' => $mode]]);
$fileRepository
->setServer($server)
->chmodFiles($this->path, [['file' => $file->name, 'mode' => $mode]]);
Notification::make()
->title('Permissions changed to ' . $mode)
@@ -293,27 +270,20 @@ class ListFiles extends ListRecords
}),
Action::make('archive')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
->disabled($this->isDisabled)
->label('Archive')
->icon('tabler-archive')
->form([
TextInput::make('name')
->label('Archive name')
->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z')
->suffix('.tar.gz'),
])
->action(function ($data, File $file) {
$archive = $this->getDaemonFileRepository()->compressFiles($this->path, [$file->name], $data['name']);
->action(function (File $file, DaemonFileRepository $fileRepository) use ($server) {
$fileRepository
->setServer($server)
->compressFiles($this->path, [$file->name]);
Activity::event('server:file.compress')
->property('name', $archive['name'])
->property('directory', $this->path)
->property('files', [$file->name])
->log();
Notification::make()
->title('Archive created')
->body($archive['name'])
->success()
->send();
@@ -321,12 +291,13 @@ class ListFiles extends ListRecords
}),
Action::make('unarchive')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
->disabled($this->isDisabled)
->label('Unarchive')
->icon('tabler-archive')
->visible(fn (File $file) => $file->isArchive())
->action(function (File $file) {
$this->getDaemonFileRepository()->decompressFile($this->path, $file->name);
->action(function (File $file, DaemonFileRepository $fileRepository) use ($server) {
$fileRepository
->setServer($server)
->decompressFile($this->path, $file->name);
Activity::event('server:file.decompress')
->property('directory', $this->path)
@@ -343,14 +314,15 @@ class ListFiles extends ListRecords
]),
DeleteAction::make()
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server))
->disabled($this->isDisabled)
->label('')
->icon('tabler-trash')
->requiresConfirmation()
->modalDescription(fn (File $file) => $file->name)
->modalHeading('Delete file?')
->action(function (File $file) {
$this->getDaemonFileRepository()->deleteFiles($this->path, [$file->name]);
->action(function (File $file, DaemonFileRepository $fileRepository) use ($server) {
$fileRepository
->setServer($server)
->deleteFiles($this->path, [$file->name]);
Activity::event('server:file.delete')
->property('directory', $this->path)
@@ -362,21 +334,23 @@ class ListFiles extends ListRecords
BulkActionGroup::make([
BulkAction::make('move')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->disabled($this->isDisabled)
->hidden() // TODO
->form([
TextInput::make('location')
->label('Directory')
->hint('Enter the new directory, relative to the current directory.')
->label('File name')
->hint('Enter the new name and directory of this file or folder, relative to the current directory.')
->default(fn (File $file) => $file->name)
->required()
->live(),
Placeholder::make('new_location')
->content(fn (Get $get) => resolve_path('./' . join_paths($this->path, $get('location') ?? ''))),
])
->action(function (Collection $files, $data) {
$location = rtrim($data['location'], '/');
->action(function (Collection $files, $data, DaemonFileRepository $fileRepository) use ($server) {
$location = resolve_path(join_paths($this->path, $data['location']));
$files = $files->map(fn ($file) => ['to' => join_paths($location, $file['name']), 'from' => $file['name']])->toArray();
$this->getDaemonFileRepository()
$files = $files->map(fn ($file) => ['to' => $location, 'from' => $file['name']])->toArray();
$fileRepository
->setServer($server)
->renameFiles($this->path, $files);
Activity::event('server:file.rename')
@@ -385,33 +359,26 @@ class ListFiles extends ListRecords
->log();
Notification::make()
->title(count($files) . ' Files were moved to ' . resolve_path(join_paths($this->path, $location)))
->title(count($files) . ' Files were moved from ' . $location)
->success()
->send();
}),
BulkAction::make('archive')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
->disabled($this->isDisabled)
->form([
TextInput::make('name')
->label('Archive name')
->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z')
->suffix('.tar.gz'),
])
->action(function ($data, Collection $files) {
->action(function (Collection $files, DaemonFileRepository $fileRepository) use ($server) {
$files = $files->map(fn ($file) => $file['name'])->toArray();
$archive = $this->getDaemonFileRepository()->compressFiles($this->path, $files, $data['name']);
$fileRepository
->setServer($server)
->compressFiles($this->path, $files);
Activity::event('server:file.compress')
->property('name', $archive['name'])
->property('directory', $this->path)
->property('files', $files)
->log();
Notification::make()
->title('Archive created')
->body($archive['name'])
->success()
->send();
@@ -419,10 +386,11 @@ class ListFiles extends ListRecords
}),
DeleteBulkAction::make()
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server))
->disabled($this->isDisabled)
->action(function (Collection $files) {
->action(function (Collection $files, DaemonFileRepository $fileRepository) use ($server) {
$files = $files->map(fn ($file) => $file['name'])->toArray();
$this->getDaemonFileRepository()->deleteFiles($this->path, $files);
$fileRepository
->setServer($server)
->deleteFiles($this->path, $files);
Activity::event('server:file.delete')
->property('directory', $this->path)
@@ -446,13 +414,14 @@ class ListFiles extends ListRecords
return [
HeaderAction::make('new_file')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
->disabled($this->isDisabled)
->label('New File')
->color('gray')
->keyBindings('')
->modalSubmitActionLabel('Create')
->action(function ($data) {
$this->getDaemonFileRepository()->putContent(join_paths($this->path, $data['name']), $data['editor'] ?? '');
->action(function ($data, DaemonFileRepository $fileRepository) use ($server) {
$fileRepository
->setServer($server)
->putContent(join_paths($this->path, $data['name']), $data['editor'] ?? '');
Activity::event('server:file.write')
->property('file', join_paths($this->path, $data['name']))
@@ -464,8 +433,6 @@ class ListFiles extends ListRecords
->required(),
Select::make('lang')
->label('Syntax Highlighting')
->searchable()
->native(false)
->live()
->options(EditorLanguages::class)
->selectablePlaceholder(false)
@@ -478,11 +445,12 @@ class ListFiles extends ListRecords
]),
HeaderAction::make('new_folder')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
->disabled($this->isDisabled)
->label('New Folder')
->color('gray')
->action(function ($data) {
$this->getDaemonFileRepository()->createDirectory($data['name'], $this->path);
->action(function ($data, DaemonFileRepository $fileRepository) use ($server) {
$fileRepository
->setServer($server)
->createDirectory($data['name'], $this->path);
Activity::event('server:file.create-directory')
->property(['directory' => $this->path, 'name' => $data['name']])
@@ -495,13 +463,14 @@ class ListFiles extends ListRecords
]),
HeaderAction::make('upload')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
->disabled($this->isDisabled)
->label('Upload')
->action(function ($data) {
->action(function ($data, DaemonFileRepository $fileRepository) use ($server) {
if (count($data['files']) > 0 && !isset($data['url'])) {
/** @var UploadedFile $file */
foreach ($data['files'] as $file) {
$this->getDaemonFileRepository()->putContent(join_paths($this->path, $file->getClientOriginalName()), $file->getContent());
$fileRepository
->setServer($server)
->putContent(join_paths($this->path, $file->getClientOriginalName()), $file->getContent());
Activity::event('server:file.uploaded')
->property('directory', $this->path)
@@ -509,7 +478,9 @@ class ListFiles extends ListRecords
->log();
}
} elseif ($data['url'] !== null) {
$this->getDaemonFileRepository()->pull($data['url'], $this->path);
$fileRepository
->setServer($server)
->pull($data['url'], $this->path);
Activity::event('server:file.pull')
->property('url', $data['url'])
@@ -545,7 +516,6 @@ class ListFiles extends ListRecords
]),
HeaderAction::make('search')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server))
->disabled($this->isDisabled)
->label('Global Search')
->modalSubmitActionLabel('Search')
->form([
@@ -561,32 +531,6 @@ class ListFiles extends ListRecords
];
}
/**
* @return string[]
*/
private function getPermissionsFromModeBit(int $mode): array
{
return match ($mode) {
1 => ['execute'],
2 => ['write'],
3 => ['write', 'execute'],
4 => ['read'],
5 => ['read', 'execute'],
6 => ['read', 'write'],
7 => ['read', 'write', 'execute'],
default => [],
};
}
private function getDaemonFileRepository(): DaemonFileRepository
{
/** @var Server $server */
$server = Filament::getTenant();
$this->fileRepository ??= (new DaemonFileRepository())->setServer($server);
return $this->fileRepository;
}
public static function route(string $path): PageRegistration
{
return new PageRegistration(
@@ -597,4 +541,28 @@ class ListFiles extends ListRecords
->where('path', '.*'),
);
}
/**
* @return string[]
*/
private function getPermissionsFromModeBit(int $mode): array
{
if ($mode === 1) {
return ['execute'];
} elseif ($mode === 2) {
return ['write'];
} elseif ($mode === 3) {
return ['write', 'execute'];
} elseif ($mode === 4) {
return ['read'];
} elseif ($mode === 5) {
return ['read', 'execute'];
} elseif ($mode === 6) {
return ['read', 'write'];
} elseif ($mode === 7) {
return ['read', 'write', 'execute'];
}
return [];
}
}

View File

@@ -4,12 +4,9 @@ namespace App\Filament\Server\Resources;
use App\Filament\Server\Resources\ScheduleResource\Pages;
use App\Filament\Server\Resources\ScheduleResource\RelationManagers\TasksRelationManager;
use App\Helpers\Utilities;
use App\Models\Permission;
use App\Models\Schedule;
use App\Models\Server;
use Carbon\Carbon;
use Exception;
use Filament\Facades\Filament;
use Filament\Forms\Components\Actions;
use Filament\Forms\Components\Actions\Action;
@@ -20,9 +17,7 @@ use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Form;
use Filament\Forms\Set;
use Filament\Notifications\Notification;
use Filament\Resources\Resource;
use Filament\Support\Exceptions\Halt;
use Illuminate\Database\Eloquent\Model;
class ScheduleResource extends Resource
@@ -319,18 +314,4 @@ class ScheduleResource extends Resource
'edit' => Pages\EditSchedule::route('/{record}/edit'),
];
}
public static function getNextRun(string $minute, string $hour, string $dayOfMonth, string $month, string $dayOfWeek): Carbon
{
try {
return Utilities::getScheduleNextRunDate($minute, $hour, $dayOfMonth, $month, $dayOfWeek);
} catch (Exception) {
Notification::make()
->title('The cron data provided does not evaluate to a valid expression')
->danger()
->send();
throw new Halt();
}
}
}

View File

@@ -2,10 +2,14 @@
namespace App\Filament\Server\Resources\ScheduleResource\Pages;
use App\Exceptions\DisplayException;
use App\Facades\Activity;
use App\Filament\Server\Resources\ScheduleResource;
use App\Helpers\Utilities;
use App\Models\Schedule;
use App\Models\Server;
use Carbon\Carbon;
use Exception;
use Filament\Facades\Filament;
use Filament\Resources\Pages\CreateRecord;
@@ -35,18 +39,27 @@ class CreateSchedule extends CreateRecord
}
if (!isset($data['next_run_at'])) {
$data['next_run_at'] = ScheduleResource::getNextRun(
$data['cron_minute'],
$data['cron_hour'],
$data['cron_day_of_month'],
$data['cron_month'],
$data['cron_day_of_week']
);
$data['next_run_at'] = $this->getNextRunAt($data['cron_minute'], $data['cron_hour'], $data['cron_day_of_month'], $data['cron_month'], $data['cron_day_of_week']);
}
return $data;
}
protected function getNextRunAt(string $minute, string $hour, string $dayOfMonth, string $month, string $dayOfWeek): Carbon
{
try {
return Utilities::getScheduleNextRunDate(
$minute,
$hour,
$dayOfMonth,
$month,
$dayOfWeek
);
} catch (Exception) {
throw new DisplayException('The cron data provided does not evaluate to a valid expression.');
}
}
public function getBreadcrumbs(): array
{
return [];

View File

@@ -22,19 +22,6 @@ class EditSchedule extends EditRecord
->log();
}
protected function mutateFormDataBeforeSave(array $data): array
{
$data['next_run_at'] = ScheduleResource::getNextRun(
$data['cron_minute'],
$data['cron_hour'],
$data['cron_day_of_month'],
$data['cron_month'],
$data['cron_day_of_week']
);
return $data;
}
protected function getHeaderActions(): array
{
return [

View File

@@ -39,10 +39,8 @@ class ListSchedules extends ListRecords
->sortable(),
DateTimeColumn::make('next_run_at')
->label('Next run')
->placeholder('Never')
->since()
->sortable()
->state(fn (Schedule $schedule) => $schedule->is_active ? $schedule->next_run_at : null),
->sortable(),
])
->actions([
ViewAction::make(),

View File

@@ -5,6 +5,7 @@ namespace App\Filament\Server\Resources;
use App\Filament\Server\Resources\UserResource\Pages;
use App\Models\Permission;
use App\Models\Server;
use App\Models\Subuser;
use App\Models\User;
use App\Services\Subusers\SubuserDeletionService;
use App\Services\Subusers\SubuserUpdateService;
@@ -90,21 +91,21 @@ class UserResource extends Resource
ImageColumn::make('picture')
->visibleFrom('lg')
->label('')
->alignCenter()->circular()
->defaultImageUrl(fn (User $user) => Filament::getUserAvatarUrl($user)),
->extraImgAttributes(['class' => 'rounded-full'])
->defaultImageUrl(fn (User $user) => 'https://gravatar.com/avatar/' . md5(strtolower($user->email))),
TextColumn::make('username')
->searchable(),
TextColumn::make('email')
->searchable(),
TextColumn::make('permissions')
->state(fn (User $user) => count($server->subusers->where('user_id', $user->id)->first()->permissions)),
->state(fn (User $user) => count(Subuser::query()->where('user_id', $user->id)->where('server_id', $server->id)->first()->permissions)),
])
->actions([
DeleteAction::make()
->label('Remove User')
->hidden(fn (User $user) => auth()->user()->id === $user->id)
->action(function (User $user, SubuserDeletionService $subuserDeletionService) use ($server) {
$subuser = $server->subusers->where('user_id', $user->id)->first();
$subuser = Subuser::query()->where('user_id', $user->id)->where('server_id', $server->id)->first();
$subuserDeletionService->handle($subuser, $server);
Notification::make()
@@ -118,7 +119,7 @@ class UserResource extends Resource
->authorize(fn () => auth()->user()->can(Permission::ACTION_USER_UPDATE, $server))
->modalHeading(fn (User $user) => 'Editing ' . $user->email)
->action(function (array $data, SubuserUpdateService $subuserUpdateService, User $user) use ($server) {
$subuser = $server->subusers->where('user_id', $user->id)->first();
$subuser = Subuser::query()->where('user_id', $user->id)->where('server_id', $server->id)->first();
$permissions = collect($data)
->forget('email')
@@ -215,9 +216,7 @@ class UserResource extends Resource
'settings' => [
'rename',
'reinstall',
],
'activity' => [
'read',
'activity',
],
];
@@ -244,7 +243,11 @@ class UserResource extends Resource
->schema([
CheckboxList::make('control')
->formatStateUsing(function (User $user, Set $set) use ($server) {
$permissionsArray = $server->subusers->where('user_id', $user->id)->first()->permissions;
$permissionsArray = Subuser::query()
->where('user_id', $user->id)
->where('server_id', $server->id)
->first()
->permissions;
$transformedPermissions = [];
@@ -261,7 +264,6 @@ class UserResource extends Resource
})
->bulkToggleable()
->label('')
->columns(2)
->options([
'console' => 'Console',
'start' => 'Start',
@@ -285,7 +287,6 @@ class UserResource extends Resource
CheckboxList::make('user')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'create' => 'Create',
@@ -309,7 +310,6 @@ class UserResource extends Resource
CheckboxList::make('file')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'read-content' => 'Read Content',
@@ -339,7 +339,6 @@ class UserResource extends Resource
CheckboxList::make('backup')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'create' => 'Create',
@@ -365,7 +364,6 @@ class UserResource extends Resource
CheckboxList::make('allocation')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'create' => 'Create',
@@ -389,7 +387,6 @@ class UserResource extends Resource
CheckboxList::make('startup')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'update' => 'Update',
@@ -411,7 +408,6 @@ class UserResource extends Resource
CheckboxList::make('database')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'create' => 'Create',
@@ -437,7 +433,6 @@ class UserResource extends Resource
CheckboxList::make('schedule')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'create' => 'Create',
@@ -461,32 +456,15 @@ class UserResource extends Resource
CheckboxList::make('settings')
->bulkToggleable()
->label('')
->columns(2)
->options([
'rename' => 'Rename',
'reinstall' => 'Reinstall',
'activity' => 'Activity',
])
->descriptions([
'rename' => trans('server/users.permissions.setting_rename'),
'reinstall' => trans('server/users.permissions.setting_reinstall'),
]),
]),
]),
Tab::make('Activity')
->schema([
Section::make()
->description(trans('server/users.permissions.activity_desc'))
->icon('tabler-stack')
->schema([
CheckboxList::make('activity')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
])
->descriptions([
'read' => trans('server/users.permissions.activity_read'),
'activity' => trans('server/users.permissions.activity_desc'),
]),
]),
]),

View File

@@ -16,7 +16,6 @@ use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Get;
use Filament\Forms\Set;
@@ -139,7 +138,7 @@ class ListUsers extends ListRecords
Tabs::make()
->columnSpanFull()
->schema([
Tab::make('Console')
Tabs\Tab::make('Console')
->schema([
Section::make()
->description(trans('server/users.permissions.control_desc'))
@@ -163,7 +162,7 @@ class ListUsers extends ListRecords
]),
]),
]),
Tab::make('User')
Tabs\Tab::make('User')
->schema([
Section::make()
->description(trans('server/users.permissions.user_desc'))
@@ -187,7 +186,7 @@ class ListUsers extends ListRecords
]),
]),
]),
Tab::make('File')
Tabs\Tab::make('File')
->schema([
Section::make()
->description(trans('server/users.permissions.file_desc'))
@@ -217,7 +216,7 @@ class ListUsers extends ListRecords
]),
]),
]),
Tab::make('Backup')
Tabs\Tab::make('Backup')
->schema([
Section::make()
->description(trans('server/users.permissions.backup_desc'))
@@ -243,7 +242,7 @@ class ListUsers extends ListRecords
]),
]),
]),
Tab::make('Allocation')
Tabs\Tab::make('Allocation')
->schema([
Section::make()
->description(trans('server/users.permissions.allocation_desc'))
@@ -267,7 +266,7 @@ class ListUsers extends ListRecords
]),
]),
]),
Tab::make('Startup')
Tabs\Tab::make('Startup')
->schema([
Section::make()
->description(trans('server/users.permissions.startup_desc'))
@@ -289,7 +288,7 @@ class ListUsers extends ListRecords
]),
]),
]),
Tab::make('Database')
Tabs\Tab::make('Database')
->schema([
Section::make()
->description(trans('server/users.permissions.database_desc'))
@@ -315,7 +314,7 @@ class ListUsers extends ListRecords
]),
]),
]),
Tab::make('Schedule')
Tabs\Tab::make('Schedule')
->schema([
Section::make()
->description(trans('server/users.permissions.schedule_desc'))
@@ -339,7 +338,7 @@ class ListUsers extends ListRecords
]),
]),
]),
Tab::make('Settings')
Tabs\Tab::make('Settings')
->schema([
Section::make()
->description(trans('server/users.permissions.settings_desc'))
@@ -352,14 +351,16 @@ class ListUsers extends ListRecords
->options([
'rename' => 'Rename',
'reinstall' => 'Reinstall',
'activity' => 'Activity',
])
->descriptions([
'rename' => trans('server/users.permissions.setting_rename'),
'reinstall' => trans('server/users.permissions.setting_reinstall'),
'activity' => trans('server/users.permissions.activity_desc'),
]),
]),
]),
Tab::make('Activity')
Tabs\Tab::make('Activity')
->schema([
Section::make()
->description(trans('server/users.permissions.activity_desc'))

View File

@@ -130,7 +130,7 @@ class ServerConsole extends Widget
#[On('websocket-error')]
public function websocketError(): void
{
AlertBanner::make('websocket_error')
AlertBanner::make()
->title('Could not connect to websocket!')
->body('Check your browser console for more details.')
->danger()

View File

@@ -4,6 +4,7 @@ namespace App\Filament\Server\Widgets;
use App\Enums\ContainerStatus;
use App\Filament\Server\Components\SmallStatBlock;
use App\Filament\Server\Components\StatBlock;
use App\Models\Server;
use Carbon\CarbonInterface;
use Filament\Widgets\StatsOverviewWidget;
@@ -18,12 +19,13 @@ class ServerOverview extends StatsOverviewWidget
protected function getStats(): array
{
return [
SmallStatBlock::make('Name', $this->server->name)
StatBlock::make('Name', $this->server->name)
->description($this->server->description)
->extraAttributes([
'class' => 'overflow-x-auto',
]),
SmallStatBlock::make('Status', $this->status()),
SmallStatBlock::make('Address', $this->server->allocation->address)
StatBlock::make('Status', $this->status()),
StatBlock::make('Address', $this->server->allocation->address)
->extraAttributes([
'class' => 'overflow-x-auto',
]),

View File

@@ -6,6 +6,7 @@ use App\Models\Node;
use Illuminate\Http\JsonResponse;
use Spatie\QueryBuilder\QueryBuilder;
use App\Services\Nodes\NodeUpdateService;
use App\Services\Nodes\NodeCreationService;
use App\Services\Nodes\NodeDeletionService;
use App\Transformers\Api\Application\NodeTransformer;
use App\Http\Requests\Api\Application\Nodes\GetNodeRequest;
@@ -15,7 +16,6 @@ use App\Http\Requests\Api\Application\Nodes\DeleteNodeRequest;
use App\Http\Requests\Api\Application\Nodes\UpdateNodeRequest;
use App\Http\Controllers\Api\Application\ApplicationApiController;
use Dedoc\Scramble\Attributes\Group;
use Exception;
#[Group('Node', weight: 0)]
class NodeController extends ApplicationApiController
@@ -24,6 +24,7 @@ class NodeController extends ApplicationApiController
* NodeController constructor.
*/
public function __construct(
private NodeCreationService $creationService,
private NodeDeletionService $deletionService,
private NodeUpdateService $updateService
) {
@@ -73,7 +74,7 @@ class NodeController extends ApplicationApiController
*/
public function store(StoreNodeRequest $request): JsonResponse
{
$node = Node::create($request->validated());
$node = $this->creationService->handle($request->validated());
return $this->fractal->item($node)
->transformWith($this->getTransformer(NodeTransformer::class))
@@ -96,15 +97,11 @@ class NodeController extends ApplicationApiController
*/
public function update(UpdateNodeRequest $request, Node $node): array
{
try {
$node = $this->updateService->handle(
$node,
$request->validated(),
$request->input('reset_secret') === true
);
} catch (Exception $exception) {
report($exception);
}
$node = $this->updateService->handle(
$node,
$request->validated(),
$request->input('reset_secret') === true
);
return $this->fractal->item($node)
->transformWith($this->getTransformer(NodeTransformer::class))

View File

@@ -13,7 +13,6 @@ use App\Services\Servers\TransferServerService;
use Dedoc\Scramble\Attributes\Group;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Response;
use Illuminate\Support\Arr;
#[Group('Server', weight: 4)]
class ServerManagementController extends ApplicationApiController
@@ -83,24 +82,15 @@ class ServerManagementController extends ApplicationApiController
$validatedData = $request->validate([
'node_id' => 'required|exists:nodes,id',
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
'allocation_additional' => 'nullable|array',
'allocation_additional.*' => 'integer|exists:allocations,id',
'allocation_additional' => 'nullable',
]);
if ($this->transferServerService->handle($server, Arr::get($validatedData, 'node_id'), Arr::get($validatedData, 'allocation_id'), Arr::get($validatedData, 'allocation_additional', []))) {
/**
* Transfer started
*
* @status 204
*/
if ($this->transferServerService->handle($server, $validatedData)) {
// Transfer started
return $this->returnNoContent();
}
/**
* Node was not viable
*
* @status 406
*/
// Node was not viable
return $this->returnNotAcceptable();
}
@@ -114,11 +104,7 @@ class ServerManagementController extends ApplicationApiController
public function cancelTransfer(ServerWriteRequest $request, Server $server): Response
{
if (!$transfer = $server->transfer) {
/**
* Server is not transferring
*
* @status 406
*/
// Server is not transferring
return $this->returnNotAcceptable();
}
@@ -127,11 +113,6 @@ class ServerManagementController extends ApplicationApiController
$this->daemonServerRepository->setServer($server)->cancelTransfer();
/**
* Transfer cancelled
*
* @status 204
*/
return $this->returnNoContent();
}
}

View File

@@ -172,8 +172,8 @@ class FileController extends ClientApiController
Activity::event('server:file.rename')
->property('directory', $request->input('root'))
->property('files', $files)
->property('to', $files[0]['to'])
->property('from', $files[0]['from'])
->property('to', $files['to'])
->property('from', $files['from'])
->log();
return new JsonResponse([], Response::HTTP_NO_CONTENT);
@@ -210,12 +210,10 @@ class FileController extends ClientApiController
{
$file = $this->fileRepository->setServer($server)->compressFiles(
$request->input('root'),
$request->input('files'),
$request->input('name')
$request->input('files')
);
Activity::event('server:file.compress')
->property('name', $file['name'])
->property('directory', $request->input('root'))
->property('files', $request->input('files'))
->log();

View File

@@ -5,8 +5,10 @@ namespace App\Http\Controllers\Api\Remote;
use Carbon\Carbon;
use Illuminate\Support\Str;
use App\Models\User;
use Webmozart\Assert\Assert;
use App\Models\Server;
use App\Models\ActivityLog;
use App\Models\ActivityLogSubject;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\Remote\ActivityEventRequest;
@@ -69,17 +71,19 @@ class ActivityProcessingController extends Controller
}
foreach ($logs as $key => $data) {
$server = $servers->get($key);
assert($server instanceof Server);
Assert::isInstanceOf($server = $servers->get($key), Server::class);
$batch = [];
foreach ($data as $datum) {
/** @var ActivityLog $activityLog */
$activityLog = ActivityLog::forceCreate($datum);
$activityLog->subjects()->create([
$id = ActivityLog::insertGetId($datum);
$batch[] = [
'activity_log_id' => $id,
'subject_id' => $server->id,
'subject_type' => $server->getMorphClass(),
]);
];
}
ActivityLogSubject::insert($batch);
}
}
}

View File

@@ -53,7 +53,7 @@ class OAuthController extends Controller
$this->updateService->handle($request->user(), ['oauth' => $oauth]);
return redirect(EditProfile::getUrl(['tab' => '-oauth-tab'], panel: 'app'));
return redirect(EditProfile::getUrl(['tab' => '-oauth-tab']));
}
try {

View File

@@ -21,7 +21,6 @@ class CompressFilesRequest extends ClientApiRequest
'root' => 'sometimes|nullable|string',
'files' => 'required|array',
'files.*' => 'string',
'name' => 'sometimes|nullable|string',
];
}
}

View File

@@ -44,8 +44,9 @@ final class AlertBanner implements Wireable
public static function fromLivewire(mixed $value): AlertBanner
{
$static = AlertBanner::make($value['id']);
$static = AlertBanner::make();
$static->id($value['id']);
$static->title($value['title']);
$static->body($value['body']);
$static->status($value['status']);

View File

@@ -3,7 +3,6 @@
namespace App\Livewire;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\On;
use Livewire\Component;
class AlertBannerContainer extends Component
@@ -17,7 +16,6 @@ class AlertBannerContainer extends Component
$this->pullFromSession();
}
#[On('alertBannerSent')]
public function pullFromSession(): void
{
foreach (session()->pull('alert-banners', []) as $alertBanner) {

View File

@@ -19,7 +19,6 @@ class DatabaseStep
'sqlite' => 'SQLite',
'mariadb' => 'MariaDB',
'mysql' => 'MySQL',
'pgsql' => 'PostgreSQL',
];
public static function make(PanelInstaller $installer): Step
@@ -40,24 +39,15 @@ class DatabaseStep
->afterStateUpdated(function ($state, Set $set, Get $get) {
$set('env_database.DB_DATABASE', $state === 'sqlite' ? 'database.sqlite' : 'panel');
switch ($state) {
case 'sqlite':
$set('env_database.DB_HOST', null);
$set('env_database.DB_PORT', null);
$set('env_database.DB_USERNAME', null);
$set('env_database.DB_PASSWORD', null);
break;
case 'mariadb':
case 'mysql':
$set('env_database.DB_HOST', $get('env_database.DB_HOST') ?? '127.0.0.1');
$set('env_database.DB_USERNAME', $get('env_database.DB_USERNAME') ?? 'pelican');
$set('env_database.DB_PORT', '3306');
break;
case 'pgsql':
$set('env_database.DB_HOST', $get('env_database.DB_HOST') ?? '127.0.0.1');
$set('env_database.DB_USERNAME', $get('env_database.DB_USERNAME') ?? 'pelican');
$set('env_database.DB_PORT', '5432');
break;
if ($state === 'sqlite') {
$set('env_database.DB_HOST', null);
$set('env_database.DB_PORT', null);
$set('env_database.DB_USERNAME', null);
$set('env_database.DB_PASSWORD', null);
} else {
$set('env_database.DB_HOST', $get('env_database.DB_HOST') ?? '127.0.0.1');
$set('env_database.DB_PORT', $get('env_database.DB_PORT') ?? '3306');
$set('env_database.DB_USERNAME', $get('env_database.DB_USERNAME') ?? 'pelican');
}
}),
TextInput::make('env_database.DB_DATABASE')
@@ -124,6 +114,7 @@ class DatabaseStep
'database' => $database,
'username' => $username,
'password' => $password,
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'strict' => true,
]);

View File

@@ -6,7 +6,6 @@ use App\Traits\HasValidation;
use Carbon\Carbon;
use Illuminate\Support\Facades\Event;
use App\Events\ActivityLogged;
use Filament\Facades\Filament;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
use Illuminate\Database\Eloquent\Builder;
@@ -121,6 +120,11 @@ class ActivityLog extends Model implements HasIcon, HasLabel
return $builder->whereMorphedTo('actor', $actor);
}
/**
* Returns models to be pruned.
*
* @see https://laravel.com/docs/9.x/eloquent#pruning-models
*/
public function prunable(): Builder
{
if (is_null(config('activity.prune_days'))) {
@@ -130,6 +134,10 @@ class ActivityLog extends Model implements HasIcon, HasLabel
return static::where('timestamp', '<=', Carbon::now()->subDays(config('activity.prune_days')));
}
/**
* Boots the model event listeners. This will trigger an activity log event every
* time a new model is inserted which can then be captured and worked with as needed.
*/
protected static function boot(): void
{
parent::boot();
@@ -173,11 +181,9 @@ class ActivityLog extends Model implements HasIcon, HasLabel
]);
}
$avatarUrl = Filament::getUserAvatarUrl($user);
return "
<div style='display: flex; align-items: center;'>
<img width='50px' height='50px' src='{$avatarUrl}' style='margin-right: 15px' />
<img width='50px' height='50px' src='{$user->getFilamentAvatarUrl()}' style='margin-right: 15px' />
<div>
<p>$user->username$this->event</p>

View File

@@ -72,20 +72,6 @@ class Allocation extends Model
static::deleting(function (self $allocation) {
throw_if($allocation->server_id, new ServerUsingAllocationException(trans('exceptions.allocations.server_using')));
});
static::updating(function ($allocation) {
$originalServerId = $allocation->getOriginal('server_id');
if (!$originalServerId) {
return;
}
$server = Server::find($originalServerId);
if (!$server) {
return;
}
if ($allocation->isDirty('server_id') && is_null($allocation->server_id) && $allocation->id === $server->allocation_id) {
return false;
}
});
}
protected function casts(): array

View File

@@ -46,7 +46,6 @@ use Illuminate\Support\Str;
* @property string|null $inherit_config_stop
* @property string $inherit_file_denylist
* @property string[]|null $inherit_features
* @property string[] $tags
* @property \Illuminate\Database\Eloquent\Collection|\App\Models\Server[] $servers
* @property int|null $servers_count
* @property \Illuminate\Database\Eloquent\Collection|\App\Models\EggVariable[] $variables
@@ -129,7 +128,6 @@ class Egg extends Model implements Validatable
'config_files' => ['required_without:config_from', 'nullable', 'json'],
'update_url' => ['sometimes', 'nullable', 'string'],
'force_outgoing_ip' => ['sometimes', 'boolean'],
'tags' => ['array'],
];
protected $attributes = [

View File

@@ -8,7 +8,6 @@ use Carbon\Carbon;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Client\ConnectionException;
use Sushi\Sushi;
/**
@@ -151,16 +150,10 @@ class File extends Model
try {
$fileRepository = (new DaemonFileRepository())->setServer(self::$server);
$contents = [];
try {
if (!is_null(self::$searchTerm)) {
$contents = cache()->remember('file_search_' . self::$path . '_' . self::$searchTerm, now()->addMinute(), fn () => $fileRepository->search(self::$searchTerm, self::$path));
} else {
$contents = $fileRepository->getDirectory(self::$path ?? '/');
}
} catch (ConnectionException $exception) {
report($exception);
if (!is_null(self::$searchTerm)) {
$contents = cache()->remember('file_search_' . self::$path . '_' . self::$searchTerm, now()->addMinute(), fn () => $fileRepository->search(self::$searchTerm, self::$path));
} else {
$contents = $fileRepository->getDirectory(self::$path ?? '/');
}
if (isset($contents['error'])) {

View File

@@ -89,7 +89,7 @@ class Node extends Model implements Validatable
'name' => ['required', 'string', 'min:1', 'max:100'],
'description' => ['string', 'nullable'],
'public' => ['boolean'],
'fqdn' => ['required', 'string', 'notIn:0.0.0.0,127.0.0.1,localhost'],
'fqdn' => ['required', 'string'],
'scheme' => ['required', 'string', 'in:http,https'],
'behind_proxy' => ['boolean'],
'memory' => ['required', 'numeric', 'min:0'],
@@ -104,7 +104,6 @@ class Node extends Model implements Validatable
'daemon_listen' => ['required', 'numeric', 'between:1,65535'],
'maintenance_mode' => ['boolean'],
'upload_size' => ['int', 'between:1,1024'],
'tags' => ['array'],
];
/**
@@ -365,20 +364,16 @@ class Node extends Model implements Validatable
];
try {
$this->systemInformation();
$data = Http::daemon($this)
return Http::daemon($this)
->connectTimeout(1)
->timeout(1)
->get('/api/system/utilization')
->json();
if ($data['memory_total']) {
return $data;
}
->json() ?? $default;
} catch (Exception) {
return $default;
}
return $default;
}
/** @return string[] */

View File

@@ -19,7 +19,7 @@ class RecoveryToken extends Model implements Validatable
use HasValidation;
/**
* There are no updates to this model, only creates and deletes.
* There are no updates to this model, only inserts and deletes.
*/
public const UPDATED_AT = null;

View File

@@ -2,8 +2,6 @@
namespace App\Models;
use App\Enums\RolePermissionModels;
use App\Enums\RolePermissionPrefixes;
use Spatie\Permission\Models\Role as BaseRole;
/**
@@ -43,76 +41,6 @@ class Role extends BaseRole
],
];
/** @var array<string, array<string>> */
protected static array $customPermissions = [];
/** @param array<string, array<string>> $customPermissions */
public static function registerCustomPermissions(array $customPermissions): void
{
static::$customPermissions = [
...static::$customPermissions,
...$customPermissions,
];
}
public static function registerCustomDefaultPermissions(string $model): void
{
$permissions = [];
foreach (RolePermissionPrefixes::cases() as $prefix) {
$permissions[] = $prefix->value;
}
static::registerCustomPermissions([
$model => $permissions,
]);
}
/** @return array<string, array<string>> */
public static function getPermissionList(): array
{
$allPermissions = [];
// Standard permissions for our default model
foreach (RolePermissionModels::cases() as $model) {
$allPermissions[$model->value] ??= [];
foreach (RolePermissionPrefixes::cases() as $prefix) {
array_push($allPermissions[$model->value], $prefix->value);
}
if (array_key_exists($model->value, Role::MODEL_SPECIFIC_PERMISSIONS)) {
foreach (static::MODEL_SPECIFIC_PERMISSIONS[$model->value] as $permission) {
array_push($allPermissions[$model->value], $permission);
}
}
}
// Special permissions for our default models
foreach (static::SPECIAL_PERMISSIONS as $model => $prefixes) {
$allPermissions[$model] ??= [];
foreach ($prefixes as $prefix) {
array_push($allPermissions[$model], $prefix);
}
}
// Custom third party permissions
foreach (static::$customPermissions as $model => $prefixes) {
$allPermissions[$model] ??= [];
foreach ($prefixes as $prefix) {
array_push($allPermissions[$model], $prefix);
}
}
foreach ($allPermissions as $model => $permissions) {
$allPermissions[$model] = array_unique($permissions);
}
return $allPermissions;
}
public function isRootAdmin(): bool
{
return $this->name === self::ROOT_ADMIN;

View File

@@ -315,7 +315,6 @@ class Server extends Model implements Validatable
{
return $this->serverVariables()
->join('egg_variables', 'egg_variables.id', '=', 'server_variables.variable_id')
->orderBy('egg_variables.sort')
->where('egg_variables.user_viewable', true);
}
@@ -481,7 +480,7 @@ class Server extends Model implements Validatable
}
if ($resourceAmount === 0 & $limit) {
return "\u{221E}";
return 'Unlimited';
}
if ($type === ServerResourceType::Percentage) {

View File

@@ -9,6 +9,7 @@ use App\Facades\Activity;
use App\Traits\HasValidation;
use DateTimeZone;
use Filament\Models\Contracts\FilamentUser;
use Filament\Models\Contracts\HasAvatar;
use Filament\Models\Contracts\HasName;
use Filament\Models\Contracts\HasTenants;
use Filament\Panel;
@@ -30,6 +31,7 @@ use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use App\Notifications\SendPasswordReset as ResetPasswordNotification;
use ResourceBundle;
use Spatie\Permission\Traits\HasRoles;
@@ -49,6 +51,7 @@ use Spatie\Permission\Traits\HasRoles;
* @property string|null $totp_secret
* @property \Illuminate\Support\Carbon|null $totp_authenticated_at
* @property string[]|null $oauth
* @property bool $gravatar
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Database\Eloquent\Collection|\App\Models\ApiKey[] $apiKeys
@@ -66,7 +69,6 @@ use Spatie\Permission\Traits\HasRoles;
* @property int|null $tokens_count
* @property \Illuminate\Database\Eloquent\Collection|\App\Models\Role[] $roles
* @property int|null $roles_count
* @property string|null $customization
*
* @method static \Database\Factories\UserFactory factory(...$parameters)
* @method static Builder|User newModelQuery()
@@ -75,6 +77,7 @@ use Spatie\Permission\Traits\HasRoles;
* @method static Builder|User whereCreatedAt($value)
* @method static Builder|User whereEmail($value)
* @method static Builder|User whereExternalId($value)
* @method static Builder|User whereGravatar($value)
* @method static Builder|User whereId($value)
* @method static Builder|User whereLanguage($value)
* @method static Builder|User whereTimezone($value)
@@ -87,7 +90,7 @@ use Spatie\Permission\Traits\HasRoles;
* @method static Builder|User whereUsername($value)
* @method static Builder|User whereUuid($value)
*/
class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract, FilamentUser, HasName, HasTenants, Validatable
class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract, FilamentUser, HasAvatar, HasName, HasTenants, Validatable
{
use Authenticatable;
use Authorizable { can as protected canned; }
@@ -121,8 +124,8 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
'use_totp',
'totp_secret',
'totp_authenticated_at',
'gravatar',
'oauth',
'customization',
];
/**
@@ -140,7 +143,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
'use_totp' => false,
'totp_secret' => null,
'oauth' => '[]',
'customization' => null,
];
/** @var array<array-key, string[]> */
@@ -155,20 +157,16 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
'use_totp' => ['boolean'],
'totp_secret' => ['nullable', 'string'],
'oauth' => ['array', 'nullable'],
'customization' => ['array', 'nullable'],
'customization.console_rows' => ['integer', 'min:1'],
'customization.console_font' => ['string'],
'customization.console_font_size' => ['integer', 'min:1'],
];
protected function casts(): array
{
return [
'use_totp' => 'boolean',
'gravatar' => 'boolean',
'totp_authenticated_at' => 'datetime',
'totp_secret' => 'encrypted',
'oauth' => 'array',
'customization' => 'array',
];
}
@@ -181,10 +179,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
return true;
});
static::saving(function (self $user) {
$user->email = mb_strtolower($user->email);
});
static::deleting(function (self $user) {
throw_if($user->servers()->count() > 0, new DisplayException(trans('exceptions.users.has_servers')));
@@ -207,6 +201,21 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
return $rules;
}
/**
* Send the password reset notification.
*
* @param string $token
*/
public function sendPasswordResetNotification($token): void
{
Activity::event('auth:reset-password')
->withRequestMetadata()
->subject($this)
->log('sending password reset email');
$this->notify(new ResetPasswordNotification($token));
}
public function username(): Attribute
{
return Attribute::make(
@@ -280,7 +289,8 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
->leftJoin('subusers', 'subusers.server_id', '=', 'servers.id')
->where(function (Builder $builder) {
$builder->where('servers.owner_id', $this->id)->orWhere('subusers.user_id', $this->id);
});
})
->groupBy('servers.id');
}
public function subusers(): HasMany
@@ -372,6 +382,11 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
return $this->username;
}
public function getFilamentAvatarUrl(): ?string
{
return 'https://gravatar.com/avatar/' . md5(strtolower($this->email));
}
public function canTarget(Model $user): bool
{
if ($this->isRootAdmin()) {
@@ -400,10 +415,4 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
return false;
}
/** @return array<mixed> */
public function getCustomization(): array
{
return json_decode($this->customization, true) ?? [];
}
}

View File

@@ -3,7 +3,6 @@
namespace App\Notifications;
use App\Models\User;
use Filament\Facades\Filament;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
@@ -30,7 +29,7 @@ class AccountCreated extends Notification implements ShouldQueue
->line('Email: ' . $notifiable->email);
if (!is_null($this->token)) {
return $message->action('Setup Your Account', Filament::getResetPasswordUrl($this->token, $notifiable));
return $message->action('Setup Your Account', url('/auth/password/reset/' . $this->token . '?email=' . urlencode($notifiable->email)));
}
return $message;

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Notifications;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
class SendPasswordReset extends Notification implements ShouldQueue
{
use Queueable;
public function __construct(public string $token) {}
/** @return string[] */
public function via(): array
{
return ['mail'];
}
public function toMail(User $notifiable): MailMessage
{
return (new MailMessage())
->subject('Reset Password')
->line('You are receiving this email because we received a password reset request for your account.')
->action('Reset Password', url('/auth/password/reset/' . $this->token . '?email=' . urlencode($notifiable->email)))
->line('If you did not request a password reset, no further action is required.');
}
}

View File

@@ -2,25 +2,18 @@
namespace App\Providers;
use App\Checks\CacheCheck;
use App\Checks\DatabaseCheck;
use App\Checks\DebugModeCheck;
use App\Checks\EnvironmentCheck;
use App\Checks\NodeVersionsCheck;
use App\Checks\PanelVersionCheck;
use App\Checks\ScheduleCheck;
use App\Checks\UsedDiskSpaceCheck;
use App\Extensions\Avatar\Providers\GravatarProvider;
use App\Extensions\Avatar\Providers\LocalAvatarProvider;
use App\Extensions\Avatar\Providers\UiAvatarsProvider;
use App\Extensions\OAuth\Providers\GitlabProvider;
use App\Models;
use App\Extensions\Captcha\Providers\TurnstileProvider;
use App\Extensions\OAuth\Providers\AuthentikProvider;
use App\Extensions\OAuth\Providers\CommonProvider;
use App\Extensions\OAuth\Providers\DiscordProvider;
use App\Extensions\OAuth\Providers\GithubProvider;
use App\Extensions\OAuth\Providers\SteamProvider;
use App\Models;
use App\Models\ApiKey;
use App\Models\Node;
use App\Models\User;
use App\Services\Helpers\SoftwareVersionService;
use Dedoc\Scramble\Scramble;
use Dedoc\Scramble\Support\Generator\OpenApi;
@@ -40,13 +33,14 @@ use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
use Laravel\Sanctum\Sanctum;
use Livewire\Component;
use Livewire\Livewire;
use App\Checks\CacheCheck;
use App\Checks\DatabaseCheck;
use App\Checks\DebugModeCheck;
use App\Checks\EnvironmentCheck;
use App\Checks\ScheduleCheck;
use App\Extensions\Captcha\Providers\TurnstileProvider;
use Spatie\Health\Facades\Health;
use function Livewire\on;
use function Livewire\store;
class AppServiceProvider extends ServiceProvider
{
/**
@@ -82,7 +76,7 @@ class AppServiceProvider extends ServiceProvider
Http::macro(
'daemon',
fn (Models\Node $node, array $headers = []) => Http::acceptJson()
fn (Node $node, array $headers = []) => Http::acceptJson()
->asJson()
->withToken($node->daemon_token)
->withHeaders($headers)
@@ -92,7 +86,7 @@ class AppServiceProvider extends ServiceProvider
->baseUrl($node->getConnectionAddress())
);
Sanctum::usePersonalAccessTokenModel(Models\ApiKey::class);
Sanctum::usePersonalAccessTokenModel(ApiKey::class);
Gate::define('viewApiDocs', fn () => true);
@@ -106,7 +100,7 @@ class AppServiceProvider extends ServiceProvider
CommonProvider::register($app, 'linkedin', null, 'tabler-brand-linkedin-f', '#0a66c2');
CommonProvider::register($app, 'google', null, 'tabler-brand-google-f', '#4285f4');
GithubProvider::register($app);
GitlabProvider::register($app);
CommonProvider::register($app, 'gitlab', null, 'tabler-brand-gitlab', '#fca326');
CommonProvider::register($app, 'bitbucket', null, 'tabler-brand-bitbucket-f', '#205081');
CommonProvider::register($app, 'slack', null, 'tabler-brand-slack', '#6ecadc');
@@ -118,11 +112,6 @@ class AppServiceProvider extends ServiceProvider
// Default Captcha provider
TurnstileProvider::register($app);
// Default Avatar providers
GravatarProvider::register();
UiAvatarsProvider::register();
LocalAvatarProvider::register();
FilamentColor::register([
'danger' => Color::Red,
'gray' => Color::Zinc,
@@ -134,7 +123,10 @@ class AppServiceProvider extends ServiceProvider
FilamentView::registerRenderHook(
PanelsRenderHook::HEAD_START,
fn () => Blade::render('filament.layouts.header')
fn (): string => Blade::render(<<<'HTML'
@vite(['resources/css/app.css', 'resources/js/app.js'])
@livewireStyles
HTML),
);
FilamentView::registerRenderHook(
@@ -144,30 +136,12 @@ class AppServiceProvider extends ServiceProvider
FilamentView::registerRenderHook(
PanelsRenderHook::BODY_END,
fn () => Blade::render('filament.layouts.body-end'),
fn (): string => Blade::render(<<<'HTML'
@livewireScripts
@vite(['resources/js/app.js'])
HTML),
);
FilamentView::registerRenderHook(
PanelsRenderHook::FOOTER,
fn () => Blade::render('filament.layouts.footer'),
);
on('dehydrate', function (Component $component) {
if (!Livewire::isLivewireRequest()) {
return;
}
if (store($component)->has('redirect')) {
return;
}
if (count(session()->get('alert-banners') ?? []) <= 0) {
return;
}
$component->dispatch('alertBannerSent');
});
// Don't run any health checks during tests
if (!$app->runningUnitTests()) {
Health::checks([
@@ -182,7 +156,7 @@ class AppServiceProvider extends ServiceProvider
]);
}
Gate::before(function (Models\User $user, $ability) {
Gate::before(function (User $user, $ability) {
return $user->isRootAdmin() ? true : null;
});

View File

@@ -2,7 +2,6 @@
namespace App\Providers\Filament;
use App\Extensions\Avatar\AvatarProvider;
use App\Filament\Pages\Auth\Login;
use App\Filament\Pages\Auth\EditProfile;
use App\Http\Middleware\LanguageMiddleware;
@@ -39,15 +38,11 @@ class AdminPanelProvider extends PanelProvider
->favicon(config('app.favicon', '/pelican.ico'))
->topNavigation(config('panel.filament.top-navigation', true))
->maxContentWidth(config('panel.filament.display-width', 'screen-2xl'))
->defaultAvatarProvider(fn () => get_class(AvatarProvider::getProvider(config('panel.filament.avatar-provider'))))
->profile(EditProfile::class, false)
->login(Login::class)
->passwordReset()
->userMenuItems([
'profile' => MenuItem::make()
->label(fn () => trans('filament-panels::pages/auth/edit-profile.label'))
->url(fn () => EditProfile::getUrl(panel: 'app')),
MenuItem::make()
->label(fn () => trans('profile.exit_admin'))
->label(trans('profile.exit_admin'))
->url('/')
->icon('tabler-arrow-back')
->sort(24),
@@ -62,7 +57,6 @@ class AdminPanelProvider extends PanelProvider
->sidebarCollapsibleOnDesktop()
->discoverResources(in: app_path('Filament/Admin/Resources'), for: 'App\\Filament\\Admin\\Resources')
->discoverPages(in: app_path('Filament/Admin/Pages'), for: 'App\\Filament\\Admin\\Pages')
->discoverWidgets(in: app_path('Filament/Admin/Widgets'), for: 'App\\Filament\\Admin\\Widgets')
->middleware([
EncryptCookies::class,
AddQueuedCookiesToResponse::class,

View File

@@ -2,7 +2,6 @@
namespace App\Providers\Filament;
use App\Extensions\Avatar\AvatarProvider;
use App\Filament\Pages\Auth\Login;
use App\Filament\Pages\Auth\EditProfile;
use Filament\Facades\Filament;
@@ -35,11 +34,9 @@ class AppPanelProvider extends PanelProvider
->favicon(config('app.favicon', '/pelican.ico'))
->topNavigation(config('panel.filament.top-navigation', true))
->maxContentWidth(config('panel.filament.display-width', 'screen-2xl'))
->defaultAvatarProvider(fn () => get_class(AvatarProvider::getProvider(config('panel.filament.avatar-provider'))))
->navigation(false)
->profile(EditProfile::class, false)
->login(Login::class)
->passwordReset()
->userMenuItems([
MenuItem::make()
->label('Admin')

View File

@@ -2,7 +2,6 @@
namespace App\Providers\Filament;
use App\Extensions\Avatar\AvatarProvider;
use App\Filament\App\Resources\ServerResource\Pages\ListServers;
use App\Filament\Pages\Auth\Login;
use App\Filament\Admin\Resources\ServerResource\Pages\EditServer;
@@ -42,13 +41,9 @@ class ServerPanelProvider extends PanelProvider
->favicon(config('app.favicon', '/pelican.ico'))
->topNavigation(config('panel.filament.top-navigation', true))
->maxContentWidth(config('panel.filament.display-width', 'screen-2xl'))
->defaultAvatarProvider(fn () => get_class(AvatarProvider::getProvider(config('panel.filament.avatar-provider'))))
->login(Login::class)
->passwordReset()
->userMenuItems([
'profile' => MenuItem::make()
->label(fn () => trans('filament-panels::pages/auth/edit-profile.label'))
->url(fn () => EditProfile::getUrl(panel: 'app')),
'profile' => MenuItem::make()->label('Profile')->url(fn () => EditProfile::getUrl(panel: 'app')),
MenuItem::make()
->label('Server List')
->icon('tabler-brand-docker')

View File

@@ -5,7 +5,6 @@ namespace App\Repositories\Daemon;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Http\Client\Response;
use App\Exceptions\Http\Server\FileSizeTooLargeException;
use App\Exceptions\Repository\FileNotEditableException;
use Illuminate\Http\Client\ConnectionException;
class DaemonFileRepository extends DaemonRepository
@@ -30,10 +29,6 @@ class DaemonFileRepository extends DaemonRepository
throw new FileSizeTooLargeException();
}
if ($response->getStatusCode() === 400) {
throw new FileNotEditableException();
}
if ($response->getStatusCode() === 404) {
throw new FileNotFoundException();
}
@@ -138,7 +133,7 @@ class DaemonFileRepository extends DaemonRepository
*
* @throws ConnectionException
*/
public function compressFiles(?string $root, array $files, ?string $name): array
public function compressFiles(?string $root, array $files): array
{
return $this->getHttpClient()
// Wait for up to 15 minutes for the archive to be completed when calling this endpoint
@@ -148,7 +143,6 @@ class DaemonFileRepository extends DaemonRepository
[
'root' => $root ?? '/',
'files' => $files,
'name' => $name ?? '',
]
)->json();
}

View File

@@ -10,6 +10,7 @@ use Illuminate\Support\Collection;
use App\Models\ActivityLog;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Request;
use App\Models\ActivityLogSubject;
use App\Models\Server;
use Filament\Facades\Filament;
use Illuminate\Database\ConnectionInterface;
@@ -235,12 +236,16 @@ class ActivityLogService
$response = $this->connection->transaction(function () {
$this->activity->save();
foreach ($this->subjects as $subject) {
$this->activity->subjects()->forceCreate([
$subjects = Collection::make($this->subjects)
->map(fn (Model $subject) => [
'activity_log_id' => $this->activity->id,
'subject_id' => $subject->getKey(),
'subject_type' => $subject->getMorphClass(),
]);
}
])
->values()
->toArray();
ActivityLogSubject::insert($subjects);
return $this->activity;
});

View File

@@ -70,7 +70,7 @@ class AssignmentService
throw new InvalidPortMappingException($port);
}
$newAllocations = [];
$insertData = [];
if (preg_match(self::PORT_RANGE_REGEX, $port, $matches)) {
$block = range($matches[1], $matches[2]);
@@ -83,7 +83,7 @@ class AssignmentService
}
foreach ($block as $unit) {
$newAllocations[] = [
$insertData[] = [
'node_id' => $node->id,
'ip' => $ip->__toString(),
'port' => (int) $unit,
@@ -96,7 +96,7 @@ class AssignmentService
throw new PortOutOfRangeException();
}
$newAllocations[] = [
$insertData[] = [
'node_id' => $node->id,
'ip' => $ip->__toString(),
'port' => (int) $port,
@@ -105,8 +105,8 @@ class AssignmentService
];
}
foreach ($newAllocations as $newAllocation) {
$allocation = Allocation::query()->create($newAllocation);
foreach ($insertData as $insert) {
$allocation = Allocation::query()->create($insert);
$ids[] = $allocation->id;
}
}

View File

@@ -85,7 +85,7 @@ class InitiateBackupService
if ($previous->count() >= $limit) {
$message = sprintf('Only %d backups may be generated within a %d second span of time.', $limit, $period);
throw new TooManyRequestsHttpException((int) now()->diffInSeconds($previous->last()->created_at->addSeconds((int) $period)), $message);
throw new TooManyRequestsHttpException((int) now()->diffInSeconds($previous->last()->created_at->addSeconds($period)), $message);
}
}

View File

@@ -155,8 +155,8 @@ class AllocationSelectionService
$query->whereIn('node_id', $nodes);
}
return $query
->groupBy('ip')
return $query->groupBy('ip')
->get()
->pluck('ip')
->toArray();
}

View File

@@ -27,7 +27,6 @@ class EggExporterService
'author' => $egg->author,
'uuid' => $egg->uuid,
'description' => $egg->description,
'tags' => $egg->tags,
'features' => $egg->features,
'docker_images' => $egg->docker_images,
'file_denylist' => Collection::make($egg->inherit_file_denylist)->filter(function ($value) {

View File

@@ -160,7 +160,6 @@ class EggImporterService
* @param array{
* name: string,
* description: string,
* tags: string[],
* features: string[],
* docker_images: string[],
* file_denylist: string[],
@@ -177,10 +176,10 @@ class EggImporterService
return $model->forceFill([
'name' => Arr::get($parsed, 'name'),
'description' => Arr::get($parsed, 'description'),
'tags' => Arr::get($parsed, 'tags', []),
'features' => Arr::get($parsed, 'features'),
'docker_images' => Arr::get($parsed, 'docker_images'),
'file_denylist' => Collection::make(Arr::get($parsed, 'file_denylist'))->filter(fn ($value) => !empty($value)),
'file_denylist' => Collection::make(Arr::get($parsed, 'file_denylist'))
->filter(fn ($value) => !empty($value)),
'update_url' => Arr::get($parsed, 'meta.update_url'),
'config_files' => Arr::get($parsed, 'config.files'),
'config_startup' => Arr::get($parsed, 'config.startup'),

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Services\Nodes;
use Ramsey\Uuid\Uuid;
use Illuminate\Support\Str;
use App\Models\Node;
class NodeCreationService
{
/**
* Create a new node on the panel.
*
* @todo remove this class
*
* @param array<string, mixed> $data
*
* @throws \App\Exceptions\Model\DataValidationException
*/
public function handle(array $data): Node
{
$data['uuid'] = Uuid::uuid4()->toString();
$data['daemon_token'] = Str::random(Node::DAEMON_TOKEN_LENGTH);
$data['daemon_token_id'] = Str::random(Node::DAEMON_TOKEN_ID_LENGTH);
return Node::query()->create($data);
}
}

View File

@@ -42,7 +42,7 @@ class ProcessScheduleService
// Check that the server is currently in a starting or running state before executing
// this schedule if this option has been set.
try {
$state = ContainerStatus::tryFrom(fluent($this->serverRepository->setServer($schedule->server)->getDetails())->get('state')) ?? ContainerStatus::Offline;
$state = fluent($this->serverRepository->setServer($schedule->server)->getDetails())->get('state') ?? ContainerStatus::Offline;
// If the server is stopping or offline just do nothing with this task.
if ($state->isOffline()) {

View File

@@ -47,6 +47,14 @@ class EnvironmentService
$variables->put($key, object_get($server, $object));
}
// Process variables set in the configuration file.
foreach (config('panel.environment_variables', []) as $key => $object) {
$variables->put(
$key,
is_callable($object) ? call_user_func($object, $server) : object_get($server, $object)
);
}
// Process dynamically included environment variables.
foreach ($this->additional as $key => $closure) {
$variables->put($key, call_user_func($closure, $server));

View File

@@ -3,6 +3,7 @@
namespace App\Services\Servers;
use App\Enums\ServerState;
use App\Models\ServerVariable;
use Illuminate\Http\Client\ConnectionException;
use Ramsey\Uuid\Uuid;
use Illuminate\Support\Arr;
@@ -198,11 +199,20 @@ class ServerCreationService
*/
private function storeEggVariables(Server $server, Collection $variables): void
{
foreach ($variables as $variable) {
$server->serverVariables()->forceCreate([
'variable_id' => $variable->id,
'variable_value' => $variable->value ?? '',
]);
$now = now();
$records = $variables->map(function ($result) use ($server, $now) {
return [
'server_id' => $server->id,
'variable_id' => $result->id,
'variable_value' => $result->value ?? '',
'created_at' => $now,
'updated_at' => $now,
];
})->toArray();
if (!empty($records)) {
ServerVariable::query()->insert($records);
}
}

View File

@@ -22,28 +22,37 @@ class TransferServerService
private NodeJWTService $nodeJWTService,
) {}
private function notify(ServerTransfer $transfer, Plain $token): void
private function notify(Server $server, Plain $token): void
{
Http::daemon($transfer->oldNode)->post("/api/servers/{$transfer->server->uuid}/transfer", [
'url' => $transfer->newNode->getConnectionAddress() . '/api/transfers',
'token' => 'Bearer ' . $token->toString(),
'server' => [
'uuid' => $transfer->server->uuid,
'start_on_completion' => false,
Http::daemon($server->node)->post('/api/transfer', [
'json' => [
'server_id' => $server->uuid,
'url' => $server->node->getConnectionAddress() . "/api/servers/$server->uuid/archive",
'token' => 'Bearer ' . $token->toString(),
'server' => [
'uuid' => $server->uuid,
'start_on_completion' => false,
],
],
]);
])->toPsrResponse();
}
/**
* Starts a transfer of a server to a new node.
*
* @param int[] $additional_allocations
* @param array{
* allocation_id: int,
* node_id: int,
* allocation_additional?: ?int[],
* } $data
*
* @throws \Throwable
*/
public function handle(Server $server, int $node_id, int $allocation_id, array $additional_allocations): bool
public function handle(Server $server, array $data): bool
{
$additional_allocations = array_map(intval(...), $additional_allocations);
$node_id = $data['node_id'];
$allocation_id = intval($data['allocation_id']);
$additional_allocations = array_map(intval(...), $data['allocation_additional'] ?? []);
// Check if the node is viable for the transfer.
$node = Node::query()
@@ -85,7 +94,7 @@ class TransferServerService
->handle($transfer->newNode, $server->uuid, 'sha256');
// Notify the source node of the pending outgoing transfer.
$this->notify($transfer, $token);
$this->notify($server, $token);
return $transfer;
});

View File

@@ -2,6 +2,9 @@
namespace App\Services\Users;
use App\Models\RecoveryToken;
use Carbon\Carbon;
use Illuminate\Support\Str;
use App\Models\User;
use PragmaRX\Google2FA\Google2FA;
use Illuminate\Database\ConnectionInterface;
@@ -46,14 +49,27 @@ class ToggleTwoFactorService
// on their account.
$tokens = [];
if ((!$toggleState && !$user->use_totp) || $toggleState) {
$user->recoveryTokens()->delete();
$inserts = [];
for ($i = 0; $i < 10; $i++) {
$token = str_random(10);
$user->recoveryTokens()->forceCreate([
$token = Str::random(10);
$inserts[] = [
'user_id' => $user->id,
'token' => password_hash($token, PASSWORD_DEFAULT),
]);
// insert() won't actually set the time on the models, so make sure we do this
// manually here.
'created_at' => Carbon::now(),
];
$tokens[] = $token;
}
// Before inserting any new records make sure all the old ones are deleted to avoid
// any issues or storing an unnecessary number of tokens in the database.
$user->recoveryTokens()->delete();
// Bulk insert the hashed tokens.
RecoveryToken::query()->insert($inserts);
}
$user->totp_authenticated_at = now();

View File

@@ -35,9 +35,9 @@ services:
ports:
- "80:80"
- "443:443"
# - "9000:9000" # enable when not using caddy to be able to reach php-fpm
# - "9000:9000" # enable when not using caddy to be abel to reach php-fpm
extra_hosts:
- "host.docker.internal:host-gateway" # shows the panel on the internal docker network as well. usually '172.17.0.1'
- "host.docker.internal:host-gateway" # shows the panel on te internal docker network as well. usually '172.17.0.1'
volumes:
- pelican-data:/pelican-data
- pelican-logs:/var/www/html/storage/logs

View File

@@ -14,9 +14,9 @@
"chillerlan/php-qrcode": "^5.0.2",
"dedoc/scramble": "^0.12.10",
"doctrine/dbal": "~3.6.0",
"filament/filament": "^3.3",
"filament/filament": "3.3.3",
"guzzlehttp/guzzle": "^7.9",
"laravel/framework": "^12.6",
"laravel/framework": "^12.2",
"laravel/helpers": "^1.7",
"laravel/sanctum": "^4.0.2",
"laravel/socialite": "^5.18",
@@ -49,14 +49,14 @@
"require-dev": {
"barryvdh/laravel-ide-helper": "^3.5",
"fakerphp/faker": "^1.23.1",
"larastan/larastan": "3.x-dev#5bd1c40edb43a727584081e74e9a1a2a201ea2ee",
"laravel/pail": "^1.2.2",
"larastan/larastan": "^3.1",
"laravel/pint": "^1.15.3",
"laravel/sail": "^1.41",
"mockery/mockery": "^1.6.11",
"nunomaduro/collision": "^8.6",
"pestphp/pest": "^3.7",
"spatie/laravel-ignition": "^2.9"
"spatie/laravel-ignition": "^2.9",
"laravel/pail": "^1.2.2"
},
"autoload": {
"files": [
@@ -80,8 +80,11 @@
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump"
],
"post-install-cmd": [
"php -r \"file_exists('.env') || copy('.env.example', '.env');\""
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi"
],
"dev": [
"Composer\\Config::disableProcessTimeout",
@@ -94,9 +97,6 @@
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true
},
"platform": {
"php": "8.2"
}
},
"minimum-stability": "stable",

611
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,10 +3,10 @@
return [
'name' => env('APP_NAME', 'Pelican'),
'logo' => env('APP_LOGO'),
'logo' => env('APP_LOGO', '/pelican.svg'),
'favicon' => env('APP_FAVICON', '/pelican.ico'),
'version' => 'canary',
'version' => '1.0.0-beta18',
'timezone' => 'UTC',

View File

@@ -89,21 +89,6 @@ return [
]) : [],
],
'pgsql' => [
'driver' => 'pgsql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'panel'),
'username' => env('DB_USERNAME', 'pelican'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => 'prefer',
],
],
/*

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