Admin Roles (#502)
* add spatie/permissions * add policies * add role resource * add root admin role handling * replace some "root_admin" with function * add model specific permissions * make permission selection nicer * fix user creation * fix tests * add back subuser checks in server policy * add custom model for role * assign new users to role if root_admin is set * add api for roles * fix phpstan * add permissions for settings page * remove "restore" and "forceDelete" permissions * add user count to list * prevent deletion if role has users * update user list * fix server policy * remove old `root_admin` column * small refactor * fix tests * forgot can checks here * forgot use * disable editing own roles & disable assigning root admin * don't allow to rename root admin role * remove php bombing exception handler * fix role assignment when creating a user * fix disableOptionWhen * fix missing `root_admin` attribute on react frontend * add permission check for bulk delete * rename viewAny to viewList * improve canAccessPanel check * fix admin not displaying for non-root admins * make sure non root admins can't edit root admins * fix import * fix settings page permission check * fix server permissions for non-subusers * fix settings page permission check v2 * small cleanup * cleanup config file * move consts from resouce into enum & model * Update database/migrations/2024_08_01_114538_remove_root_admin_column.php Co-authored-by: Lance Pioch <lancepioch@gmail.com> * fix config * fix phpstan * fix phpstan 2.0 --------- Co-authored-by: Lance Pioch <lancepioch@gmail.com>
This commit is contained in:
parent
68a0cbbf10
commit
fc643f57f9
@ -52,7 +52,7 @@ class MakeUserCommand extends Command
|
|||||||
['UUID', $user->uuid],
|
['UUID', $user->uuid],
|
||||||
['Email', $user->email],
|
['Email', $user->email],
|
||||||
['Username', $user->username],
|
['Username', $user->username],
|
||||||
['Admin', $user->root_admin ? 'Yes' : 'No'],
|
['Admin', $user->isRootAdmin() ? 'Yes' : 'No'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
16
app/Enums/RolePermissionModels.php
Normal file
16
app/Enums/RolePermissionModels.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum RolePermissionModels: string
|
||||||
|
{
|
||||||
|
case ApiKey = 'apikey';
|
||||||
|
case DatabaseHost = 'databasehost';
|
||||||
|
case Database = 'database';
|
||||||
|
case Egg = 'egg';
|
||||||
|
case Mount = 'mount';
|
||||||
|
case Node = 'node';
|
||||||
|
case Role = 'role';
|
||||||
|
case Server = 'server';
|
||||||
|
case User = 'user';
|
||||||
|
}
|
12
app/Enums/RolePermissionPrefixes.php
Normal file
12
app/Enums/RolePermissionPrefixes.php
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum RolePermissionPrefixes: string
|
||||||
|
{
|
||||||
|
case ViewAny = 'viewList';
|
||||||
|
case View = 'view';
|
||||||
|
case Create = 'create';
|
||||||
|
case Update = 'update';
|
||||||
|
case Delete = 'delete';
|
||||||
|
}
|
@ -49,12 +49,18 @@ class Settings extends Page implements HasForms
|
|||||||
$this->form->fill();
|
$this->form->fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function canAccess(): bool
|
||||||
|
{
|
||||||
|
return auth()->user()->can('view settings');
|
||||||
|
}
|
||||||
|
|
||||||
protected function getFormSchema(): array
|
protected function getFormSchema(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Tabs::make('Tabs')
|
Tabs::make('Tabs')
|
||||||
->columns()
|
->columns()
|
||||||
->persistTabInQueryString()
|
->persistTabInQueryString()
|
||||||
|
->disabled(fn () => !auth()->user()->can('update settings'))
|
||||||
->tabs([
|
->tabs([
|
||||||
Tab::make('general')
|
Tab::make('general')
|
||||||
->label('General')
|
->label('General')
|
||||||
@ -147,10 +153,12 @@ class Settings extends Page implements HasForms
|
|||||||
->color('danger')
|
->color('danger')
|
||||||
->icon('tabler-trash')
|
->icon('tabler-trash')
|
||||||
->requiresConfirmation()
|
->requiresConfirmation()
|
||||||
|
->authorize(fn () => auth()->user()->can('update settings'))
|
||||||
->action(fn (Set $set) => $set('TRUSTED_PROXIES', [])),
|
->action(fn (Set $set) => $set('TRUSTED_PROXIES', [])),
|
||||||
FormAction::make('cloudflare')
|
FormAction::make('cloudflare')
|
||||||
->label('Set to Cloudflare IPs')
|
->label('Set to Cloudflare IPs')
|
||||||
->icon('tabler-brand-cloudflare')
|
->icon('tabler-brand-cloudflare')
|
||||||
|
->authorize(fn () => auth()->user()->can('update settings'))
|
||||||
->action(fn (Set $set) => $set('TRUSTED_PROXIES', [
|
->action(fn (Set $set) => $set('TRUSTED_PROXIES', [
|
||||||
'173.245.48.0/20',
|
'173.245.48.0/20',
|
||||||
'103.21.244.0/22',
|
'103.21.244.0/22',
|
||||||
@ -226,6 +234,7 @@ class Settings extends Page implements HasForms
|
|||||||
->label('Send Test Mail')
|
->label('Send Test Mail')
|
||||||
->icon('tabler-send')
|
->icon('tabler-send')
|
||||||
->hidden(fn (Get $get) => $get('MAIL_MAILER') === 'log')
|
->hidden(fn (Get $get) => $get('MAIL_MAILER') === 'log')
|
||||||
|
->authorize(fn () => auth()->user()->can('update settings'))
|
||||||
->action(function () {
|
->action(function () {
|
||||||
try {
|
try {
|
||||||
MailNotification::route('mail', auth()->user()->email)
|
MailNotification::route('mail', auth()->user()->email)
|
||||||
@ -561,12 +570,9 @@ class Settings extends Page implements HasForms
|
|||||||
return [
|
return [
|
||||||
Action::make('save')
|
Action::make('save')
|
||||||
->action('save')
|
->action('save')
|
||||||
|
->authorize(fn () => auth()->user()->can('update settings'))
|
||||||
->keyBindings(['mod+s']),
|
->keyBindings(['mod+s']),
|
||||||
];
|
];
|
||||||
|
|
||||||
}
|
}
|
||||||
protected function getFormActions(): array
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,8 @@ class ListDatabaseHosts extends ListRecords
|
|||||||
])
|
])
|
||||||
->bulkActions([
|
->bulkActions([
|
||||||
BulkActionGroup::make([
|
BulkActionGroup::make([
|
||||||
DeleteBulkAction::make(),
|
DeleteBulkAction::make()
|
||||||
|
->authorize(fn () => auth()->user()->can('delete databasehost')),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,10 @@ namespace App\Filament\Resources\DatabaseResource\Pages;
|
|||||||
|
|
||||||
use App\Filament\Resources\DatabaseResource;
|
use App\Filament\Resources\DatabaseResource;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Tables\Actions\EditAction;
|
|
||||||
use Filament\Resources\Pages\ListRecords;
|
use Filament\Resources\Pages\ListRecords;
|
||||||
use Filament\Tables\Actions\BulkActionGroup;
|
use Filament\Tables\Actions\BulkActionGroup;
|
||||||
use Filament\Tables\Actions\DeleteBulkAction;
|
use Filament\Tables\Actions\DeleteBulkAction;
|
||||||
|
use Filament\Tables\Actions\EditAction;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
@ -48,7 +48,8 @@ class ListDatabases extends ListRecords
|
|||||||
])
|
])
|
||||||
->bulkActions([
|
->bulkActions([
|
||||||
BulkActionGroup::make([
|
BulkActionGroup::make([
|
||||||
DeleteBulkAction::make(),
|
DeleteBulkAction::make()
|
||||||
|
->authorize(fn () => auth()->user()->can('delete database')),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,15 @@
|
|||||||
|
|
||||||
namespace App\Filament\Resources\EggResource\Pages;
|
namespace App\Filament\Resources\EggResource\Pages;
|
||||||
|
|
||||||
|
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
|
||||||
use App\Filament\Resources\EggResource;
|
use App\Filament\Resources\EggResource;
|
||||||
use App\Filament\Resources\EggResource\RelationManagers\ServersRelationManager;
|
use App\Filament\Resources\EggResource\RelationManagers\ServersRelationManager;
|
||||||
use App\Models\Egg;
|
use App\Models\Egg;
|
||||||
|
use App\Services\Eggs\Sharing\EggExporterService;
|
||||||
use App\Services\Eggs\Sharing\EggImporterService;
|
use App\Services\Eggs\Sharing\EggImporterService;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
|
use Filament\Forms;
|
||||||
use Filament\Forms\Components\Checkbox;
|
use Filament\Forms\Components\Checkbox;
|
||||||
use Filament\Forms\Components\Fieldset;
|
use Filament\Forms\Components\Fieldset;
|
||||||
use Filament\Forms\Components\FileUpload;
|
use Filament\Forms\Components\FileUpload;
|
||||||
@ -22,12 +25,9 @@ use Filament\Forms\Components\TagsInput;
|
|||||||
use Filament\Forms\Components\Textarea;
|
use Filament\Forms\Components\Textarea;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Forms\Components\Toggle;
|
use Filament\Forms\Components\Toggle;
|
||||||
|
use Filament\Forms\Form;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Resources\Pages\EditRecord;
|
use Filament\Resources\Pages\EditRecord;
|
||||||
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
|
|
||||||
use App\Services\Eggs\Sharing\EggExporterService;
|
|
||||||
use Filament\Forms;
|
|
||||||
use Filament\Forms\Form;
|
|
||||||
|
|
||||||
class EditEgg extends EditRecord
|
class EditEgg extends EditRecord
|
||||||
{
|
{
|
||||||
@ -245,14 +245,13 @@ class EditEgg extends EditRecord
|
|||||||
Actions\DeleteAction::make('deleteEgg')
|
Actions\DeleteAction::make('deleteEgg')
|
||||||
->disabled(fn (Egg $egg): bool => $egg->servers()->count() > 0)
|
->disabled(fn (Egg $egg): bool => $egg->servers()->count() > 0)
|
||||||
->label(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? 'Delete' : 'In Use'),
|
->label(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? 'Delete' : 'In Use'),
|
||||||
|
|
||||||
Actions\Action::make('exportEgg')
|
Actions\Action::make('exportEgg')
|
||||||
->label('Export')
|
->label('Export')
|
||||||
->color('primary')
|
->color('primary')
|
||||||
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
|
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
|
||||||
echo $service->handle($egg->id);
|
echo $service->handle($egg->id);
|
||||||
}, 'egg-' . $egg->getKebabName() . '.json')),
|
}, 'egg-' . $egg->getKebabName() . '.json'))
|
||||||
|
->authorize(fn () => auth()->user()->can('export egg')),
|
||||||
Actions\Action::make('importEgg')
|
Actions\Action::make('importEgg')
|
||||||
->label('Import')
|
->label('Import')
|
||||||
->form([
|
->form([
|
||||||
@ -321,8 +320,8 @@ class EditEgg extends EditRecord
|
|||||||
->title('Import Success')
|
->title('Import Success')
|
||||||
->success()
|
->success()
|
||||||
->send();
|
->send();
|
||||||
}),
|
})
|
||||||
|
->authorize(fn () => auth()->user()->can('import egg')),
|
||||||
$this->getSaveFormAction()->formId('form'),
|
$this->getSaveFormAction()->formId('form'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -14,13 +14,13 @@ use Filament\Forms\Components\Tabs\Tab;
|
|||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Resources\Pages\ListRecords;
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
use Filament\Tables;
|
||||||
use Filament\Tables\Actions\BulkActionGroup;
|
use Filament\Tables\Actions\BulkActionGroup;
|
||||||
use Filament\Tables\Actions\DeleteBulkAction;
|
use Filament\Tables\Actions\DeleteBulkAction;
|
||||||
use Filament\Tables\Actions\EditAction;
|
use Filament\Tables\Actions\EditAction;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||||
use Filament\Tables;
|
|
||||||
|
|
||||||
class ListEggs extends ListRecords
|
class ListEggs extends ListRecords
|
||||||
{
|
{
|
||||||
@ -55,11 +55,13 @@ class ListEggs extends ListRecords
|
|||||||
->color('primary')
|
->color('primary')
|
||||||
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
|
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
|
||||||
echo $service->handle($egg->id);
|
echo $service->handle($egg->id);
|
||||||
}, 'egg-' . $egg->getKebabName() . '.json')),
|
}, 'egg-' . $egg->getKebabName() . '.json'))
|
||||||
|
->authorize(fn () => auth()->user()->can('export egg')),
|
||||||
])
|
])
|
||||||
->bulkActions([
|
->bulkActions([
|
||||||
BulkActionGroup::make([
|
BulkActionGroup::make([
|
||||||
DeleteBulkAction::make(),
|
DeleteBulkAction::make()
|
||||||
|
->authorize(fn () => auth()->user()->can('delete egg')),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -138,7 +140,8 @@ class ListEggs extends ListRecords
|
|||||||
->title('Import Success')
|
->title('Import Success')
|
||||||
->success()
|
->success()
|
||||||
->send();
|
->send();
|
||||||
}),
|
})
|
||||||
|
->authorize(fn () => auth()->user()->can('import egg')),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,8 @@ class ListMounts extends ListRecords
|
|||||||
])
|
])
|
||||||
->bulkActions([
|
->bulkActions([
|
||||||
BulkActionGroup::make([
|
BulkActionGroup::make([
|
||||||
DeleteBulkAction::make(),
|
DeleteBulkAction::make()
|
||||||
|
->authorize(fn () => auth()->user()->can('delete mount')),
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
->emptyStateIcon('tabler-layers-linked')
|
->emptyStateIcon('tabler-layers-linked')
|
||||||
|
@ -84,7 +84,8 @@ class ListNodes extends ListRecords
|
|||||||
])
|
])
|
||||||
->bulkActions([
|
->bulkActions([
|
||||||
BulkActionGroup::make([
|
BulkActionGroup::make([
|
||||||
DeleteBulkAction::make(),
|
DeleteBulkAction::make()
|
||||||
|
->authorize(fn () => auth()->user()->can('delete node')),
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
->emptyStateIcon('tabler-server-2')
|
->emptyStateIcon('tabler-server-2')
|
||||||
|
@ -7,12 +7,12 @@ use App\Models\Node;
|
|||||||
use App\Services\Allocations\AssignmentService;
|
use App\Services\Allocations\AssignmentService;
|
||||||
use Filament\Forms\Components\TagsInput;
|
use Filament\Forms\Components\TagsInput;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Forms\Set;
|
|
||||||
use Filament\Tables\Actions\BulkActionGroup;
|
|
||||||
use Filament\Tables\Actions\DeleteBulkAction;
|
|
||||||
use Filament\Forms\Form;
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Forms\Set;
|
||||||
use Filament\Resources\RelationManagers\RelationManager;
|
use Filament\Resources\RelationManagers\RelationManager;
|
||||||
use Filament\Tables;
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Actions\BulkActionGroup;
|
||||||
|
use Filament\Tables\Actions\DeleteBulkAction;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Columns\TextInputColumn;
|
use Filament\Tables\Columns\TextInputColumn;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
@ -152,7 +152,8 @@ class AllocationsRelationManager extends RelationManager
|
|||||||
])
|
])
|
||||||
->bulkActions([
|
->bulkActions([
|
||||||
BulkActionGroup::make([
|
BulkActionGroup::make([
|
||||||
DeleteBulkAction::make(),
|
DeleteBulkAction::make()
|
||||||
|
->authorize(fn () => auth()->user()->can('delete allocation')),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
146
app/Filament/Resources/RoleResource.php
Normal file
146
app/Filament/Resources/RoleResource.php
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
|
use App\Enums\RolePermissionModels;
|
||||||
|
use App\Enums\RolePermissionPrefixes;
|
||||||
|
use App\Filament\Resources\RoleResource\Pages;
|
||||||
|
use App\Models\Role;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
|
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\Placeholder;
|
||||||
|
use Filament\Forms\Components\Section;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Forms\Get;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class RoleResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = Role::class;
|
||||||
|
|
||||||
|
protected static ?string $navigationIcon = 'tabler-users-group';
|
||||||
|
|
||||||
|
protected static ?string $recordTitleAttribute = 'name';
|
||||||
|
|
||||||
|
public static function getNavigationBadge(): ?string
|
||||||
|
{
|
||||||
|
return static::getModel()::count() ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function form(Form $form): Form
|
||||||
|
{
|
||||||
|
$permissions = [];
|
||||||
|
|
||||||
|
foreach (RolePermissionModels::cases() as $model) {
|
||||||
|
$options = [];
|
||||||
|
|
||||||
|
foreach (RolePermissionPrefixes::cases() as $prefix) {
|
||||||
|
$options[$prefix->value . ' ' . strtolower($model->value)] = Str::headline($prefix->value);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
->columns(1)
|
||||||
|
->schema([
|
||||||
|
TextInput::make('name')
|
||||||
|
->label('Role Name')
|
||||||
|
->required()
|
||||||
|
->disabled(fn (Get $get) => $get('name') === Role::ROOT_ADMIN),
|
||||||
|
TextInput::make('guard_name')
|
||||||
|
->label('Guard Name')
|
||||||
|
->default(Filament::getCurrentPanel()?->getAuthGuard() ?? '')
|
||||||
|
->nullable()
|
||||||
|
->hidden(),
|
||||||
|
Fieldset::make('Permissions')
|
||||||
|
->columns(3)
|
||||||
|
->schema($permissions)
|
||||||
|
->hidden(fn (Get $get) => $get('name') === Role::ROOT_ADMIN),
|
||||||
|
Placeholder::make('permissions')
|
||||||
|
->content('The Root Admin has all permissions.')
|
||||||
|
->visible(fn (Get $get) => $get('name') === Role::ROOT_ADMIN),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function makeSection(string $model, array $options): Section
|
||||||
|
{
|
||||||
|
$icon = null;
|
||||||
|
|
||||||
|
if (class_exists('\App\Filament\Resources\\' . $model . 'Resource')) {
|
||||||
|
$icon = ('\App\Filament\Resources\\' . $model . 'Resource')::getNavigationIcon();
|
||||||
|
} elseif (class_exists('\App\Filament\Pages\\' . $model)) {
|
||||||
|
$icon = ('\App\Filament\Pages\\' . $model)::getNavigationIcon();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Section::make(Str::headline(Str::plural($model)))
|
||||||
|
->columnSpan(1)
|
||||||
|
->collapsible()
|
||||||
|
->collapsed()
|
||||||
|
->icon($icon)
|
||||||
|
->headerActions([
|
||||||
|
Action::make('count')
|
||||||
|
->label(fn (Get $get) => count($get(strtolower($model) . '_list')))
|
||||||
|
->badge(),
|
||||||
|
])
|
||||||
|
->schema([
|
||||||
|
CheckboxList::make(strtolower($model) . '_list')
|
||||||
|
->label('')
|
||||||
|
->options($options)
|
||||||
|
->columns()
|
||||||
|
->gridDirection('row')
|
||||||
|
->bulkToggleable()
|
||||||
|
->live()
|
||||||
|
->afterStateHydrated(
|
||||||
|
function (Component $component, string $operation, ?Role $record) use ($options) {
|
||||||
|
if (in_array($operation, ['edit', 'view'])) {
|
||||||
|
|
||||||
|
if (blank($record)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($component->isVisible()) {
|
||||||
|
$component->state(
|
||||||
|
collect($options)
|
||||||
|
->filter(fn ($value, $key) => $record->checkPermissionTo($key))
|
||||||
|
->keys()
|
||||||
|
->toArray()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
->dehydrated(fn ($state) => !blank($state)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\ListRoles::route('/'),
|
||||||
|
'create' => Pages\CreateRole::route('/create'),
|
||||||
|
'edit' => Pages\EditRole::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
48
app/Filament/Resources/RoleResource/Pages/CreateRole.php
Normal file
48
app/Filament/Resources/RoleResource/Pages/CreateRole.php
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\RoleResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\RoleResource;
|
||||||
|
use App\Models\Role;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Spatie\Permission\Models\Permission;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property Role $record
|
||||||
|
*/
|
||||||
|
class CreateRole extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = RoleResource::class;
|
||||||
|
|
||||||
|
protected static bool $canCreateAnother = false;
|
||||||
|
|
||||||
|
public Collection $permissions;
|
||||||
|
|
||||||
|
protected function mutateFormDataBeforeCreate(array $data): array
|
||||||
|
{
|
||||||
|
$this->permissions = collect($data)
|
||||||
|
->filter(function ($permission, $key) {
|
||||||
|
return !in_array($key, ['name', 'guard_name']);
|
||||||
|
})
|
||||||
|
->values()
|
||||||
|
->flatten()
|
||||||
|
->unique();
|
||||||
|
|
||||||
|
return Arr::only($data, ['name', 'guard_name']);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function afterCreate(): void
|
||||||
|
{
|
||||||
|
$permissionModels = collect();
|
||||||
|
$this->permissions->each(function ($permission) use ($permissionModels) {
|
||||||
|
$permissionModels->push(Permission::firstOrCreate([
|
||||||
|
'name' => $permission,
|
||||||
|
'guard_name' => $this->data['guard_name'],
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->record->syncPermissions($permissionModels);
|
||||||
|
}
|
||||||
|
}
|
56
app/Filament/Resources/RoleResource/Pages/EditRole.php
Normal file
56
app/Filament/Resources/RoleResource/Pages/EditRole.php
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\RoleResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\RoleResource;
|
||||||
|
use App\Models\Role;
|
||||||
|
use Filament\Actions\DeleteAction;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Spatie\Permission\Models\Permission;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property Role $record
|
||||||
|
*/
|
||||||
|
class EditRole extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = RoleResource::class;
|
||||||
|
|
||||||
|
public Collection $permissions;
|
||||||
|
|
||||||
|
protected function mutateFormDataBeforeSave(array $data): array
|
||||||
|
{
|
||||||
|
$this->permissions = collect($data)
|
||||||
|
->filter(function ($permission, $key) {
|
||||||
|
return !in_array($key, ['name', 'guard_name']);
|
||||||
|
})
|
||||||
|
->values()
|
||||||
|
->flatten()
|
||||||
|
->unique();
|
||||||
|
|
||||||
|
return Arr::only($data, ['name', 'guard_name']);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function afterSave(): void
|
||||||
|
{
|
||||||
|
$permissionModels = collect();
|
||||||
|
$this->permissions->each(function ($permission) use ($permissionModels) {
|
||||||
|
$permissionModels->push(Permission::firstOrCreate([
|
||||||
|
'name' => $permission,
|
||||||
|
'guard_name' => $this->data['guard_name'],
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->record->syncPermissions($permissionModels);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
DeleteAction::make()
|
||||||
|
->disabled(fn (Role $role) => $role->isRootAdmin() || $role->users_count >= 1)
|
||||||
|
->label(fn (Role $role) => $role->isRootAdmin() ? 'Can\'t delete Root Admin' : ($role->users_count >= 1 ? 'In Use' : 'Delete')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
68
app/Filament/Resources/RoleResource/Pages/ListRoles.php
Normal file
68
app/Filament/Resources/RoleResource/Pages/ListRoles.php
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\RoleResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\RoleResource;
|
||||||
|
use App\Models\Role;
|
||||||
|
use Filament\Actions\CreateAction;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
use Filament\Tables\Actions\BulkActionGroup;
|
||||||
|
use Filament\Tables\Actions\CreateAction as CreateActionTable;
|
||||||
|
use Filament\Tables\Actions\DeleteBulkAction;
|
||||||
|
use Filament\Tables\Actions\EditAction;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
|
class ListRoles extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = RoleResource::class;
|
||||||
|
|
||||||
|
public function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
TextColumn::make('name')
|
||||||
|
->sortable()
|
||||||
|
->searchable(),
|
||||||
|
TextColumn::make('guard_name')
|
||||||
|
->hidden()
|
||||||
|
->sortable()
|
||||||
|
->searchable(),
|
||||||
|
TextColumn::make('permissions_count')
|
||||||
|
->label('Permissions')
|
||||||
|
->badge()
|
||||||
|
->counts('permissions')
|
||||||
|
->formatStateUsing(fn (Role $role, $state) => $role->isRootAdmin() ? 'All' : $state),
|
||||||
|
TextColumn::make('users_count')
|
||||||
|
->label('Users')
|
||||||
|
->counts('users')
|
||||||
|
->icon('tabler-users'),
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
EditAction::make(),
|
||||||
|
])
|
||||||
|
->checkIfRecordIsSelectableUsing(fn (Role $role) => !$role->isRootAdmin() && $role->users_count <= 0)
|
||||||
|
->bulkActions([
|
||||||
|
BulkActionGroup::make([
|
||||||
|
DeleteBulkAction::make()
|
||||||
|
->authorize(fn () => auth()->user()->can('delete role')),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
->emptyStateIcon('tabler-users-group')
|
||||||
|
->emptyStateDescription('')
|
||||||
|
->emptyStateHeading('No Roles')
|
||||||
|
->emptyStateActions([
|
||||||
|
CreateActionTable::make('create')
|
||||||
|
->label('Create Role')
|
||||||
|
->button(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
CreateAction::make()
|
||||||
|
->label('Create Role'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -81,7 +81,7 @@ class CreateServer extends CreateRecord
|
|||||||
])
|
])
|
||||||
->relationship('user', 'username')
|
->relationship('user', 'username')
|
||||||
->searchable(['username', 'email'])
|
->searchable(['username', 'email'])
|
||||||
->getOptionLabelFromRecordUsing(fn (User $user) => "$user->email | $user->username " . ($user->root_admin ? '(admin)' : ''))
|
->getOptionLabelFromRecordUsing(fn (User $user) => "$user->email | $user->username " . ($user->isRootAdmin() ? '(admin)' : ''))
|
||||||
->createOptionForm([
|
->createOptionForm([
|
||||||
Forms\Components\TextInput::make('username')
|
Forms\Components\TextInput::make('username')
|
||||||
->alphaNum()
|
->alphaNum()
|
||||||
@ -98,21 +98,6 @@ class CreateServer extends CreateRecord
|
|||||||
->hintIcon('tabler-question-mark')
|
->hintIcon('tabler-question-mark')
|
||||||
->hintIconTooltip('Providing a user password is optional. New user email will prompt users to create a password the first time they login.')
|
->hintIconTooltip('Providing a user password is optional. New user email will prompt users to create a password the first time they login.')
|
||||||
->password(),
|
->password(),
|
||||||
|
|
||||||
Forms\Components\ToggleButtons::make('root_admin')
|
|
||||||
->label('Administrator (Root)')
|
|
||||||
->options([
|
|
||||||
false => 'No',
|
|
||||||
true => 'Admin',
|
|
||||||
])
|
|
||||||
->colors([
|
|
||||||
false => 'primary',
|
|
||||||
true => 'danger',
|
|
||||||
])
|
|
||||||
->inline()
|
|
||||||
->required()
|
|
||||||
->default(false)
|
|
||||||
->hidden(),
|
|
||||||
])
|
])
|
||||||
->createOptionUsing(function ($data) {
|
->createOptionUsing(function ($data) {
|
||||||
resolve(UserCreationService::class)->handle($data);
|
resolve(UserCreationService::class)->handle($data);
|
||||||
|
@ -4,6 +4,7 @@ namespace App\Filament\Resources\ServerResource\Pages;
|
|||||||
|
|
||||||
use App\Filament\Resources\ServerResource;
|
use App\Filament\Resources\ServerResource;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
|
use App\Models\User;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\ListRecords;
|
use Filament\Resources\Pages\ListRecords;
|
||||||
use Filament\Tables\Actions\CreateAction;
|
use Filament\Tables\Actions\CreateAction;
|
||||||
@ -76,7 +77,13 @@ class ListServers extends ListRecords
|
|||||||
->actions([
|
->actions([
|
||||||
Tables\Actions\Action::make('View')
|
Tables\Actions\Action::make('View')
|
||||||
->icon('tabler-terminal')
|
->icon('tabler-terminal')
|
||||||
->url(fn (Server $server) => "/server/$server->uuid_short"),
|
->url(fn (Server $server) => "/server/$server->uuid_short")
|
||||||
|
->visible(function (Server $server) {
|
||||||
|
/** @var User $user */
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
return $user->isRootAdmin() || $user->id === $server->owner_id;
|
||||||
|
}),
|
||||||
Tables\Actions\EditAction::make(),
|
Tables\Actions\EditAction::make(),
|
||||||
])
|
])
|
||||||
->emptyStateIcon('tabler-brand-docker')
|
->emptyStateIcon('tabler-brand-docker')
|
||||||
|
@ -3,13 +3,16 @@
|
|||||||
namespace App\Filament\Resources\UserResource\Pages;
|
namespace App\Filament\Resources\UserResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\UserResource;
|
use App\Filament\Resources\UserResource;
|
||||||
use App\Services\Exceptions\FilamentExceptionHandler;
|
use App\Models\Role;
|
||||||
use Filament\Actions;
|
|
||||||
use Filament\Resources\Pages\EditRecord;
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Filament\Forms;
|
use Filament\Actions\DeleteAction;
|
||||||
|
use Filament\Forms\Components\CheckboxList;
|
||||||
|
use Filament\Forms\Components\Hidden;
|
||||||
use Filament\Forms\Components\Section;
|
use Filament\Forms\Components\Section;
|
||||||
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Forms\Form;
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
class EditUser extends EditRecord
|
class EditUser extends EditRecord
|
||||||
@ -20,54 +23,33 @@ class EditUser extends EditRecord
|
|||||||
return $form
|
return $form
|
||||||
->schema([
|
->schema([
|
||||||
Section::make()->schema([
|
Section::make()->schema([
|
||||||
Forms\Components\TextInput::make('username')->required()->maxLength(255),
|
TextInput::make('username')->required()->maxLength(255),
|
||||||
Forms\Components\TextInput::make('email')->email()->required()->maxLength(255),
|
TextInput::make('email')->email()->required()->maxLength(255),
|
||||||
|
TextInput::make('password')
|
||||||
Forms\Components\TextInput::make('password')
|
|
||||||
->dehydrateStateUsing(fn (string $state): string => Hash::make($state))
|
->dehydrateStateUsing(fn (string $state): string => Hash::make($state))
|
||||||
->dehydrated(fn (?string $state): bool => filled($state))
|
->dehydrated(fn (?string $state): bool => filled($state))
|
||||||
->required(fn (string $operation): bool => $operation === 'create')
|
->required(fn (string $operation): bool => $operation === 'create')
|
||||||
->password(),
|
->password(),
|
||||||
|
Select::make('language')
|
||||||
Forms\Components\ToggleButtons::make('root_admin')
|
|
||||||
->label('Administrator (Root)')
|
|
||||||
->options([
|
|
||||||
false => 'No',
|
|
||||||
true => 'Admin',
|
|
||||||
])
|
|
||||||
->colors([
|
|
||||||
false => 'primary',
|
|
||||||
true => 'danger',
|
|
||||||
])
|
|
||||||
->disableOptionWhen(function (string $operation, $value, User $user) {
|
|
||||||
if ($operation !== 'edit' || $value) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $user->isLastRootAdmin();
|
|
||||||
})
|
|
||||||
->hint(fn (User $user) => $user->isLastRootAdmin() ? 'This is the last root administrator!' : '')
|
|
||||||
->helperText(fn (User $user) => $user->isLastRootAdmin() ? 'You must have at least one root administrator in your system.' : '')
|
|
||||||
->hintColor('warning')
|
|
||||||
->inline()
|
|
||||||
->required()
|
|
||||||
->default(false),
|
|
||||||
|
|
||||||
Forms\Components\Hidden::make('skipValidation')->default(true),
|
|
||||||
|
|
||||||
Forms\Components\Select::make('language')
|
|
||||||
->required()
|
->required()
|
||||||
->hidden()
|
->hidden()
|
||||||
->default('en')
|
->default('en')
|
||||||
->options(fn (User $user) => $user->getAvailableLanguages()),
|
->options(fn (User $user) => $user->getAvailableLanguages()),
|
||||||
|
Hidden::make('skipValidation')->default(true),
|
||||||
|
CheckboxList::make('roles')
|
||||||
|
->disabled(fn (User $user) => $user->id === auth()->user()->id)
|
||||||
|
->disableOptionWhen(fn (string $value): bool => $value == Role::getRootAdmin()->id)
|
||||||
|
->relationship('roles', 'name')
|
||||||
|
->label('Admin Roles')
|
||||||
|
->columnSpanFull()
|
||||||
|
->bulkToggleable(false),
|
||||||
])->columns(),
|
])->columns(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Actions\DeleteAction::make()
|
DeleteAction::make()
|
||||||
->label(fn (User $user) => auth()->user()->id === $user->id ? 'Can\'t Delete Yourself' : ($user->servers()->count() > 0 ? 'User Has Servers' : 'Delete'))
|
->label(fn (User $user) => auth()->user()->id === $user->id ? 'Can\'t Delete Yourself' : ($user->servers()->count() > 0 ? 'User Has Servers' : 'Delete'))
|
||||||
->disabled(fn (User $user) => auth()->user()->id === $user->id || $user->servers()->count() > 0),
|
->disabled(fn (User $user) => auth()->user()->id === $user->id || $user->servers()->count() > 0),
|
||||||
$this->getSaveFormAction()->formId('form'),
|
$this->getSaveFormAction()->formId('form'),
|
||||||
@ -78,9 +60,4 @@ class EditUser extends EditRecord
|
|||||||
{
|
{
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function exception($exception, $stopPropagation): void
|
|
||||||
{
|
|
||||||
(new FilamentExceptionHandler())->handle($exception, $stopPropagation);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -3,14 +3,22 @@
|
|||||||
namespace App\Filament\Resources\UserResource\Pages;
|
namespace App\Filament\Resources\UserResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\UserResource;
|
use App\Filament\Resources\UserResource;
|
||||||
|
use App\Models\Role;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\Users\UserCreationService;
|
use App\Services\Users\UserCreationService;
|
||||||
use Filament\Actions;
|
use Filament\Actions\CreateAction;
|
||||||
|
use Filament\Forms\Components\CheckboxList;
|
||||||
|
use Filament\Forms\Components\Grid;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Resources\Pages\ListRecords;
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
use Filament\Tables\Actions\BulkActionGroup;
|
||||||
|
use Filament\Tables\Actions\DeleteBulkAction;
|
||||||
|
use Filament\Tables\Actions\EditAction;
|
||||||
|
use Filament\Tables\Columns\IconColumn;
|
||||||
|
use Filament\Tables\Columns\ImageColumn;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Filament\Tables;
|
|
||||||
use Filament\Forms;
|
|
||||||
|
|
||||||
class ListUsers extends ListRecords
|
class ListUsers extends ListRecords
|
||||||
{
|
{
|
||||||
@ -21,101 +29,102 @@ class ListUsers extends ListRecords
|
|||||||
return $table
|
return $table
|
||||||
->searchable(false)
|
->searchable(false)
|
||||||
->columns([
|
->columns([
|
||||||
Tables\Columns\ImageColumn::make('picture')
|
ImageColumn::make('picture')
|
||||||
->visibleFrom('lg')
|
->visibleFrom('lg')
|
||||||
->label('')
|
->label('')
|
||||||
->extraImgAttributes(['class' => 'rounded-full'])
|
->extraImgAttributes(['class' => 'rounded-full'])
|
||||||
->defaultImageUrl(fn (User $user) => 'https://gravatar.com/avatar/' . md5(strtolower($user->email))),
|
->defaultImageUrl(fn (User $user) => 'https://gravatar.com/avatar/' . md5(strtolower($user->email))),
|
||||||
Tables\Columns\TextColumn::make('external_id')
|
TextColumn::make('external_id')
|
||||||
->searchable()
|
->searchable()
|
||||||
->hidden(),
|
->hidden(),
|
||||||
Tables\Columns\TextColumn::make('uuid')
|
TextColumn::make('uuid')
|
||||||
->label('UUID')
|
->label('UUID')
|
||||||
->hidden()
|
->hidden()
|
||||||
->searchable(),
|
->searchable(),
|
||||||
Tables\Columns\TextColumn::make('username')
|
TextColumn::make('username')
|
||||||
->searchable(),
|
->searchable(),
|
||||||
Tables\Columns\TextColumn::make('email')
|
TextColumn::make('email')
|
||||||
->searchable()
|
->searchable()
|
||||||
->icon('tabler-mail'),
|
->icon('tabler-mail'),
|
||||||
Tables\Columns\IconColumn::make('root_admin')
|
IconColumn::make('use_totp')
|
||||||
->visibleFrom('md')
|
->label('2FA')
|
||||||
->label('Admin')
|
|
||||||
->boolean()
|
|
||||||
->trueIcon('tabler-star-filled')
|
|
||||||
->falseIcon('tabler-star-off')
|
|
||||||
->sortable(),
|
|
||||||
Tables\Columns\IconColumn::make('use_totp')->label('2FA')
|
|
||||||
->visibleFrom('lg')
|
->visibleFrom('lg')
|
||||||
->icon(fn (User $user) => $user->use_totp ? 'tabler-lock' : 'tabler-lock-open-off')
|
->icon(fn (User $user) => $user->use_totp ? 'tabler-lock' : 'tabler-lock-open-off')
|
||||||
->boolean()->sortable(),
|
->boolean()->sortable(),
|
||||||
Tables\Columns\TextColumn::make('servers_count')
|
TextColumn::make('roles_count')
|
||||||
|
->counts('roles')
|
||||||
|
->icon('tabler-users-group')
|
||||||
|
->label('Roles')
|
||||||
|
->formatStateUsing(fn (User $user, $state) => $state . ($user->isRootAdmin() ? ' (Root Admin)' : '')),
|
||||||
|
TextColumn::make('servers_count')
|
||||||
->counts('servers')
|
->counts('servers')
|
||||||
->icon('tabler-server')
|
->icon('tabler-server')
|
||||||
->label('Servers'),
|
->label('Servers'),
|
||||||
Tables\Columns\TextColumn::make('subusers_count')
|
TextColumn::make('subusers_count')
|
||||||
->visibleFrom('sm')
|
->visibleFrom('sm')
|
||||||
->label('Subusers')
|
->label('Subusers')
|
||||||
->counts('subusers')
|
->counts('subusers')
|
||||||
->icon('tabler-users'),
|
->icon('tabler-users'),
|
||||||
// ->formatStateUsing(fn (string $state, $record): string => (string) ($record->servers_count + $record->subusers_count))
|
// ->formatStateUsing(fn (string $state, $record): string => (string) ($record->servers_count + $record->subusers_count))
|
||||||
])
|
])
|
||||||
->filters([
|
|
||||||
//
|
|
||||||
])
|
|
||||||
->actions([
|
->actions([
|
||||||
Tables\Actions\EditAction::make(),
|
EditAction::make(),
|
||||||
])
|
])
|
||||||
->checkIfRecordIsSelectableUsing(fn (User $user) => auth()->user()->id !== $user->id && !$user->servers_count)
|
->checkIfRecordIsSelectableUsing(fn (User $user) => auth()->user()->id !== $user->id && !$user->servers_count)
|
||||||
->bulkActions([
|
->bulkActions([
|
||||||
Tables\Actions\BulkActionGroup::make([
|
BulkActionGroup::make([
|
||||||
Tables\Actions\DeleteBulkAction::make(),
|
DeleteBulkAction::make()
|
||||||
|
->authorize(fn () => auth()->user()->can('delete user')),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Actions\CreateAction::make('create')
|
CreateAction::make('create')
|
||||||
->label('Create User')
|
->label('Create User')
|
||||||
->createAnother(false)
|
->createAnother(false)
|
||||||
->form([
|
->form([
|
||||||
Forms\Components\Grid::make()
|
Grid::make()
|
||||||
->schema([
|
->schema([
|
||||||
Forms\Components\TextInput::make('username')
|
TextInput::make('username')
|
||||||
->alphaNum()
|
->alphaNum()
|
||||||
->required()
|
->required()
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
Forms\Components\TextInput::make('email')
|
TextInput::make('email')
|
||||||
->email()
|
->email()
|
||||||
->required()
|
->required()
|
||||||
->unique()
|
->unique()
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
|
TextInput::make('password')
|
||||||
Forms\Components\TextInput::make('password')
|
|
||||||
->hintIcon('tabler-question-mark')
|
->hintIcon('tabler-question-mark')
|
||||||
->hintIconTooltip('Providing a user password is optional. New user email will prompt users to create a password the first time they login.')
|
->hintIconTooltip('Providing a user password is optional. New user email will prompt users to create a password the first time they login.')
|
||||||
->password(),
|
->password(),
|
||||||
|
CheckboxList::make('roles')
|
||||||
Forms\Components\ToggleButtons::make('root_admin')
|
->disableOptionWhen(fn (string $value): bool => $value == Role::getRootAdmin()->id)
|
||||||
->label('Administrator (Root)')
|
->relationship('roles', 'name')
|
||||||
->options([
|
->dehydrated()
|
||||||
false => 'No',
|
->label('Admin Roles')
|
||||||
true => 'Admin',
|
->columnSpanFull()
|
||||||
])
|
->bulkToggleable(false),
|
||||||
->colors([
|
|
||||||
false => 'primary',
|
|
||||||
true => 'danger',
|
|
||||||
])
|
|
||||||
->inline()
|
|
||||||
->required()
|
|
||||||
->default(false),
|
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
->successRedirectUrl(route('filament.admin.resources.users.index'))
|
->successRedirectUrl(route('filament.admin.resources.users.index'))
|
||||||
->action(function (array $data) {
|
->action(function (array $data) {
|
||||||
resolve(UserCreationService::class)->handle($data);
|
$roles = $data['roles'];
|
||||||
Notification::make()->title('User Created!')->success()->send();
|
$roles = collect($roles)->map(fn ($role) => Role::findById($role));
|
||||||
|
unset($data['roles']);
|
||||||
|
|
||||||
|
/** @var UserCreationService $creationService */
|
||||||
|
$creationService = resolve(UserCreationService::class);
|
||||||
|
$user = $creationService->handle($data);
|
||||||
|
|
||||||
|
$user->syncRoles($roles);
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title('User Created!')
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
|
||||||
return redirect()->route('filament.admin.resources.users.index');
|
return redirect()->route('filament.admin.resources.users.index');
|
||||||
}),
|
}),
|
||||||
|
@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\Application\Roles;
|
||||||
|
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use App\Models\Role;
|
||||||
|
use Spatie\QueryBuilder\QueryBuilder;
|
||||||
|
use App\Transformers\Api\Application\RoleTransformer;
|
||||||
|
use App\Http\Controllers\Api\Application\ApplicationApiController;
|
||||||
|
use App\Http\Requests\Api\Application\Roles\GetRoleRequest;
|
||||||
|
use App\Http\Requests\Api\Application\Roles\StoreRoleRequest;
|
||||||
|
use App\Http\Requests\Api\Application\Roles\DeleteRoleRequest;
|
||||||
|
use App\Http\Requests\Api\Application\Roles\UpdateRoleRequest;
|
||||||
|
|
||||||
|
class RoleController extends ApplicationApiController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Return all the roles currently registered on the Panel.
|
||||||
|
*/
|
||||||
|
public function index(GetRoleRequest $request): array
|
||||||
|
{
|
||||||
|
$roles = QueryBuilder::for(Role::query())
|
||||||
|
->allowedFilters(['name'])
|
||||||
|
->allowedSorts(['name'])
|
||||||
|
->paginate($request->query('per_page') ?? 10);
|
||||||
|
|
||||||
|
return $this->fractal->collection($roles)
|
||||||
|
->transformWith($this->getTransformer(RoleTransformer::class))
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a single role.
|
||||||
|
*/
|
||||||
|
public function view(GetRoleRequest $request, Role $role): array
|
||||||
|
{
|
||||||
|
return $this->fractal->item($role)
|
||||||
|
->transformWith($this->getTransformer(RoleTransformer::class))
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a new role on the Panel and return an HTTP/201 response code with the
|
||||||
|
* new role attached.
|
||||||
|
*
|
||||||
|
* @throws \Throwable
|
||||||
|
*/
|
||||||
|
public function store(StoreRoleRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
$role = Role::create($request->validated());
|
||||||
|
|
||||||
|
return $this->fractal->item($role)
|
||||||
|
->transformWith($this->getTransformer(RoleTransformer::class))
|
||||||
|
->addMeta([
|
||||||
|
'resource' => route('api.application.roles.view', [
|
||||||
|
'role' => $role->id,
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
->respond(201);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a role on the Panel and return the updated record to the user.
|
||||||
|
*
|
||||||
|
* @throws \Throwable
|
||||||
|
*/
|
||||||
|
public function update(UpdateRoleRequest $request, Role $role): array
|
||||||
|
{
|
||||||
|
$role->update($request->validated());
|
||||||
|
|
||||||
|
return $this->fractal->item($role)
|
||||||
|
->transformWith($this->getTransformer(RoleTransformer::class))
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a role from the Panel.
|
||||||
|
*
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function delete(DeleteRoleRequest $request, Role $role): Response
|
||||||
|
{
|
||||||
|
$role->delete();
|
||||||
|
|
||||||
|
return $this->returnNoContent();
|
||||||
|
}
|
||||||
|
}
|
@ -13,6 +13,7 @@ use App\Http\Requests\Api\Application\Users\StoreUserRequest;
|
|||||||
use App\Http\Requests\Api\Application\Users\DeleteUserRequest;
|
use App\Http\Requests\Api\Application\Users\DeleteUserRequest;
|
||||||
use App\Http\Requests\Api\Application\Users\UpdateUserRequest;
|
use App\Http\Requests\Api\Application\Users\UpdateUserRequest;
|
||||||
use App\Http\Controllers\Api\Application\ApplicationApiController;
|
use App\Http\Controllers\Api\Application\ApplicationApiController;
|
||||||
|
use App\Http\Requests\Api\Application\Users\AssignUserRolesRequest;
|
||||||
|
|
||||||
class UserController extends ApplicationApiController
|
class UserController extends ApplicationApiController
|
||||||
{
|
{
|
||||||
@ -75,6 +76,19 @@ class UserController extends ApplicationApiController
|
|||||||
return $response->toArray();
|
return $response->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign roles to a user.
|
||||||
|
*/
|
||||||
|
public function roles(AssignUserRolesRequest $request, User $user): array
|
||||||
|
{
|
||||||
|
$user->syncRoles($request->input('roles'));
|
||||||
|
|
||||||
|
$response = $this->fractal->item($user)
|
||||||
|
->transformWith($this->getTransformer(UserTransformer::class));
|
||||||
|
|
||||||
|
return $response->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store a new user on the system. Returns the created user and an HTTP/201
|
* Store a new user on the system. Returns the created user and an HTTP/201
|
||||||
* header on successful creation.
|
* header on successful creation.
|
||||||
|
@ -48,7 +48,7 @@ class ClientController extends ClientApiController
|
|||||||
if (in_array($type, ['admin', 'admin-all'])) {
|
if (in_array($type, ['admin', 'admin-all'])) {
|
||||||
// If they aren't an admin but want all the admin servers don't fail the request, just
|
// If they aren't an admin but want all the admin servers don't fail the request, just
|
||||||
// make it a query that will never return any results back.
|
// make it a query that will never return any results back.
|
||||||
if (!$user->root_admin) {
|
if (!$user->isRootAdmin()) {
|
||||||
$builder->whereRaw('1 = 2');
|
$builder->whereRaw('1 = 2');
|
||||||
} else {
|
} else {
|
||||||
$builder = $type === 'admin-all'
|
$builder = $type === 'admin-all'
|
||||||
|
@ -13,6 +13,7 @@ use Illuminate\Database\Query\JoinClause;
|
|||||||
use App\Http\Requests\Api\Client\ClientApiRequest;
|
use App\Http\Requests\Api\Client\ClientApiRequest;
|
||||||
use App\Transformers\Api\Client\ActivityLogTransformer;
|
use App\Transformers\Api\Client\ActivityLogTransformer;
|
||||||
use App\Http\Controllers\Api\Client\ClientApiController;
|
use App\Http\Controllers\Api\Client\ClientApiController;
|
||||||
|
use App\Models\Role;
|
||||||
|
|
||||||
class ActivityLogController extends ClientApiController
|
class ActivityLogController extends ClientApiController
|
||||||
{
|
{
|
||||||
@ -32,15 +33,16 @@ class ActivityLogController extends ClientApiController
|
|||||||
// We could do this with a query and a lot of joins, but that gets pretty
|
// We could do this with a query and a lot of joins, but that gets pretty
|
||||||
// painful so for now we'll execute a simpler query.
|
// painful so for now we'll execute a simpler query.
|
||||||
$subusers = $server->subusers()->pluck('user_id')->merge([$server->owner_id]);
|
$subusers = $server->subusers()->pluck('user_id')->merge([$server->owner_id]);
|
||||||
|
$rootAdmins = Role::getRootAdmin()->users()->pluck('id');
|
||||||
|
|
||||||
$builder->select('activity_logs.*')
|
$builder->select('activity_logs.*')
|
||||||
->leftJoin('users', function (JoinClause $join) {
|
->leftJoin('users', function (JoinClause $join) {
|
||||||
$join->on('users.id', 'activity_logs.actor_id')
|
$join->on('users.id', 'activity_logs.actor_id')
|
||||||
->where('activity_logs.actor_type', (new User())->getMorphClass());
|
->where('activity_logs.actor_type', (new User())->getMorphClass());
|
||||||
})
|
})
|
||||||
->where(function (Builder $builder) use ($subusers) {
|
->where(function (Builder $builder) use ($subusers, $rootAdmins) {
|
||||||
$builder->whereNull('users.id')
|
$builder->whereNull('users.id')
|
||||||
->orWhere('users.root_admin', 0)
|
->orWhereNotIn('users.id', $rootAdmins)
|
||||||
->orWhereIn('users.id', $subusers);
|
->orWhereIn('users.id', $subusers);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
@ -140,7 +140,7 @@ class SftpAuthenticationController extends Controller
|
|||||||
*/
|
*/
|
||||||
protected function validateSftpAccess(User $user, Server $server): void
|
protected function validateSftpAccess(User $user, Server $server): void
|
||||||
{
|
{
|
||||||
if (!$user->root_admin && $server->owner_id !== $user->id) {
|
if (!$user->isRootAdmin() && $server->owner_id !== $user->id) {
|
||||||
$permissions = $this->permissions->handle($server, $user);
|
$permissions = $this->permissions->handle($server, $user);
|
||||||
|
|
||||||
if (!in_array(Permission::ACTION_FILE_SFTP, $permissions)) {
|
if (!in_array(Permission::ACTION_FILE_SFTP, $permissions)) {
|
||||||
|
@ -14,7 +14,7 @@ class AdminAuthenticate
|
|||||||
*/
|
*/
|
||||||
public function handle(Request $request, \Closure $next): mixed
|
public function handle(Request $request, \Closure $next): mixed
|
||||||
{
|
{
|
||||||
if (!$request->user() || !$request->user()->root_admin) {
|
if (!$request->user() || !$request->user()->isRootAdmin()) {
|
||||||
throw new AccessDeniedHttpException();
|
throw new AccessDeniedHttpException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ class AuthenticateApplicationUser
|
|||||||
{
|
{
|
||||||
/** @var \App\Models\User|null $user */
|
/** @var \App\Models\User|null $user */
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
if (!$user || !$user->root_admin) {
|
if (!$user || !$user->isRootAdmin()) {
|
||||||
throw new AccessDeniedHttpException('This account does not have permission to access the API.');
|
throw new AccessDeniedHttpException('This account does not have permission to access the API.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ class AuthenticateServerAccess
|
|||||||
// At the very least, ensure that the user trying to make this request is the
|
// At the very least, ensure that the user trying to make this request is the
|
||||||
// server owner, a subuser, or a root admin. We'll leave it up to the controllers
|
// server owner, a subuser, or a root admin. We'll leave it up to the controllers
|
||||||
// to authenticate more detailed permissions if needed.
|
// to authenticate more detailed permissions if needed.
|
||||||
if ($user->id !== $server->owner_id && !$user->root_admin) {
|
if ($user->id !== $server->owner_id && !$user->isRootAdmin()) {
|
||||||
// Check for subuser status.
|
// Check for subuser status.
|
||||||
if (!$server->subusers->contains('user_id', $user->id)) {
|
if (!$server->subusers->contains('user_id', $user->id)) {
|
||||||
throw new NotFoundHttpException(trans('exceptions.api.resource_not_found'));
|
throw new NotFoundHttpException(trans('exceptions.api.resource_not_found'));
|
||||||
@ -55,7 +55,7 @@ class AuthenticateServerAccess
|
|||||||
if (($server->isSuspended() || $server->node->isUnderMaintenance()) && !$request->routeIs('api:client:server.resources')) {
|
if (($server->isSuspended() || $server->node->isUnderMaintenance()) && !$request->routeIs('api:client:server.resources')) {
|
||||||
throw $exception;
|
throw $exception;
|
||||||
}
|
}
|
||||||
if (!$user->root_admin || !$request->routeIs($this->except)) {
|
if (!$user->isRootAdmin() || !$request->routeIs($this->except)) {
|
||||||
throw $exception;
|
throw $exception;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ class RequireTwoFactorAuthentication
|
|||||||
// If the level is set as admin and the user is not an admin, pass them through as well.
|
// If the level is set as admin and the user is not an admin, pass them through as well.
|
||||||
if ($level === self::LEVEL_NONE || $user->use_totp) {
|
if ($level === self::LEVEL_NONE || $user->use_totp) {
|
||||||
return $next($request);
|
return $next($request);
|
||||||
} elseif ($level === self::LEVEL_ADMIN && !$user->root_admin) {
|
} elseif ($level === self::LEVEL_ADMIN && !$user->isRootAdmin()) {
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ abstract class AdminFormRequest extends FormRequest
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (bool) $this->user()->root_admin;
|
return $this->user()->isRootAdmin();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,7 +22,6 @@ class NewUserFormRequest extends AdminFormRequest
|
|||||||
'name_last',
|
'name_last',
|
||||||
'password',
|
'password',
|
||||||
'language',
|
'language',
|
||||||
'root_admin',
|
|
||||||
])->toArray();
|
])->toArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,6 @@ class UserFormRequest extends AdminFormRequest
|
|||||||
'name_last',
|
'name_last',
|
||||||
'password',
|
'password',
|
||||||
'language',
|
'language',
|
||||||
'root_admin',
|
|
||||||
])->toArray();
|
])->toArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Api\Application\Roles;
|
||||||
|
|
||||||
|
use App\Services\Acl\Api\AdminAcl;
|
||||||
|
use App\Http\Requests\Api\Application\ApplicationApiRequest;
|
||||||
|
|
||||||
|
class DeleteRoleRequest extends ApplicationApiRequest
|
||||||
|
{
|
||||||
|
protected ?string $resource = AdminAcl::RESOURCE_ROLES;
|
||||||
|
|
||||||
|
protected int $permission = AdminAcl::WRITE;
|
||||||
|
}
|
13
app/Http/Requests/Api/Application/Roles/GetRoleRequest.php
Normal file
13
app/Http/Requests/Api/Application/Roles/GetRoleRequest.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Api\Application\Roles;
|
||||||
|
|
||||||
|
use App\Services\Acl\Api\AdminAcl;
|
||||||
|
use App\Http\Requests\Api\Application\ApplicationApiRequest;
|
||||||
|
|
||||||
|
class GetRoleRequest extends ApplicationApiRequest
|
||||||
|
{
|
||||||
|
protected ?string $resource = AdminAcl::RESOURCE_ROLES;
|
||||||
|
|
||||||
|
protected int $permission = AdminAcl::READ;
|
||||||
|
}
|
21
app/Http/Requests/Api/Application/Roles/StoreRoleRequest.php
Normal file
21
app/Http/Requests/Api/Application/Roles/StoreRoleRequest.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Api\Application\Roles;
|
||||||
|
|
||||||
|
use App\Services\Acl\Api\AdminAcl;
|
||||||
|
use App\Http\Requests\Api\Application\ApplicationApiRequest;
|
||||||
|
|
||||||
|
class StoreRoleRequest extends ApplicationApiRequest
|
||||||
|
{
|
||||||
|
protected ?string $resource = AdminAcl::RESOURCE_ROLES;
|
||||||
|
|
||||||
|
protected int $permission = AdminAcl::WRITE;
|
||||||
|
|
||||||
|
public function rules(array $rules = null): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => 'required|string',
|
||||||
|
'guard_name' => 'nullable|string',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Api\Application\Roles;
|
||||||
|
|
||||||
|
class UpdateRoleRequest extends StoreRoleRequest
|
||||||
|
{
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Api\Application\Users;
|
||||||
|
|
||||||
|
class AssignUserRolesRequest extends StoreUserRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Return the validation rules for this request.
|
||||||
|
*/
|
||||||
|
public function rules(array $rules = null): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'roles' => 'array',
|
||||||
|
'roles.*' => 'string',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -26,7 +26,6 @@ class StoreUserRequest extends ApplicationApiRequest
|
|||||||
'password',
|
'password',
|
||||||
'language',
|
'language',
|
||||||
'timezone',
|
'timezone',
|
||||||
'root_admin',
|
|
||||||
])->toArray();
|
])->toArray();
|
||||||
|
|
||||||
$response['first_name'] = $rules['name_first'];
|
$response['first_name'] = $rules['name_first'];
|
||||||
@ -56,7 +55,6 @@ class StoreUserRequest extends ApplicationApiRequest
|
|||||||
'external_id' => 'Third Party Identifier',
|
'external_id' => 'Third Party Identifier',
|
||||||
'name_first' => 'First Name',
|
'name_first' => 'First Name',
|
||||||
'name_last' => 'Last Name',
|
'name_last' => 'Last Name',
|
||||||
'root_admin' => 'Root Administrator Status',
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,7 @@ abstract class SubuserRequest extends ClientApiRequest
|
|||||||
$server = $this->route()->parameter('server');
|
$server = $this->route()->parameter('server');
|
||||||
|
|
||||||
// If we are a root admin or the server owner, no need to perform these checks.
|
// If we are a root admin or the server owner, no need to perform these checks.
|
||||||
if ($user->root_admin || $user->id === $server->owner_id) {
|
if ($user->isRootAdmin() || $user->id === $server->owner_id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
48
app/Models/Role.php
Normal file
48
app/Models/Role.php
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Spatie\Permission\Models\Role as BaseRole;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $id
|
||||||
|
* @property string $name
|
||||||
|
* @property string $guard_name
|
||||||
|
* @property \Illuminate\Database\Eloquent\Collection|\Spatie\Permission\Models\Permission[] $permissions
|
||||||
|
* @property int|null $permissions_count
|
||||||
|
* @property \Illuminate\Database\Eloquent\Collection|\App\Models\User[] $users
|
||||||
|
* @property int|null $users_count
|
||||||
|
*/
|
||||||
|
class Role extends BaseRole
|
||||||
|
{
|
||||||
|
public const RESOURCE_NAME = 'role';
|
||||||
|
|
||||||
|
public const ROOT_ADMIN = 'Root Admin';
|
||||||
|
|
||||||
|
public const MODEL_SPECIFIC_PERMISSIONS = [
|
||||||
|
'egg' => [
|
||||||
|
'import',
|
||||||
|
'export',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
public const SPECIAL_PERMISSIONS = [
|
||||||
|
'settings' => [
|
||||||
|
'view',
|
||||||
|
'update',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
public function isRootAdmin(): bool
|
||||||
|
{
|
||||||
|
return $this->name === self::ROOT_ADMIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRootAdmin(): self
|
||||||
|
{
|
||||||
|
/** @var self $role */
|
||||||
|
$role = self::findOrCreate(self::ROOT_ADMIN);
|
||||||
|
|
||||||
|
return $role;
|
||||||
|
}
|
||||||
|
}
|
@ -25,6 +25,9 @@ use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
|
|||||||
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
|
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
|
||||||
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
|
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
|
||||||
use App\Notifications\SendPasswordReset as ResetPasswordNotification;
|
use App\Notifications\SendPasswordReset as ResetPasswordNotification;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
|
use Illuminate\Database\Eloquent\Model as IlluminateModel;
|
||||||
|
use Spatie\Permission\Traits\HasRoles;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App\Models\User.
|
* App\Models\User.
|
||||||
@ -40,7 +43,6 @@ use App\Notifications\SendPasswordReset as ResetPasswordNotification;
|
|||||||
* @property string|null $remember_token
|
* @property string|null $remember_token
|
||||||
* @property string $language
|
* @property string $language
|
||||||
* @property string $timezone
|
* @property string $timezone
|
||||||
* @property bool $root_admin
|
|
||||||
* @property bool $use_totp
|
* @property bool $use_totp
|
||||||
* @property string|null $totp_secret
|
* @property string|null $totp_secret
|
||||||
* @property \Illuminate\Support\Carbon|null $totp_authenticated_at
|
* @property \Illuminate\Support\Carbon|null $totp_authenticated_at
|
||||||
@ -77,7 +79,6 @@ use App\Notifications\SendPasswordReset as ResetPasswordNotification;
|
|||||||
* @method static Builder|User whereNameLast($value)
|
* @method static Builder|User whereNameLast($value)
|
||||||
* @method static Builder|User wherePassword($value)
|
* @method static Builder|User wherePassword($value)
|
||||||
* @method static Builder|User whereRememberToken($value)
|
* @method static Builder|User whereRememberToken($value)
|
||||||
* @method static Builder|User whereRootAdmin($value)
|
|
||||||
* @method static Builder|User whereTotpAuthenticatedAt($value)
|
* @method static Builder|User whereTotpAuthenticatedAt($value)
|
||||||
* @method static Builder|User whereTotpSecret($value)
|
* @method static Builder|User whereTotpSecret($value)
|
||||||
* @method static Builder|User whereUpdatedAt($value)
|
* @method static Builder|User whereUpdatedAt($value)
|
||||||
@ -94,6 +95,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
|||||||
use AvailableLanguages;
|
use AvailableLanguages;
|
||||||
use CanResetPassword;
|
use CanResetPassword;
|
||||||
use HasAccessTokens;
|
use HasAccessTokens;
|
||||||
|
use HasRoles;
|
||||||
use Notifiable;
|
use Notifiable;
|
||||||
|
|
||||||
public const USER_LEVEL_USER = 0;
|
public const USER_LEVEL_USER = 0;
|
||||||
@ -131,7 +133,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
|||||||
'totp_secret',
|
'totp_secret',
|
||||||
'totp_authenticated_at',
|
'totp_authenticated_at',
|
||||||
'gravatar',
|
'gravatar',
|
||||||
'root_admin',
|
|
||||||
'oauth',
|
'oauth',
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -145,7 +146,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
|||||||
*/
|
*/
|
||||||
protected $attributes = [
|
protected $attributes = [
|
||||||
'external_id' => null,
|
'external_id' => null,
|
||||||
'root_admin' => false,
|
|
||||||
'language' => 'en',
|
'language' => 'en',
|
||||||
'timezone' => 'UTC',
|
'timezone' => 'UTC',
|
||||||
'use_totp' => false,
|
'use_totp' => false,
|
||||||
@ -166,7 +166,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
|||||||
'name_first' => 'nullable|string|between:0,255',
|
'name_first' => 'nullable|string|between:0,255',
|
||||||
'name_last' => 'nullable|string|between:0,255',
|
'name_last' => 'nullable|string|between:0,255',
|
||||||
'password' => 'sometimes|nullable|string',
|
'password' => 'sometimes|nullable|string',
|
||||||
'root_admin' => 'boolean',
|
|
||||||
'language' => 'string',
|
'language' => 'string',
|
||||||
'timezone' => 'string',
|
'timezone' => 'string',
|
||||||
'use_totp' => 'boolean',
|
'use_totp' => 'boolean',
|
||||||
@ -177,7 +176,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
|||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'root_admin' => 'boolean',
|
|
||||||
'use_totp' => 'boolean',
|
'use_totp' => 'boolean',
|
||||||
'gravatar' => 'boolean',
|
'gravatar' => 'boolean',
|
||||||
'totp_authenticated_at' => 'datetime',
|
'totp_authenticated_at' => 'datetime',
|
||||||
@ -226,7 +224,10 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
|||||||
*/
|
*/
|
||||||
public function toReactObject(): array
|
public function toReactObject(): array
|
||||||
{
|
{
|
||||||
return collect($this->toArray())->except(['id', 'external_id'])->toArray();
|
return array_merge(collect($this->toArray())->except(['id', 'external_id'])->toArray(), [
|
||||||
|
'root_admin' => $this->isRootAdmin(),
|
||||||
|
'admin' => $this->canAccessPanel(Filament::getPanel('admin')),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -315,7 +316,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
|||||||
|
|
||||||
protected function checkPermission(Server $server, string $permission = ''): bool
|
protected function checkPermission(Server $server, string $permission = ''): bool
|
||||||
{
|
{
|
||||||
if ($this->root_admin || $server->owner_id === $this->id) {
|
if ($this->isRootAdmin() || $server->owner_id === $this->id) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -351,14 +352,23 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
|||||||
|
|
||||||
public function isLastRootAdmin(): bool
|
public function isLastRootAdmin(): bool
|
||||||
{
|
{
|
||||||
$rootAdmins = User::query()->where('root_admin', true)->limit(2)->get();
|
$rootAdmins = User::all()->filter(fn ($user) => $user->isRootAdmin());
|
||||||
|
|
||||||
return once(fn () => $rootAdmins->count() === 1 && $rootAdmins->first()->is($this));
|
return once(fn () => $rootAdmins->count() === 1 && $rootAdmins->first()->is($this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isRootAdmin(): bool
|
||||||
|
{
|
||||||
|
return $this->hasRole(Role::ROOT_ADMIN);
|
||||||
|
}
|
||||||
|
|
||||||
public function canAccessPanel(Panel $panel): bool
|
public function canAccessPanel(Panel $panel): bool
|
||||||
{
|
{
|
||||||
return $this->root_admin;
|
if ($this->isRootAdmin()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->roles()->count() >= 1 && $this->getAllPermissions()->count() >= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFilamentName(): string
|
public function getFilamentName(): string
|
||||||
@ -370,4 +380,13 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
|||||||
{
|
{
|
||||||
return 'https://gravatar.com/avatar/' . md5(strtolower($this->email));
|
return 'https://gravatar.com/avatar/' . md5(strtolower($this->email));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function canTarget(IlluminateModel $user): bool
|
||||||
|
{
|
||||||
|
if ($this->isRootAdmin()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user instanceof User && !$user->isRootAdmin();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
10
app/Policies/ApiKeyPolicy.php
Normal file
10
app/Policies/ApiKeyPolicy.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
class ApiKeyPolicy
|
||||||
|
{
|
||||||
|
use DefaultPolicies;
|
||||||
|
|
||||||
|
protected string $modelName = 'apikey';
|
||||||
|
}
|
10
app/Policies/DatabaseHostPolicy.php
Normal file
10
app/Policies/DatabaseHostPolicy.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
class DatabaseHostPolicy
|
||||||
|
{
|
||||||
|
use DefaultPolicies;
|
||||||
|
|
||||||
|
protected string $modelName = 'databasehost';
|
||||||
|
}
|
10
app/Policies/DatabasePolicy.php
Normal file
10
app/Policies/DatabasePolicy.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
class DatabasePolicy
|
||||||
|
{
|
||||||
|
use DefaultPolicies;
|
||||||
|
|
||||||
|
protected string $modelName = 'database';
|
||||||
|
}
|
49
app/Policies/DefaultPolicies.php
Normal file
49
app/Policies/DefaultPolicies.php
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
trait DefaultPolicies
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view any models.
|
||||||
|
*/
|
||||||
|
public function viewAny(User $user): bool
|
||||||
|
{
|
||||||
|
return $user->can('viewList ' . $this->modelName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view the model.
|
||||||
|
*/
|
||||||
|
public function view(User $user, Model $model): bool
|
||||||
|
{
|
||||||
|
return $user->can('view ' . $this->modelName, $model);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can create models.
|
||||||
|
*/
|
||||||
|
public function create(User $user): bool
|
||||||
|
{
|
||||||
|
return $user->can('create ' . $this->modelName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can update the model.
|
||||||
|
*/
|
||||||
|
public function update(User $user, Model $model): bool
|
||||||
|
{
|
||||||
|
return $user->can('update ' . $this->modelName, $model);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can delete the model.
|
||||||
|
*/
|
||||||
|
public function delete(User $user, Model $model): bool
|
||||||
|
{
|
||||||
|
return $user->can('delete ' . $this->modelName, $model);
|
||||||
|
}
|
||||||
|
}
|
@ -2,12 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Policies;
|
namespace App\Policies;
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
|
|
||||||
class EggPolicy
|
class EggPolicy
|
||||||
{
|
{
|
||||||
public function create(User $user): bool
|
use DefaultPolicies;
|
||||||
{
|
|
||||||
return true;
|
protected string $modelName = 'egg';
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
10
app/Policies/MountPolicy.php
Normal file
10
app/Policies/MountPolicy.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
class MountPolicy
|
||||||
|
{
|
||||||
|
use DefaultPolicies;
|
||||||
|
|
||||||
|
protected string $modelName = 'mount';
|
||||||
|
}
|
10
app/Policies/NodePolicy.php
Normal file
10
app/Policies/NodePolicy.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
class NodePolicy
|
||||||
|
{
|
||||||
|
use DefaultPolicies;
|
||||||
|
|
||||||
|
protected string $modelName = 'node';
|
||||||
|
}
|
10
app/Policies/RolePolicy.php
Normal file
10
app/Policies/RolePolicy.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
class RolePolicy
|
||||||
|
{
|
||||||
|
use DefaultPolicies;
|
||||||
|
|
||||||
|
protected string $modelName = 'role';
|
||||||
|
}
|
@ -2,34 +2,38 @@
|
|||||||
|
|
||||||
namespace App\Policies;
|
namespace App\Policies;
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
class ServerPolicy
|
class ServerPolicy
|
||||||
{
|
{
|
||||||
|
use DefaultPolicies;
|
||||||
|
|
||||||
|
protected string $modelName = 'server';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the user has the given permission on/for the server.
|
* Runs before any of the functions are called. Used to determine if the (sub-)user has permissions.
|
||||||
*/
|
*/
|
||||||
protected function checkPermission(User $user, Server $server, string $permission): bool
|
public function before(User $user, string $ability, string|Server $server): ?bool
|
||||||
{
|
{
|
||||||
$subuser = $server->subusers->where('user_id', $user->id)->first();
|
// For "viewAny" the $server param is the class name
|
||||||
if (!$subuser || empty($permission)) {
|
if (is_string($server)) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return in_array($permission, $subuser->permissions);
|
// Owner has full server permissions
|
||||||
}
|
if ($server->owner_id === $user->id) {
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs before any of the functions are called. Used to determine if user is root admin, if so, ignore permissions.
|
|
||||||
*/
|
|
||||||
public function before(User $user, string $ability, Server $server): bool
|
|
||||||
{
|
|
||||||
if ($user->root_admin || $server->owner_id === $user->id) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->checkPermission($user, $server, $ability);
|
$subuser = $server->subusers->where('user_id', $user->id)->first();
|
||||||
|
// If the user is a subuser check their permissions
|
||||||
|
if ($subuser) {
|
||||||
|
return in_array($ability, $subuser->permissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return null to let default policies take over
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
26
app/Policies/UserPolicy.php
Normal file
26
app/Policies/UserPolicy.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class UserPolicy
|
||||||
|
{
|
||||||
|
use DefaultPolicies {
|
||||||
|
update as defaultUpdate;
|
||||||
|
delete as defaultDelete;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected string $modelName = 'user';
|
||||||
|
|
||||||
|
public function update(User $user, Model $model): bool
|
||||||
|
{
|
||||||
|
return $user->canTarget($model) && $this->defaultUpdate($user, $model);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(User $user, Model $model): bool
|
||||||
|
{
|
||||||
|
return $user->canTarget($model) && $this->defaultDelete($user, $model);
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,7 @@ use App\Extensions\Themes\Theme;
|
|||||||
use App\Models;
|
use App\Models;
|
||||||
use App\Models\ApiKey;
|
use App\Models\ApiKey;
|
||||||
use App\Models\Node;
|
use App\Models\Node;
|
||||||
|
use App\Models\User;
|
||||||
use App\Services\Helpers\SoftwareVersionService;
|
use App\Services\Helpers\SoftwareVersionService;
|
||||||
use Dedoc\Scramble\Scramble;
|
use Dedoc\Scramble\Scramble;
|
||||||
use Dedoc\Scramble\Support\Generator\OpenApi;
|
use Dedoc\Scramble\Support\Generator\OpenApi;
|
||||||
@ -91,6 +92,10 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
'success' => Color::Green,
|
'success' => Color::Green,
|
||||||
'warning' => Color::Amber,
|
'warning' => Color::Amber,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
Gate::before(function (User $user, $ability) {
|
||||||
|
return $user->isRootAdmin() ? true : null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -32,6 +32,7 @@ class AdminAcl
|
|||||||
public const RESOURCE_DATABASE_HOSTS = 'database_hosts';
|
public const RESOURCE_DATABASE_HOSTS = 'database_hosts';
|
||||||
public const RESOURCE_SERVER_DATABASES = 'server_databases';
|
public const RESOURCE_SERVER_DATABASES = 'server_databases';
|
||||||
public const RESOURCE_MOUNTS = 'mounts';
|
public const RESOURCE_MOUNTS = 'mounts';
|
||||||
|
public const RESOURCE_ROLES = 'roles';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if an API key has permission to perform a specific read/write operation.
|
* Determine if an API key has permission to perform a specific read/write operation.
|
||||||
|
@ -14,10 +14,10 @@ class GetUserPermissionsService
|
|||||||
*/
|
*/
|
||||||
public function handle(Server $server, User $user): array
|
public function handle(Server $server, User $user): array
|
||||||
{
|
{
|
||||||
if ($user->root_admin || $user->id === $server->owner_id) {
|
if ($user->isRootAdmin() || $user->id === $server->owner_id) {
|
||||||
$permissions = ['*'];
|
$permissions = ['*'];
|
||||||
|
|
||||||
if ($user->root_admin) {
|
if ($user->isRootAdmin()) {
|
||||||
$permissions[] = 'admin.websocket.errors';
|
$permissions[] = 'admin.websocket.errors';
|
||||||
$permissions[] = 'admin.websocket.install';
|
$permissions[] = 'admin.websocket.install';
|
||||||
$permissions[] = 'admin.websocket.transfer';
|
$permissions[] = 'admin.websocket.transfer';
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Services\Users;
|
namespace App\Services\Users;
|
||||||
|
|
||||||
|
use App\Models\Role;
|
||||||
use Ramsey\Uuid\Uuid;
|
use Ramsey\Uuid\Uuid;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Contracts\Hashing\Hasher;
|
use Illuminate\Contracts\Hashing\Hasher;
|
||||||
@ -39,10 +40,17 @@ class UserCreationService
|
|||||||
$data['password'] = $this->hasher->make(str_random(30));
|
$data['password'] = $this->hasher->make(str_random(30));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$isRootAdmin = array_key_exists('root_admin', $data) && $data['root_admin'];
|
||||||
|
unset($data['root_admin']);
|
||||||
|
|
||||||
$user = User::query()->forceCreate(array_merge($data, [
|
$user = User::query()->forceCreate(array_merge($data, [
|
||||||
'uuid' => Uuid::uuid4()->toString(),
|
'uuid' => Uuid::uuid4()->toString(),
|
||||||
]));
|
]));
|
||||||
|
|
||||||
|
if ($isRootAdmin) {
|
||||||
|
$user->syncRoles(Role::getRootAdmin());
|
||||||
|
}
|
||||||
|
|
||||||
if (isset($generateResetToken)) {
|
if (isset($generateResetToken)) {
|
||||||
$token = $this->passwordBroker->createToken($user);
|
$token = $this->passwordBroker->createToken($user);
|
||||||
}
|
}
|
||||||
|
@ -77,7 +77,7 @@ abstract class BaseTransformer extends TransformerAbstract
|
|||||||
// the user is a root admin at the moment. In a future release we'll be rolling
|
// the user is a root admin at the moment. In a future release we'll be rolling
|
||||||
// out more specific permissions for keys.
|
// out more specific permissions for keys.
|
||||||
if ($token->key_type === ApiKey::TYPE_ACCOUNT) {
|
if ($token->key_type === ApiKey::TYPE_ACCOUNT) {
|
||||||
return $this->request->user()->root_admin;
|
return $this->request->user()->isRootAdmin();
|
||||||
}
|
}
|
||||||
|
|
||||||
return AdminAcl::check($token, $resource);
|
return AdminAcl::check($token, $resource);
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Transformers\Api\Application;
|
||||||
|
|
||||||
|
use Spatie\Permission\Models\Permission;
|
||||||
|
|
||||||
|
class RolePermissionTransformer extends BaseTransformer
|
||||||
|
{
|
||||||
|
public function getResourceName(): string
|
||||||
|
{
|
||||||
|
return 'permissions';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function transform(Permission $model): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => $model->name,
|
||||||
|
'guard_name' => $model->guard_name,
|
||||||
|
'created_at' => $model->created_at->toAtomString(),
|
||||||
|
'updated_at' => $model->updated_at->toAtomString(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
47
app/Transformers/Api/Application/RoleTransformer.php
Normal file
47
app/Transformers/Api/Application/RoleTransformer.php
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Transformers\Api\Application;
|
||||||
|
|
||||||
|
use App\Models\Role;
|
||||||
|
use League\Fractal\Resource\Collection;
|
||||||
|
use League\Fractal\Resource\NullResource;
|
||||||
|
|
||||||
|
class RoleTransformer extends BaseTransformer
|
||||||
|
{
|
||||||
|
protected array $availableIncludes = [
|
||||||
|
'permissions',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the resource name for the JSONAPI output.
|
||||||
|
*/
|
||||||
|
public function getResourceName(): string
|
||||||
|
{
|
||||||
|
return Role::RESOURCE_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform role into a representation for the application API.
|
||||||
|
*/
|
||||||
|
public function transform(Role $model): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => $model->name,
|
||||||
|
'guard_name' => $model->guard_name,
|
||||||
|
'created_at' => $model->created_at->toAtomString(),
|
||||||
|
'updated_at' => $model->updated_at->toAtomString(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Include the permissions associated with this role.
|
||||||
|
*
|
||||||
|
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
|
||||||
|
*/
|
||||||
|
public function includePermissions(Role $model): Collection|NullResource
|
||||||
|
{
|
||||||
|
$model->loadMissing('permissions');
|
||||||
|
|
||||||
|
return $this->collection($model->getRelation('permissions'), $this->makeTransformer(RolePermissionTransformer::class), 'permissions');
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Transformers\Api\Application;
|
namespace App\Transformers\Api\Application;
|
||||||
|
|
||||||
|
use App\Models\Role;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use League\Fractal\Resource\Collection;
|
use League\Fractal\Resource\Collection;
|
||||||
use League\Fractal\Resource\NullResource;
|
use League\Fractal\Resource\NullResource;
|
||||||
@ -12,7 +13,10 @@ class UserTransformer extends BaseTransformer
|
|||||||
/**
|
/**
|
||||||
* List of resources that can be included.
|
* List of resources that can be included.
|
||||||
*/
|
*/
|
||||||
protected array $availableIncludes = ['servers'];
|
protected array $availableIncludes = [
|
||||||
|
'servers',
|
||||||
|
'roles',
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the resource name for the JSONAPI output.
|
* Return the resource name for the JSONAPI output.
|
||||||
@ -36,7 +40,7 @@ class UserTransformer extends BaseTransformer
|
|||||||
'first_name' => $user->name_first,
|
'first_name' => $user->name_first,
|
||||||
'last_name' => $user->name_last,
|
'last_name' => $user->name_last,
|
||||||
'language' => $user->language,
|
'language' => $user->language,
|
||||||
'root_admin' => (bool) $user->root_admin,
|
'root_admin' => $user->isRootAdmin(),
|
||||||
'2fa_enabled' => (bool) $user->use_totp,
|
'2fa_enabled' => (bool) $user->use_totp,
|
||||||
'2fa' => (bool) $user->use_totp, // deprecated, use "2fa_enabled"
|
'2fa' => (bool) $user->use_totp, // deprecated, use "2fa_enabled"
|
||||||
'created_at' => $this->formatTimestamp($user->created_at),
|
'created_at' => $this->formatTimestamp($user->created_at),
|
||||||
@ -59,4 +63,20 @@ class UserTransformer extends BaseTransformer
|
|||||||
|
|
||||||
return $this->collection($user->getRelation('servers'), $this->makeTransformer(ServerTransformer::class), 'server');
|
return $this->collection($user->getRelation('servers'), $this->makeTransformer(ServerTransformer::class), 'server');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the roles associated with this user.
|
||||||
|
*
|
||||||
|
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
|
||||||
|
*/
|
||||||
|
public function includeRoles(User $user): Collection|NullResource
|
||||||
|
{
|
||||||
|
if (!$this->authorize(AdminAcl::RESOURCE_ROLES)) {
|
||||||
|
return $this->null();
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->loadMissing('roles');
|
||||||
|
|
||||||
|
return $this->collection($user->getRelation('roles'), $this->makeTransformer(RoleTransformer::class), Role::RESOURCE_NAME);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -113,6 +113,6 @@ class ActivityLogTransformer extends BaseClientTransformer
|
|||||||
*/
|
*/
|
||||||
protected function canViewIP(Model $actor = null): bool
|
protected function canViewIP(Model $actor = null): bool
|
||||||
{
|
{
|
||||||
return $actor?->is($this->request->user()) || $this->request->user()->root_admin;
|
return $actor?->is($this->request->user()) || $this->request->user()->isRootAdmin();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,8 +29,8 @@ class UserTransformer extends BaseClientTransformer
|
|||||||
'last_name' => $user->name_last,
|
'last_name' => $user->name_last,
|
||||||
'language' => $user->language,
|
'language' => $user->language,
|
||||||
'image' => 'https://gravatar.com/avatar/' . md5(Str::lower($user->email)), // deprecated
|
'image' => 'https://gravatar.com/avatar/' . md5(Str::lower($user->email)), // deprecated
|
||||||
'admin' => (bool) $user->root_admin, // deprecated, use "root_admin"
|
'admin' => $user->isRootAdmin(), // deprecated, use "root_admin"
|
||||||
'root_admin' => (bool) $user->root_admin,
|
'root_admin' => $user->isRootAdmin(),
|
||||||
'2fa_enabled' => (bool) $user->use_totp,
|
'2fa_enabled' => (bool) $user->use_totp,
|
||||||
'created_at' => $this->formatTimestamp($user->created_at),
|
'created_at' => $this->formatTimestamp($user->created_at),
|
||||||
'updated_at' => $this->formatTimestamp($user->updated_at),
|
'updated_at' => $this->formatTimestamp($user->updated_at),
|
||||||
|
@ -34,6 +34,7 @@
|
|||||||
"s1lentium/iptools": "~1.2.0",
|
"s1lentium/iptools": "~1.2.0",
|
||||||
"socialiteproviders/discord": "^4.2",
|
"socialiteproviders/discord": "^4.2",
|
||||||
"spatie/laravel-fractal": "^6.2",
|
"spatie/laravel-fractal": "^6.2",
|
||||||
|
"spatie/laravel-permission": "^6.9",
|
||||||
"spatie/laravel-query-builder": "^5.8.1",
|
"spatie/laravel-query-builder": "^5.8.1",
|
||||||
"spatie/temporary-directory": "^2.2",
|
"spatie/temporary-directory": "^2.2",
|
||||||
"symfony/http-client": "^7.1",
|
"symfony/http-client": "^7.1",
|
||||||
|
84
composer.lock
generated
84
composer.lock
generated
@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "443ec1d95b892b261af5481f27b31083",
|
"content-hash": "507ac5b637c51b90e6ae00717fe085cc",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "abdelhamiderrahmouni/filament-monaco-editor",
|
"name": "abdelhamiderrahmouni/filament-monaco-editor",
|
||||||
@ -7234,6 +7234,88 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-03-20T07:29:11+00:00"
|
"time": "2024-03-20T07:29:11+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "spatie/laravel-permission",
|
||||||
|
"version": "6.9.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/spatie/laravel-permission.git",
|
||||||
|
"reference": "fe973a58b44380d0e8620107259b7bda22f70408"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/spatie/laravel-permission/zipball/fe973a58b44380d0e8620107259b7bda22f70408",
|
||||||
|
"reference": "fe973a58b44380d0e8620107259b7bda22f70408",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"illuminate/auth": "^8.12|^9.0|^10.0|^11.0",
|
||||||
|
"illuminate/container": "^8.12|^9.0|^10.0|^11.0",
|
||||||
|
"illuminate/contracts": "^8.12|^9.0|^10.0|^11.0",
|
||||||
|
"illuminate/database": "^8.12|^9.0|^10.0|^11.0",
|
||||||
|
"php": "^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"laravel/passport": "^11.0|^12.0",
|
||||||
|
"orchestra/testbench": "^6.23|^7.0|^8.0|^9.0",
|
||||||
|
"phpunit/phpunit": "^9.4|^10.1"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-main": "6.x-dev",
|
||||||
|
"dev-master": "6.x-dev"
|
||||||
|
},
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"Spatie\\Permission\\PermissionServiceProvider"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"src/helpers.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Spatie\\Permission\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Freek Van der Herten",
|
||||||
|
"email": "freek@spatie.be",
|
||||||
|
"homepage": "https://spatie.be",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Permission handling for Laravel 8.0 and up",
|
||||||
|
"homepage": "https://github.com/spatie/laravel-permission",
|
||||||
|
"keywords": [
|
||||||
|
"acl",
|
||||||
|
"laravel",
|
||||||
|
"permission",
|
||||||
|
"permissions",
|
||||||
|
"rbac",
|
||||||
|
"roles",
|
||||||
|
"security",
|
||||||
|
"spatie"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/spatie/laravel-permission/issues",
|
||||||
|
"source": "https://github.com/spatie/laravel-permission/tree/6.9.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/spatie",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2024-06-22T23:04:52+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "spatie/laravel-query-builder",
|
"name": "spatie/laravel-query-builder",
|
||||||
"version": "5.8.1",
|
"version": "5.8.1",
|
||||||
|
13
config/permission.php
Normal file
13
config/permission.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
'models' => [
|
||||||
|
|
||||||
|
'permission' => Spatie\Permission\Models\Permission::class,
|
||||||
|
|
||||||
|
'role' => \App\Models\Role::class,
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
@ -33,19 +33,10 @@ class UserFactory extends Factory
|
|||||||
'name_last' => $this->faker->lastName(),
|
'name_last' => $this->faker->lastName(),
|
||||||
'password' => $password ?: $password = bcrypt('password'),
|
'password' => $password ?: $password = bcrypt('password'),
|
||||||
'language' => 'en',
|
'language' => 'en',
|
||||||
'root_admin' => false,
|
|
||||||
'use_totp' => false,
|
'use_totp' => false,
|
||||||
'oauth' => [],
|
'oauth' => [],
|
||||||
'created_at' => Carbon::now(),
|
'created_at' => Carbon::now(),
|
||||||
'updated_at' => Carbon::now(),
|
'updated_at' => Carbon::now(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicate that the user is an admin.
|
|
||||||
*/
|
|
||||||
public function admin(): static
|
|
||||||
{
|
|
||||||
return $this->state(['root_admin' => true]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\Role;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
class DatabaseSeeder extends Seeder
|
class DatabaseSeeder extends Seeder
|
||||||
@ -12,5 +13,7 @@ class DatabaseSeeder extends Seeder
|
|||||||
public function run()
|
public function run()
|
||||||
{
|
{
|
||||||
$this->call(EggSeeder::class);
|
$this->call(EggSeeder::class);
|
||||||
|
|
||||||
|
Role::firstOrCreate(['name' => Role::ROOT_ADMIN]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,140 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$teams = config('permission.teams');
|
||||||
|
$tableNames = config('permission.table_names');
|
||||||
|
$columnNames = config('permission.column_names');
|
||||||
|
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
|
||||||
|
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
|
||||||
|
|
||||||
|
if (empty($tableNames)) {
|
||||||
|
throw new \Exception('Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
|
||||||
|
}
|
||||||
|
if ($teams && empty($columnNames['team_foreign_key'] ?? null)) {
|
||||||
|
throw new \Exception('Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema::create($tableNames['permissions'], function (Blueprint $table) {
|
||||||
|
//$table->engine('InnoDB');
|
||||||
|
$table->bigIncrements('id'); // permission id
|
||||||
|
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
|
||||||
|
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['name', 'guard_name']);
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create($tableNames['roles'], function (Blueprint $table) use ($teams, $columnNames) {
|
||||||
|
//$table->engine('InnoDB');
|
||||||
|
$table->bigIncrements('id'); // role id
|
||||||
|
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
|
||||||
|
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
|
||||||
|
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
|
||||||
|
}
|
||||||
|
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
|
||||||
|
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
|
||||||
|
$table->timestamps();
|
||||||
|
if ($teams || config('permission.testing')) {
|
||||||
|
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
|
||||||
|
} else {
|
||||||
|
$table->unique(['name', 'guard_name']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
|
||||||
|
$table->unsignedBigInteger($pivotPermission);
|
||||||
|
|
||||||
|
$table->string('model_type');
|
||||||
|
$table->unsignedBigInteger($columnNames['model_morph_key']);
|
||||||
|
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
|
||||||
|
|
||||||
|
$table->foreign($pivotPermission)
|
||||||
|
->references('id') // permission id
|
||||||
|
->on($tableNames['permissions'])
|
||||||
|
->onDelete('cascade');
|
||||||
|
if ($teams) {
|
||||||
|
$table->unsignedBigInteger($columnNames['team_foreign_key']);
|
||||||
|
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
|
||||||
|
|
||||||
|
$table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
|
||||||
|
'model_has_permissions_permission_model_type_primary');
|
||||||
|
} else {
|
||||||
|
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
|
||||||
|
'model_has_permissions_permission_model_type_primary');
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
|
||||||
|
$table->unsignedBigInteger($pivotRole);
|
||||||
|
|
||||||
|
$table->string('model_type');
|
||||||
|
$table->unsignedBigInteger($columnNames['model_morph_key']);
|
||||||
|
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
|
||||||
|
|
||||||
|
$table->foreign($pivotRole)
|
||||||
|
->references('id') // role id
|
||||||
|
->on($tableNames['roles'])
|
||||||
|
->onDelete('cascade');
|
||||||
|
if ($teams) {
|
||||||
|
$table->unsignedBigInteger($columnNames['team_foreign_key']);
|
||||||
|
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
|
||||||
|
|
||||||
|
$table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
|
||||||
|
'model_has_roles_role_model_type_primary');
|
||||||
|
} else {
|
||||||
|
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'],
|
||||||
|
'model_has_roles_role_model_type_primary');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
|
||||||
|
$table->unsignedBigInteger($pivotPermission);
|
||||||
|
$table->unsignedBigInteger($pivotRole);
|
||||||
|
|
||||||
|
$table->foreign($pivotPermission)
|
||||||
|
->references('id') // permission id
|
||||||
|
->on($tableNames['permissions'])
|
||||||
|
->onDelete('cascade');
|
||||||
|
|
||||||
|
$table->foreign($pivotRole)
|
||||||
|
->references('id') // role id
|
||||||
|
->on($tableNames['roles'])
|
||||||
|
->onDelete('cascade');
|
||||||
|
|
||||||
|
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
|
||||||
|
});
|
||||||
|
|
||||||
|
app('cache')
|
||||||
|
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
|
||||||
|
->forget(config('permission.cache.key'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$tableNames = config('permission.table_names');
|
||||||
|
|
||||||
|
if (empty($tableNames)) {
|
||||||
|
throw new \Exception('Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema::drop($tableNames['role_has_permissions']);
|
||||||
|
Schema::drop($tableNames['model_has_roles']);
|
||||||
|
Schema::drop($tableNames['model_has_permissions']);
|
||||||
|
Schema::drop($tableNames['roles']);
|
||||||
|
Schema::drop($tableNames['permissions']);
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Role;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$adminUsers = User::whereRootAdmin(true)->get();
|
||||||
|
foreach ($adminUsers as $adminUser) {
|
||||||
|
$adminUser->syncRoles(Role::getRootAdmin());
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('root_admin');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->tinyInteger('root_admin')->unsigned()->default(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
@ -13,7 +13,6 @@ return [
|
|||||||
'hint' => 'This is the last root administrator!',
|
'hint' => 'This is the last root administrator!',
|
||||||
'helper_text' => 'You must have at least one root administrator in your system.',
|
'helper_text' => 'You must have at least one root administrator in your system.',
|
||||||
],
|
],
|
||||||
'root_admin' => 'Administrator (Root)',
|
|
||||||
'language' => [
|
'language' => [
|
||||||
'helper_text1' => 'Your language (:state) has not been translated yet!\nBut never fear, you can help fix that by',
|
'helper_text1' => 'Your language (:state) has not been translated yet!\nBut never fear, you can help fix that by',
|
||||||
'helper_text2' => 'contributing directly here',
|
'helper_text2' => 'contributing directly here',
|
||||||
|
@ -27,6 +27,7 @@ interface ExtendedWindow extends Window {
|
|||||||
email: string;
|
email: string;
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
root_admin: boolean;
|
root_admin: boolean;
|
||||||
|
admin: boolean;
|
||||||
use_totp: boolean;
|
use_totp: boolean;
|
||||||
language: string;
|
language: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
@ -46,6 +47,7 @@ const App = () => {
|
|||||||
email: PanelUser.email,
|
email: PanelUser.email,
|
||||||
language: PanelUser.language,
|
language: PanelUser.language,
|
||||||
rootAdmin: PanelUser.root_admin,
|
rootAdmin: PanelUser.root_admin,
|
||||||
|
admin: PanelUser.admin,
|
||||||
useTotp: PanelUser.use_totp,
|
useTotp: PanelUser.use_totp,
|
||||||
createdAt: new Date(PanelUser.created_at),
|
createdAt: new Date(PanelUser.created_at),
|
||||||
updatedAt: new Date(PanelUser.updated_at),
|
updatedAt: new Date(PanelUser.updated_at),
|
||||||
|
@ -37,7 +37,7 @@ export default () => {
|
|||||||
const { t } = useTranslation('strings');
|
const { t } = useTranslation('strings');
|
||||||
|
|
||||||
const name = useStoreState((state: ApplicationStore) => state.settings.data!.name);
|
const name = useStoreState((state: ApplicationStore) => state.settings.data!.name);
|
||||||
const rootAdmin = useStoreState((state: ApplicationStore) => state.user.data!.rootAdmin);
|
const isAdmin = useStoreState((state: ApplicationStore) => state.user.data!.admin);
|
||||||
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
||||||
|
|
||||||
const onTriggerLogout = () => {
|
const onTriggerLogout = () => {
|
||||||
@ -69,7 +69,7 @@ export default () => {
|
|||||||
<FontAwesomeIcon icon={faLayerGroup} />
|
<FontAwesomeIcon icon={faLayerGroup} />
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{rootAdmin && (
|
{isAdmin && (
|
||||||
<Tooltip placement={'bottom'} content={t<string>('admin')}>
|
<Tooltip placement={'bottom'} content={t<string>('admin')}>
|
||||||
<a href={'/admin'} rel={'noreferrer'}>
|
<a href={'/admin'} rel={'noreferrer'}>
|
||||||
<FontAwesomeIcon icon={faCogs} />
|
<FontAwesomeIcon icon={faCogs} />
|
||||||
|
@ -7,6 +7,7 @@ export interface UserData {
|
|||||||
email: string;
|
email: string;
|
||||||
language: string;
|
language: string;
|
||||||
rootAdmin: boolean;
|
rootAdmin: boolean;
|
||||||
|
admin: boolean;
|
||||||
useTotp: boolean;
|
useTotp: boolean;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
@ -19,6 +19,8 @@ Route::prefix('/users')->group(function () {
|
|||||||
Route::post('/', [Application\Users\UserController::class, 'store']);
|
Route::post('/', [Application\Users\UserController::class, 'store']);
|
||||||
Route::patch('/{user:id}', [Application\Users\UserController::class, 'update']);
|
Route::patch('/{user:id}', [Application\Users\UserController::class, 'update']);
|
||||||
|
|
||||||
|
Route::patch('/{user:id}/roles', [Application\Users\UserController::class, 'roles']);
|
||||||
|
|
||||||
Route::delete('/{user:id}', [Application\Users\UserController::class, 'delete']);
|
Route::delete('/{user:id}', [Application\Users\UserController::class, 'delete']);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -141,3 +143,22 @@ Route::prefix('mounts')->group(function () {
|
|||||||
Route::delete('/{mount:id}/eggs/{egg_id}', [Application\Mounts\MountController::class, 'deleteEgg']);
|
Route::delete('/{mount:id}/eggs/{egg_id}', [Application\Mounts\MountController::class, 'deleteEgg']);
|
||||||
Route::delete('/{mount:id}/nodes/{node_id}', [Application\Mounts\MountController::class, 'deleteNode']);
|
Route::delete('/{mount:id}/nodes/{node_id}', [Application\Mounts\MountController::class, 'deleteNode']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Role Controller Routes
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Endpoint: /api/application/roles
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
Route::group(['prefix' => '/roles'], function () {
|
||||||
|
Route::get('/', [Application\Roles\RoleController::class, 'index'])->name('api.application.roles');
|
||||||
|
Route::get('/{role:id}', [Application\Roles\RoleController::class, 'view'])->name('api.application.roles.view');
|
||||||
|
|
||||||
|
Route::post('/', [Application\Roles\RoleController::class, 'store']);
|
||||||
|
|
||||||
|
Route::patch('/{role:id}', [Application\Roles\RoleController::class, 'update']);
|
||||||
|
|
||||||
|
Route::delete('/{role:id}', [Application\Roles\RoleController::class, 'delete']);
|
||||||
|
});
|
||||||
|
@ -6,6 +6,7 @@ use Illuminate\Http\Request;
|
|||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use PHPUnit\Framework\Assert;
|
use PHPUnit\Framework\Assert;
|
||||||
use App\Models\ApiKey;
|
use App\Models\ApiKey;
|
||||||
|
use App\Models\Role;
|
||||||
use App\Services\Acl\Api\AdminAcl;
|
use App\Services\Acl\Api\AdminAcl;
|
||||||
use App\Tests\Integration\IntegrationTestCase;
|
use App\Tests\Integration\IntegrationTestCase;
|
||||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||||
@ -67,9 +68,10 @@ abstract class ApplicationApiIntegrationTestCase extends IntegrationTestCase
|
|||||||
*/
|
*/
|
||||||
protected function createApiUser(): User
|
protected function createApiUser(): User
|
||||||
{
|
{
|
||||||
return User::factory()->create([
|
$user = User::factory()->create();
|
||||||
'root_admin' => true,
|
$user->syncRoles(Role::getRootAdmin());
|
||||||
]);
|
|
||||||
|
return $user;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -38,7 +38,7 @@ class ExternalUserControllerTest extends ApplicationApiIntegrationTestCase
|
|||||||
'first_name' => $user->name_first,
|
'first_name' => $user->name_first,
|
||||||
'last_name' => $user->name_last,
|
'last_name' => $user->name_last,
|
||||||
'language' => $user->language,
|
'language' => $user->language,
|
||||||
'root_admin' => (bool) $user->root_admin,
|
'root_admin' => (bool) $user->isRootAdmin(),
|
||||||
'2fa' => (bool) $user->totp_enabled,
|
'2fa' => (bool) $user->totp_enabled,
|
||||||
'created_at' => $this->formatTimestamp($user->created_at),
|
'created_at' => $this->formatTimestamp($user->created_at),
|
||||||
'updated_at' => $this->formatTimestamp($user->updated_at),
|
'updated_at' => $this->formatTimestamp($user->updated_at),
|
||||||
|
@ -55,7 +55,7 @@ class UserControllerTest extends ApplicationApiIntegrationTestCase
|
|||||||
'first_name' => $this->getApiUser()->name_first,
|
'first_name' => $this->getApiUser()->name_first,
|
||||||
'last_name' => $this->getApiUser()->name_last,
|
'last_name' => $this->getApiUser()->name_last,
|
||||||
'language' => $this->getApiUser()->language,
|
'language' => $this->getApiUser()->language,
|
||||||
'root_admin' => $this->getApiUser()->root_admin,
|
'root_admin' => $this->getApiUser()->isRootAdmin(),
|
||||||
'2fa_enabled' => (bool) $this->getApiUser()->totp_enabled,
|
'2fa_enabled' => (bool) $this->getApiUser()->totp_enabled,
|
||||||
'2fa' => (bool) $this->getApiUser()->totp_enabled,
|
'2fa' => (bool) $this->getApiUser()->totp_enabled,
|
||||||
'created_at' => $this->formatTimestamp($this->getApiUser()->created_at),
|
'created_at' => $this->formatTimestamp($this->getApiUser()->created_at),
|
||||||
@ -73,7 +73,7 @@ class UserControllerTest extends ApplicationApiIntegrationTestCase
|
|||||||
'first_name' => $user->name_first,
|
'first_name' => $user->name_first,
|
||||||
'last_name' => $user->name_last,
|
'last_name' => $user->name_last,
|
||||||
'language' => $user->language,
|
'language' => $user->language,
|
||||||
'root_admin' => (bool) $user->root_admin,
|
'root_admin' => (bool) $user->isRootAdmin(),
|
||||||
'2fa_enabled' => (bool) $user->totp_enabled,
|
'2fa_enabled' => (bool) $user->totp_enabled,
|
||||||
'2fa' => (bool) $user->totp_enabled,
|
'2fa' => (bool) $user->totp_enabled,
|
||||||
'created_at' => $this->formatTimestamp($user->created_at),
|
'created_at' => $this->formatTimestamp($user->created_at),
|
||||||
|
@ -7,6 +7,7 @@ use App\Models\Server;
|
|||||||
use App\Models\Subuser;
|
use App\Models\Subuser;
|
||||||
use App\Models\Allocation;
|
use App\Models\Allocation;
|
||||||
use App\Models\Permission;
|
use App\Models\Permission;
|
||||||
|
use App\Models\Role;
|
||||||
|
|
||||||
class ClientControllerTest extends ClientApiIntegrationTestCase
|
class ClientControllerTest extends ClientApiIntegrationTestCase
|
||||||
{
|
{
|
||||||
@ -47,7 +48,7 @@ class ClientControllerTest extends ClientApiIntegrationTestCase
|
|||||||
{
|
{
|
||||||
/** @var \App\Models\User[] $users */
|
/** @var \App\Models\User[] $users */
|
||||||
$users = User::factory()->times(2)->create();
|
$users = User::factory()->times(2)->create();
|
||||||
$users[0]->update(['root_admin' => true]);
|
$users[0]->syncRoles(Role::getRootAdmin());
|
||||||
|
|
||||||
/** @var \App\Models\Server[] $servers */
|
/** @var \App\Models\Server[] $servers */
|
||||||
$servers = [
|
$servers = [
|
||||||
@ -225,7 +226,7 @@ class ClientControllerTest extends ClientApiIntegrationTestCase
|
|||||||
{
|
{
|
||||||
/** @var \App\Models\User[] $users */
|
/** @var \App\Models\User[] $users */
|
||||||
$users = User::factory()->times(4)->create();
|
$users = User::factory()->times(4)->create();
|
||||||
$users[0]->update(['root_admin' => true]);
|
$users[0]->syncRoles(Role::getRootAdmin());
|
||||||
|
|
||||||
$servers = [
|
$servers = [
|
||||||
$this->createServerModel(['user_id' => $users[0]->id]),
|
$this->createServerModel(['user_id' => $users[0]->id]),
|
||||||
@ -260,7 +261,7 @@ class ClientControllerTest extends ClientApiIntegrationTestCase
|
|||||||
{
|
{
|
||||||
/** @var \App\Models\User[] $users */
|
/** @var \App\Models\User[] $users */
|
||||||
$users = User::factory()->times(4)->create();
|
$users = User::factory()->times(4)->create();
|
||||||
$users[0]->update(['root_admin' => true]);
|
$users[0]->syncRoles(Role::getRootAdmin());
|
||||||
|
|
||||||
$servers = [
|
$servers = [
|
||||||
$this->createServerModel(['user_id' => $users[0]->id]),
|
$this->createServerModel(['user_id' => $users[0]->id]),
|
||||||
|
@ -7,6 +7,7 @@ use App\Models\Node;
|
|||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use App\Models\Permission;
|
use App\Models\Permission;
|
||||||
|
use App\Models\Role;
|
||||||
use App\Models\UserSSHKey;
|
use App\Models\UserSSHKey;
|
||||||
use App\Tests\Integration\IntegrationTestCase;
|
use App\Tests\Integration\IntegrationTestCase;
|
||||||
|
|
||||||
@ -180,7 +181,7 @@ class SftpAuthenticationControllerTest extends IntegrationTestCase
|
|||||||
->assertOk()
|
->assertOk()
|
||||||
->assertJsonPath('permissions', [Permission::ACTION_FILE_READ, Permission::ACTION_FILE_SFTP]);
|
->assertJsonPath('permissions', [Permission::ACTION_FILE_READ, Permission::ACTION_FILE_SFTP]);
|
||||||
|
|
||||||
$user->update(['root_admin' => true]);
|
$user->syncRoles(Role::getRootAdmin());
|
||||||
|
|
||||||
$this->postJson('/api/remote/sftp/auth', $data)
|
$this->postJson('/api/remote/sftp/auth', $data)
|
||||||
->assertOk()
|
->assertOk()
|
||||||
@ -193,7 +194,7 @@ class SftpAuthenticationControllerTest extends IntegrationTestCase
|
|||||||
->assertOk()
|
->assertOk()
|
||||||
->assertJsonPath('permissions.0', '*');
|
->assertJsonPath('permissions.0', '*');
|
||||||
|
|
||||||
$user->update(['root_admin' => false]);
|
$user->syncRoles();
|
||||||
$this->post('/api/remote/sftp/auth', $data)->assertForbidden();
|
$this->post('/api/remote/sftp/auth', $data)->assertForbidden();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ namespace App\Tests;
|
|||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Carbon\CarbonImmutable;
|
use Carbon\CarbonImmutable;
|
||||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||||
|
use Spatie\Permission\PermissionRegistrar;
|
||||||
|
|
||||||
abstract class TestCase extends BaseTestCase
|
abstract class TestCase extends BaseTestCase
|
||||||
{
|
{
|
||||||
@ -28,6 +29,8 @@ abstract class TestCase extends BaseTestCase
|
|||||||
config()->set('app.debug', false);
|
config()->set('app.debug', false);
|
||||||
|
|
||||||
$this->setKnownUuidFactory();
|
$this->setKnownUuidFactory();
|
||||||
|
|
||||||
|
$this->app->make(PermissionRegistrar::class)->forgetCachedPermissions();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -35,13 +35,14 @@ trait RequestMockHelpers
|
|||||||
/**
|
/**
|
||||||
* Generates a new request user model and also returns the generated model.
|
* Generates a new request user model and also returns the generated model.
|
||||||
*/
|
*/
|
||||||
public function generateRequestUserModel(array $args = []): User
|
public function generateRequestUserModel(bool $isRootAdmin, array $args = []): void
|
||||||
{
|
{
|
||||||
/** @var \App\Models\User $user */
|
|
||||||
$user = User::factory()->make($args);
|
$user = User::factory()->make($args);
|
||||||
$this->setRequestUserModel($user);
|
$user = m::mock($user)->makePartial();
|
||||||
|
$user->shouldReceive('isRootAdmin')->andReturn($isRootAdmin);
|
||||||
|
|
||||||
return $user;
|
/** @var User|Mock $user */
|
||||||
|
$this->setRequestUserModel($user);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -4,6 +4,7 @@ namespace App\Tests\Unit\Http\Middleware;
|
|||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Http\Middleware\AdminAuthenticate;
|
use App\Http\Middleware\AdminAuthenticate;
|
||||||
|
use Mockery;
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
|
||||||
class AdminAuthenticateTest extends MiddlewareTestCase
|
class AdminAuthenticateTest extends MiddlewareTestCase
|
||||||
@ -13,7 +14,9 @@ class AdminAuthenticateTest extends MiddlewareTestCase
|
|||||||
*/
|
*/
|
||||||
public function testAdminsAreAuthenticated(): void
|
public function testAdminsAreAuthenticated(): void
|
||||||
{
|
{
|
||||||
$user = User::factory()->make(['root_admin' => 1]);
|
$user = User::factory()->make();
|
||||||
|
$user = Mockery::mock($user)->makePartial();
|
||||||
|
$user->shouldReceive('isRootAdmin')->andReturnTrue();
|
||||||
|
|
||||||
$this->request->shouldReceive('user')->withNoArgs()->twice()->andReturn($user);
|
$this->request->shouldReceive('user')->withNoArgs()->twice()->andReturn($user);
|
||||||
|
|
||||||
@ -39,7 +42,9 @@ class AdminAuthenticateTest extends MiddlewareTestCase
|
|||||||
{
|
{
|
||||||
$this->expectException(AccessDeniedHttpException::class);
|
$this->expectException(AccessDeniedHttpException::class);
|
||||||
|
|
||||||
$user = User::factory()->make(['root_admin' => 0]);
|
$user = User::factory()->make();
|
||||||
|
$user = Mockery::mock($user)->makePartial();
|
||||||
|
$user->shouldReceive('isRootAdmin')->andReturnFalse();
|
||||||
|
|
||||||
$this->request->shouldReceive('user')->withNoArgs()->twice()->andReturn($user);
|
$this->request->shouldReceive('user')->withNoArgs()->twice()->andReturn($user);
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ class AuthenticateUserTest extends MiddlewareTestCase
|
|||||||
{
|
{
|
||||||
$this->expectException(AccessDeniedHttpException::class);
|
$this->expectException(AccessDeniedHttpException::class);
|
||||||
|
|
||||||
$this->generateRequestUserModel(['root_admin' => false]);
|
$this->generateRequestUserModel(false);
|
||||||
|
|
||||||
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
|
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
|
||||||
}
|
}
|
||||||
@ -37,7 +37,7 @@ class AuthenticateUserTest extends MiddlewareTestCase
|
|||||||
*/
|
*/
|
||||||
public function testAdminUser(): void
|
public function testAdminUser(): void
|
||||||
{
|
{
|
||||||
$this->generateRequestUserModel(['root_admin' => true]);
|
$this->generateRequestUserModel(true);
|
||||||
|
|
||||||
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
|
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user