Compare commits

..

1 Commits

Author SHA1 Message Date
Lance Pioch
380d9900e5 Add user_mountable functionality for Mounts (#2077)
Resolves #2077
2026-02-06 02:42:22 -05:00
10 changed files with 153 additions and 25 deletions

View File

@@ -58,6 +58,9 @@ enum SubuserPermission: string
case SettingsDescription = 'settings.description';
case SettingsReinstall = 'settings.reinstall';
case MountRead = 'mount.read';
case MountUpdate = 'mount.update';
/** @return string[] */
public function split(): array
{
@@ -84,6 +87,7 @@ enum SubuserPermission: string
'schedule' => TablerIcon::Clock,
'settings' => TablerIcon::Settings,
'activity' => TablerIcon::Stack,
'mount' => TablerIcon::LayersLinked,
default => null,
};
}

View File

@@ -143,6 +143,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::Users,
true => TablerIcon::Users,
])
->colors([
false => 'warning',
true => 'success',
])
->inline()
->default(true),
TextInput::make('source')
->label(trans('admin/mount.source'))
->required()

View File

@@ -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);
}

View File

@@ -113,12 +113,6 @@ class CreateNode extends CreateRecord
return;
}
if (in_array(strtolower($state), Node::BANNED_FQDNS)) {
$set('dns', false);
return;
}
$ip = get_ip_from_hostname($state);
if ($ip) {
$set('dns', true);

View File

@@ -174,12 +174,6 @@ class EditNode extends EditRecord
return;
}
if (in_array(strtolower($state), Node::BANNED_FQDNS)) {
$set('dns', false);
return;
}
$ip = get_ip_from_hostname($state);
if ($ip) {
$set('dns', true);

View File

@@ -0,0 +1,114 @@
<?php
namespace App\Filament\Server\Pages;
use App\Enums\SubuserPermission;
use App\Enums\TablerIcon;
use App\Facades\Activity;
use App\Models\Mount;
use App\Models\Server;
use BackedEnum;
use Filament\Facades\Filament;
use Filament\Forms\Components\CheckboxList;
use Filament\Notifications\Notification;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
class Mounts extends ServerFormPage
{
protected static string|BackedEnum|null $navigationIcon = TablerIcon::LayersLinked;
protected static ?int $navigationSort = 11;
public static function canAccess(): bool
{
return parent::canAccess() && user()?->can(SubuserPermission::MountRead, Filament::getTenant());
}
protected function authorizeAccess(): void
{
abort_unless(user()?->can(SubuserPermission::MountRead, Filament::getTenant()), 403);
}
protected function fillForm(): void
{
$this->form->fill([
'mounts' => $this->getRecord()->mounts->pluck('id')->toArray(),
]);
}
public function form(Schema $schema): Schema
{
/** @var Server $server */
$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(trans('server/mount.description'))
->schema([
CheckboxList::make('mounts')
->hiddenLabel()
->relationship('mounts')
->options(fn () => $allowedMounts->mapWithKeys(fn (Mount $mount) => [$mount->id => $mount->name]))
->descriptions(fn () => $allowedMounts->mapWithKeys(fn (Mount $mount) => [$mount->id => "$mount->source -> $mount->target"]))
->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
{
/** @var Server $server */
$server = $this->getRecord();
abort_unless(user()?->can(SubuserPermission::MountUpdate, $server), 403);
try {
$this->form->getState();
$this->form->saveRelationships();
Activity::event('server:mount.update')
->log();
Notification::make()
->title(trans('server/mount.notification_updated'))
->success()
->send();
} catch (\Exception $exception) {
Notification::make()
->title(trans('server/mount.notification_failed'))
->body($exception->getMessage())
->danger()
->send();
}
}
public function getTitle(): string
{
return trans('server/mount.title');
}
public static function getNavigationLabel(): string
{
return trans('server/mount.title');
}
}

View File

@@ -18,7 +18,6 @@ use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Symfony\Component\Yaml\Yaml;
/**
@@ -73,8 +72,6 @@ class Node extends Model implements Validatable
public const DAEMON_TOKEN_LENGTH = 64;
public const BANNED_FQDNS = ['0.0.0.0', '127.0.0.1', 'localhost'];
/**
* The attributes excluded from the model's JSON form.
*/
@@ -98,7 +95,7 @@ class Node extends Model implements Validatable
'name' => ['required', 'string', 'min:1', 'max:100'],
'description' => ['string', 'nullable'],
'public' => ['boolean'],
'fqdn' => ['required', 'string'],
'fqdn' => ['required', 'string', 'notIn:0.0.0.0,127.0.0.1,localhost'],
'scheme' => ['required', 'string', 'in:http,https'],
'behind_proxy' => ['boolean'],
'memory' => ['required', 'numeric', 'min:0'],
@@ -117,14 +114,6 @@ class Node extends Model implements Validatable
'tags' => ['array'],
];
public static function getRules(): array
{
$rules = static::$validationRules;
$rules['fqdn'][] = Rule::notIn(static::BANNED_FQDNS);
return $rules;
}
/**
* Default values for specific columns that are generally not changed on base installs.
*/

View File

@@ -17,9 +17,13 @@ return [
'no_mounts' => 'No Mounts',
'eggs' => 'Eggs',
'nodes' => 'Nodes',
'user_mountable' => 'User Mountable?',
'user_mountable_help' => 'Allow users 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',

9
lang/en/server/mount.php Normal file
View File

@@ -0,0 +1,9 @@
<?php
return [
'title' => '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_failed' => 'Failed to update mounts.',
];

View File

@@ -69,5 +69,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.',
],
];