diff --git a/app/Enums/SubuserPermission.php b/app/Enums/SubuserPermission.php index f0d7a343d..b99ce63b2 100644 --- a/app/Enums/SubuserPermission.php +++ b/app/Enums/SubuserPermission.php @@ -50,6 +50,9 @@ enum SubuserPermission: string case ActivityRead = 'activity.read'; + case MountRead = 'mount.read'; + case MountUpdate = 'mount.update'; + case StartupRead = 'startup.read'; case StartupUpdate = 'startup.update'; case StartupDockerImage = 'startup.docker-image'; @@ -84,6 +87,7 @@ enum SubuserPermission: string 'schedule' => TablerIcon::Clock, 'settings' => TablerIcon::Settings, 'activity' => TablerIcon::Stack, + 'mount' => TablerIcon::LayersLinked, default => null, }; } diff --git a/app/Filament/Admin/Resources/Mounts/MountResource.php b/app/Filament/Admin/Resources/Mounts/MountResource.php index 462ae595c..75e183614 100644 --- a/app/Filament/Admin/Resources/Mounts/MountResource.php +++ b/app/Filament/Admin/Resources/Mounts/MountResource.php @@ -95,6 +95,12 @@ class MountResource extends Resource ->icon(fn ($state) => $state ? TablerIcon::WritingOff : TablerIcon::Writing) ->color(fn ($state) => $state ? 'success' : 'warning') ->formatStateUsing(fn ($state) => $state ? trans('admin/mount.toggles.read_only') : trans('admin/mount.toggles.writable')), + TextColumn::make('user_mountable') + ->label(trans('admin/mount.table.user_mountable')) + ->badge() + ->icon(fn ($state) => $state ? TablerIcon::User : TablerIcon::UserOff) + ->color(fn ($state) => $state ? 'success' : 'warning') + ->formatStateUsing(fn ($state) => $state ? trans('admin/mount.toggles.user_mountable') : trans('admin/mount.toggles.not_user_mountable')), ]) ->recordActions([ ViewAction::make() @@ -124,7 +130,8 @@ class MountResource extends Resource ->label(trans('admin/mount.name')) ->required() ->helperText(trans('admin/mount.name_help')) - ->maxLength(64), + ->maxLength(64) + ->columnSpanFull(), ToggleButtons::make('read_only') ->label(trans('admin/mount.read_only')) ->helperText(trans('admin/mount.read_only_help')) @@ -143,6 +150,24 @@ class MountResource extends Resource ]) ->inline() ->default(false), + ToggleButtons::make('user_mountable') + ->label(trans('admin/mount.user_mountable')) + ->helperText(trans('admin/mount.user_mountable_help')) + ->stateCast(new BooleanStateCast(false, true)) + ->options([ + false => trans('admin/mount.toggles.not_user_mountable'), + true => trans('admin/mount.toggles.user_mountable'), + ]) + ->icons([ + false => TablerIcon::UserOff, + true => TablerIcon::User, + ]) + ->colors([ + false => 'warning', + true => 'success', + ]) + ->inline() + ->default(true), TextInput::make('source') ->label(trans('admin/mount.source')) ->required() diff --git a/app/Filament/Admin/Resources/Mounts/Pages/CreateMount.php b/app/Filament/Admin/Resources/Mounts/Pages/CreateMount.php index e35eb5ec1..c6dd95c9c 100644 --- a/app/Filament/Admin/Resources/Mounts/Pages/CreateMount.php +++ b/app/Filament/Admin/Resources/Mounts/Pages/CreateMount.php @@ -42,7 +42,6 @@ class CreateMount extends CreateRecord protected function handleRecordCreation(array $data): Model { $data['uuid'] ??= Str::uuid()->toString(); - $data['user_mountable'] = 1; return parent::handleRecordCreation($data); } diff --git a/app/Filament/Server/Pages/Mounts.php b/app/Filament/Server/Pages/Mounts.php new file mode 100644 index 000000000..c4267e686 --- /dev/null +++ b/app/Filament/Server/Pages/Mounts.php @@ -0,0 +1,114 @@ +can(SubuserPermission::MountRead, Filament::getTenant()); + } + + protected function authorizeAccess(): void + { + abort_unless(user()?->can(SubuserPermission::MountRead, Filament::getTenant()), 403); + } + + protected function fillForm(): void + { + $this->form->fill([ + 'mounts' => $this->getRecord()->mounts->pluck('id')->toArray(), + ]); + } + + public function form(Schema $schema): Schema + { + $server = $this->getRecord(); + + $allowedMounts = Mount::query() + ->where('user_mountable', true) + ->where(function ($query) use ($server) { + $query->whereDoesntHave('nodes') + ->orWhereHas('nodes', fn ($q) => $q->where('nodes.id', $server->node_id)); + }) + ->where(function ($query) use ($server) { + $query->whereDoesntHave('eggs') + ->orWhereHas('eggs', fn ($q) => $q->where('eggs.id', $server->egg_id)); + }) + ->get(); + + return parent::form($schema) + ->components([ + Section::make([ + CheckboxList::make('mounts') + ->label(trans('server/mount.description')) + ->relationship('mounts') + ->options(fn () => $allowedMounts->mapWithKeys(fn (Mount $mount) => [$mount->id => $mount->name])) + ->descriptions(fn () => $allowedMounts->mapWithKeys(fn (Mount $mount) => [$mount->id => new HtmlString(str("$mount->source -> $mount->target")->stripTags() . ($mount->description ? '
' . str($mount->description)->stripTags() : ''))])) + ->helperText(fn () => $allowedMounts->isEmpty() ? trans('server/mount.no_mounts') : null) + ->disabled(fn (Server $server) => !user()?->can(SubuserPermission::MountUpdate, $server)) + ->bulkToggleable() + ->live() + ->afterStateUpdated(function ($state) { + $this->save(); + }) + ->columnSpanFull(), + ]), + ]); + } + + public function save(): void + { + abort_unless(user()?->can(SubuserPermission::MountUpdate, $this->getRecord()), 403); + + try { + $this->form->getState(); + $this->form->saveRelationships(); + + Activity::event('server:mount.update') + ->log(); + + Notification::make() + ->title(trans('server/mount.notification_updated')) + ->body(trans('server/mount.notification_updated_body')) + ->success() + ->send(); + } catch (Exception $exception) { + report($exception); + + Notification::make() + ->title(trans('server/mount.notification_failed')) + ->body($exception->getMessage()) + ->danger() + ->send(); + } + } + + public function getTitle(): string + { + return trans('server/mount.title'); + } + + public static function getNavigationLabel(): string + { + return trans('server/mount.title'); + } +} diff --git a/app/Filament/Server/Pages/Settings.php b/app/Filament/Server/Pages/Settings.php index 5cab330e5..5e8394a5b 100644 --- a/app/Filament/Server/Pages/Settings.php +++ b/app/Filament/Server/Pages/Settings.php @@ -27,7 +27,7 @@ class Settings extends ServerFormPage { protected static string|BackedEnum|null $navigationIcon = TablerIcon::Settings; - protected static ?int $navigationSort = 10; + protected static ?int $navigationSort = 11; /** * @throws Exception diff --git a/app/Filament/Server/Pages/Startup.php b/app/Filament/Server/Pages/Startup.php index 3bd714f51..8de8f688c 100644 --- a/app/Filament/Server/Pages/Startup.php +++ b/app/Filament/Server/Pages/Startup.php @@ -28,7 +28,7 @@ class Startup extends ServerFormPage { protected static string|BackedEnum|null $navigationIcon = TablerIcon::PlayerPlay; - protected static ?int $navigationSort = 9; + protected static ?int $navigationSort = 10; /** * @throws Exception diff --git a/lang/en/activity.php b/lang/en/activity.php index 464854f00..4667ff5f8 100644 --- a/lang/en/activity.php +++ b/lang/en/activity.php @@ -120,6 +120,9 @@ return [ 'update' => 'Updated the subuser permissions for :email', 'delete' => 'Removed :email as a subuser', ], + 'mount' => [ + 'update' => 'Updated the mounts for the server', + ], 'crashed' => 'Server crashed', ], ]; diff --git a/lang/en/admin/mount.php b/lang/en/admin/mount.php index 1f0c6f949..c0e0f826f 100644 --- a/lang/en/admin/mount.php +++ b/lang/en/admin/mount.php @@ -17,14 +17,19 @@ return [ 'no_mounts' => 'No Mounts', 'eggs' => 'Eggs', 'nodes' => 'Nodes', + 'user_mountable' => 'User Mountable?', + 'user_mountable_help' => 'Should users be able to toggle this mount on or off for their servers?', 'toggles' => [ 'writable' => 'Writable', 'read_only' => 'Read Only', + 'user_mountable' => 'User Mountable', + 'not_user_mountable' => 'Admin Only', ], 'table' => [ 'name' => 'Name', 'all_eggs' => 'All Eggs', 'all_nodes' => 'All Nodes', 'read_only' => 'Read Only', + 'user_mountable' => 'User Mountable', ], ]; diff --git a/lang/en/server/mount.php b/lang/en/server/mount.php new file mode 100644 index 000000000..42a191561 --- /dev/null +++ b/lang/en/server/mount.php @@ -0,0 +1,10 @@ + 'Mounts', + 'description' => 'Manage the mounts attached to your server:', + 'no_mounts' => 'There are no user-mountable mounts available for this server.', + 'notification_updated' => 'Mounts updated successfully', + 'notification_updated_body' => 'Restart your server to apply the new mounts', + 'notification_failed' => 'Failed to update mounts', +]; diff --git a/lang/en/server/user.php b/lang/en/server/user.php index a1b2d3e3d..631bb17fd 100644 --- a/lang/en/server/user.php +++ b/lang/en/server/user.php @@ -100,5 +100,8 @@ return [ 'backup_delete' => 'Allows a user to remove backups from the system.', 'backup_download' => 'Allows a user to download a backup for the server. Danger: this allows a user to access all files for the server in the backup.', 'backup_restore' => 'Allows a user to restore a backup for the server. Danger: this allows the user to delete all of the server files in the process.', + 'mount_desc' => 'Permissions that control a user\'s ability to manage mounts for this server.', + 'mount_read' => 'Allows a user to view the mounts page and see available mounts.', + 'mount_update' => 'Allows a user to toggle mounts on or off for the server.', ], ];