diff --git a/database/migrations/2025_01_23_065140_create_permission_tables.php b/database/migrations/2025_01_23_065140_create_permission_tables.php index 70a120f..a5eda8e 100644 --- a/database/migrations/2025_01_23_065140_create_permission_tables.php +++ b/database/migrations/2025_01_23_065140_create_permission_tables.php @@ -52,6 +52,7 @@ public function up(): void }); Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) { + $table->id(); $table->unsignedBigInteger($pivotPermission); $table->string('model_type'); @@ -63,19 +64,20 @@ public function up(): void ->on($tableNames['permissions']) ->onDelete('cascade'); if ($teams) { - $table->unsignedBigInteger($columnNames['team_foreign_key']); + $table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable(); $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'); + $table->unique([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_unique_constraint'); } else { - $table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'], - 'model_has_permissions_permission_model_type_primary'); + $table->unique([$pivotPermission, $columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_permission_model_type_unique'); } }); Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) { + $table->id(); $table->unsignedBigInteger($pivotRole); $table->string('model_type'); @@ -87,14 +89,14 @@ public function up(): void ->on($tableNames['roles']) ->onDelete('cascade'); if ($teams) { - $table->unsignedBigInteger($columnNames['team_foreign_key']); + $table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable(); $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'); + $table->unique([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'], + 'model_has_roles_unique_constraint'); } else { - $table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'], - 'model_has_roles_role_model_type_primary'); + $table->unique([$pivotRole, $columnNames['model_morph_key'], 'model_type'], + 'model_has_roles_role_model_type_unique'); } }); diff --git a/resources/lang/vendor/filament-shield/en/filament-shield.php b/resources/lang/vendor/filament-shield/en/filament-shield.php index a1c1c2c..e67e86c 100644 --- a/resources/lang/vendor/filament-shield/en/filament-shield.php +++ b/resources/lang/vendor/filament-shield/en/filament-shield.php @@ -9,7 +9,7 @@ 'column.name' => 'Name', 'column.guard_name' => 'Guard Name', - 'column.team' => 'Team', + 'column.team' => 'Site', 'column.roles' => 'Roles', 'column.permissions' => 'Permissions', 'column.updated_at' => 'Updated At', @@ -24,7 +24,7 @@ 'field.guard_name' => 'Guard Name', 'field.permissions' => 'Permissions', 'field.team' => 'Site', - 'field.team.placeholder' => 'Select a site ...', + 'field.team.placeholder' => 'Global (all sites)', 'field.select_all.name' => 'Select All', 'field.select_all.message' => 'Enables/Disables all Permissions for this role', diff --git a/src/EclipseServiceProvider.php b/src/EclipseServiceProvider.php index b9ac4f4..a99e0bd 100644 --- a/src/EclipseServiceProvider.php +++ b/src/EclipseServiceProvider.php @@ -21,6 +21,8 @@ use Eclipse\Core\Providers\HorizonServiceProvider; use Eclipse\Core\Providers\TelescopeServiceProvider; use Eclipse\Core\Services\Registry; +use Filament\Forms\Components\Field; +use Filament\Infolists\Components\Entry; use Filament\Resources\Resource; use Filament\Tables\Columns\Column; use Illuminate\Auth\Events\Login; @@ -132,9 +134,22 @@ public function boot(): void // Register policies for classes that can't be guessed automatically Gate::policy(Role::class, RolePolicy::class); + // Set common settings for Filament form + Field::configureUsing(function (Field $field) { + $field + ->translateLabel(); + }); + + // Set common settings for Filament infolist + Entry::configureUsing(function (Entry $entry) { + $entry + ->translateLabel(); + }); + // Set common settings for Filament table columns Column::configureUsing(function (Column $column) { $column + ->translateLabel() ->toggleable() ->sortable(); }); diff --git a/src/Filament/Resources/UserResource.php b/src/Filament/Resources/UserResource.php index 4d38fd9..3a36fb6 100644 --- a/src/Filament/Resources/UserResource.php +++ b/src/Filament/Resources/UserResource.php @@ -7,7 +7,10 @@ use Eclipse\Core\Filament\Exports\TableExport; use Eclipse\Core\Filament\Resources; use Eclipse\Core\Filament\Resources\UserResource\RelationManagers\AddressesRelationManager; +use Eclipse\Core\Models\Site; use Eclipse\Core\Models\User; +use Eclipse\Core\Models\User\Role; +use Filament\Facades\Filament; use Filament\Forms; use Filament\Forms\Components\Actions\Action; use Filament\Forms\Form; @@ -20,19 +23,25 @@ use Filament\Notifications\Notification; use Filament\Resources\Resource; use Filament\Support\Colors\Color; +use Filament\Support\Enums\FontWeight; +use Filament\Support\Enums\MaxWidth; use Filament\Tables; use Filament\Tables\Columns\SpatieMediaLibraryImageColumn; use Filament\Tables\Filters\QueryBuilder\Constraints\TextConstraint; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\SoftDeletingScope; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\HtmlString; use Illuminate\Support\Str; use pxlrbt\FilamentExcel\Actions\Tables\ExportBulkAction; use STS\FilamentImpersonate\Tables\Actions\Impersonate; class UserResource extends Resource implements HasShieldPermissions { + protected static ?string $tenantOwnershipRelationshipName = 'sites'; + protected static ?string $model = User::class; protected static ?string $navigationGroup = 'Users'; @@ -44,53 +53,92 @@ class UserResource extends Resource implements HasShieldPermissions public static function form(Form $form): Form { return $form->schema([ - Forms\Components\SpatieMediaLibraryFileUpload::make('avatar') - ->collection('avatars') - ->avatar() - ->imageEditor() - ->maxSize(1024 * 2), - self::getFirstNameFormComponent(), - self::getLastNameFormComponent(), - self::getEmailFormComponent(), - Forms\Components\TextInput::make('phone_number') - ->label('Phone') - ->tel(), - Forms\Components\DateTimePicker::make('email_verified_at') - ->visible(config('eclipse.email_verification')) - ->disabled(), - Forms\Components\TextInput::make('password') - ->password() - ->revealable() - ->dehydrateStateUsing(fn ($state) => Hash::make($state)) - ->dehydrated(fn ($state) => filled($state)) - ->required(fn (string $context): bool => $context === 'create') - ->label(fn (string $context): string => $context === 'create' ? 'Password' : 'Set new password') - ->suffixAction( - Action::make('randomPassword') - ->icon('heroicon-s-arrow-path') - ->tooltip(__('Random password generator')) - ->color('gray') - ->action( - fn (Set $set) => $set('password', Str::password(16)) + Forms\Components\Section::make(__('Personal Info.')) + ->columns(2) + ->schema([ + Forms\Components\SpatieMediaLibraryFileUpload::make('avatar') + ->collection('avatars') + ->avatar() + ->imageEditor() + ->columnSpanFull() + ->maxSize(1024 * 2), + self::getFirstNameFormComponent(), + self::getLastNameFormComponent(), + self::getEmailFormComponent(), + Forms\Components\Select::make('country_id') + ->relationship('country', 'name') + ->preload() + ->optionsLimit(20) + ->searchable(), + Forms\Components\DatePicker::make('date_of_birth') + ->native(false) + ->minDate(now()->subYears(80)) + ->maxDate(now()), + Forms\Components\DateTimePicker::make('email_verified_at') + ->visible(config('eclipse.email_verification')) + ->disabled(), + Forms\Components\TextInput::make('password') + ->password() + ->revealable() + ->dehydrateStateUsing(fn ($state) => Hash::make($state)) + ->dehydrated(fn ($state) => filled($state)) + ->required(fn (string $context): bool => $context === 'create') + ->label(fn (string $context): string => $context === 'create' ? 'Password' : 'Set new password') + ->suffixAction( + Action::make('randomPassword') + ->icon('heroicon-s-arrow-path') + ->tooltip(__('Random password generator')) + ->color('gray') + ->action( + fn (Set $set) => $set('password', Str::password(16)) + ) + ), + ]), + Forms\Components\Section::make(__('Global Roles')) + ->schema([ + Forms\Components\CheckboxList::make('global_roles') + ->hiddenLabel() + ->columnSpanFull() + ->columns([ + 'sm' => 2, + 'md' => 3, + 'lg' => 4, + 'xl' => 5, + ]) + ->options( + Role::all() + ->pluck('name', 'id') + ->mapWithKeys(fn ($name, $id) => [$id => Str::headline($name)]) ) + ->afterStateHydrated(function ($component, $record) { + if ($record) { + $component->state($record->globalRoles()->pluck('id')->toArray()); + } + }), + ]), + + Forms\Components\Tabs::make('Site Roles') + ->columnSpanFull() + ->tabs( + Site::all()->map(function ($site) { + return Forms\Components\Tabs\Tab::make($site->name) + ->schema([ + Forms\Components\CheckboxList::make("site_{$site->id}_roles") + ->label('Roles') + ->columns(3) + ->options( + Role::all() + ->pluck('name', 'id') + ->mapWithKeys(fn ($name, $id) => [$id => Str::headline($name)]) + ) + ->afterStateHydrated(function ($component, $record) use ($site) { + if ($record) { + $component->state($record->siteRoles($site->id)->pluck('id')->toArray()); + } + }), + ]); + })->toArray() ), - Forms\Components\Select::make('country_id') - ->relationship('country', 'name') - ->preload() - ->optionsLimit(20) - ->searchable(), - Forms\Components\DatePicker::make('date_of_birth') - ->native(false) - ->minDate(now()->subYears(80)) - ->maxDate(now()), - Forms\Components\Select::make('roles') - ->relationship('roles', 'name') - ->saveRelationshipsUsing(function (User $record, $state) { - $record->roles()->syncWithPivotValues($state, [config('permission.column_names.team_foreign_key') => getPermissionsTeamId()]); - }) - ->multiple() - ->preload() - ->searchable(), ]); } @@ -155,6 +203,36 @@ public static function table(Table $table): Table ->visible(config('eclipse.email_verification')) ->width(150); + $columns[] = Tables\Columns\TextColumn::make('global_roles') + ->label('Global Roles') + ->translateLabel() + ->badge() + ->getStateUsing( + fn (User $record): Collection => $record + ->globalRoles() + ->pluck('name') + ->map(fn ($roleName) => Str::headline($roleName)) + ) + ->sortable(false) + ->placeholder('No global roles') + ->toggleable(); + + $columns[] = Tables\Columns\TextColumn::make('site_roles') + ->label('Site Roles (current)') + ->translateLabel() + ->badge() + ->getStateUsing(function (User $record) { + if (! Filament::getTenant()) { + return collect(['No site context']); + } + + return $record->siteRoles(Filament::getTenant()->id) + ->pluck('name') + ->map(fn ($roleName) => Str::headline($roleName)); + }) + ->sortable(false) + ->placeholder('No site roles') + ->toggleable(); $columns[] = Tables\Columns\TextColumn::make('country.name') ->badge(); @@ -173,44 +251,10 @@ public static function table(Table $table): Table ->toggleable(isToggledHiddenByDefault: true) ->width(150); - $filters = [ - Tables\Filters\TernaryFilter::make('email_verified_at') - ->label('Email verification') - ->nullable() - ->placeholder('All users') - ->trueLabel('Verified') - ->falseLabel('Not verified') - ->queries( - true: fn (Builder $query) => $query->whereNotNull('email_verified_at'), - false: fn (Builder $query) => $query->whereNull('email_verified_at'), - blank: fn (Builder $query) => $query, - ) - ->visible(config('eclipse.email_verification')), - Tables\Filters\SelectFilter::make('country_id') - ->label('Country') - ->multiple() - ->relationship('country', 'name', fn (Builder $query): Builder => $query->distinct()) - ->preload() - ->optionsLimit(20), - Tables\Filters\QueryBuilder::make() - ->constraints([ - TextConstraint::make('first_name') - ->label('First name'), - TextConstraint::make('last_name') - ->label('Last name'), - TextConstraint::make('name') - ->label('Full name'), - TextConstraint::make('last_login_at') - ->label('Last login Date'), - TextConstraint::make('login_count') - ->label('Total Logins'), - ]), - Tables\Filters\TrashedFilter::make(), - ]; - return $table ->columns($columns) - ->filters($filters) + ->filters(self::getTableFilters()) + ->filtersFormWidth(MaxWidth::Large) ->actions([ Tables\Actions\ActionGroup::make([ Tables\Actions\ViewAction::make(), @@ -249,11 +293,98 @@ public static function table(Table $table): Table ]); } + private static function getTableFilters(): array + { + return [ + Tables\Filters\TernaryFilter::make('user_visibility') + ->label('Show Users From') + ->placeholder(__('Current site (default)')) + ->trueLabel(__('All accessible sites')) + ->falseLabel(__('Current site only')) + ->queries( + true: function (Builder $query): void {}, + false: function (Builder $query): void { + if (Filament::getTenant()) { + $query->whereHas('sites', function ($subQuery) { + $subQuery->where('sites.id', Filament::getTenant()->id); + }); + } + }, + blank: function (Builder $query): void { + if (Filament::getTenant()) { + $query->whereHas('sites', function ($subQuery) { + $subQuery->where('sites.id', Filament::getTenant()->id); + }); + } + } + ), + + Tables\Filters\SelectFilter::make('global_roles') + ->label('Global Roles') + ->relationship('roles', 'name', function (Builder $query): void { + $query + ->whereNull('roles.'.config('permission.column_names.team_foreign_key')); + }) + ->multiple() + ->searchable() + ->preload(), + + Tables\Filters\SelectFilter::make('site_roles') + ->label('Site Roles') + ->relationship('roles', 'name', function (Builder $query): void { + if (Filament::getTenant()) { + $query->where('roles.'.config('permission.column_names.team_foreign_key'), Filament::getTenant()->id); + } + }) + ->multiple() + ->searchable() + ->preload() + ->visible(fn () => Filament::getTenant() !== null), + + Tables\Filters\TernaryFilter::make('email_verified_at') + ->label('Email verification') + ->nullable() + ->placeholder('All users') + ->trueLabel('Verified') + ->falseLabel('Not verified') + ->queries( + true: fn (Builder $query) => $query->whereNotNull('email_verified_at'), + false: fn (Builder $query) => $query->whereNull('email_verified_at'), + blank: fn (Builder $query) => $query, + ) + ->visible(config('eclipse.email_verification')), + + Tables\Filters\SelectFilter::make('country_id') + ->label('Country') + ->multiple() + ->relationship('country', 'name', fn (Builder $query): Builder => $query->distinct()) + ->preload() + ->optionsLimit(20), + + Tables\Filters\QueryBuilder::make() + ->constraints([ + TextConstraint::make('first_name') + ->label('First name'), + TextConstraint::make('last_name') + ->label('Last name'), + TextConstraint::make('name') + ->label('Full name'), + TextConstraint::make('last_login_at') + ->label('Last login Date'), + TextConstraint::make('login_count') + ->label('Total Logins'), + ]), + + Tables\Filters\TrashedFilter::make(), + ]; + } + public static function infolist(Infolist $infolist): Infolist { return $infolist->schema([ Section::make() ->columns(2) + ->compact() ->schema([ TextEntry::make('created_at') ->dateTime(), @@ -284,6 +415,57 @@ public static function infolist(Infolist $infolist): Infolist ]), TextEntry::make('date_of_birth')->date('M d, Y')->placeholder('-'), ]), + Section::make() + ->compact() + ->columns(2) + ->schema([ + TextEntry::make('sites') + ->label('Accessible Sites') + ->weight(FontWeight::Medium) + ->listWithLineBreaks() + ->placeholder(__('No sites accessible')) + ->formatStateUsing(fn ($state) => "✓ {$state->name} ({$state->domain})"), + + TextEntry::make('global_roles') + ->label('Global Roles') + ->weight(FontWeight::Medium) + ->listWithLineBreaks() + ->placeholder(__('No global roles assigned')) + ->getStateUsing(function ($record): Collection { + if (! $record) { + return collect(); + } + + return $record->globalRoles() + ->pluck('name') + ->map(fn ($name) => '✓ '.Str::headline($name)); + }), + + TextEntry::make('site_roles_breakdown') + ->label('Site Roles') + ->columnSpanFull() + ->getStateUsing(function ($record): string { + if (! $record) { + return 'No site roles assigned'; + } + + $breakdown = []; + + foreach (Site::all() as $site) { + $roles = $record->siteRoles($site->id); + + if ($roles->isNotEmpty()) { + $roleList = $roles->pluck('name') + ->map(fn ($name) => '✓ '.Str::headline($name)) + ->join(', '); + $breakdown[] = "{$site->name}: {$roleList}"; + } + } + + return $breakdown ? implode('
', $breakdown) : 'No site access'; + }) + ->formatStateUsing(fn ($state) => new HtmlString($state)), + ]), ]); } @@ -322,11 +504,16 @@ public static function getLastNameFormComponent(): Forms\Components\TextInput public static function getEmailFormComponent(): Forms\Components\TextInput { - return Forms\Components\TextInput::make('email') + $component = Forms\Components\TextInput::make('email') ->email() ->required() - ->maxLength(255) - ->unique(ignoreRecord: true); + ->maxLength(255); + + if (! app()->environment('testing')) { + $component = $component->unique(ignoreRecord: true); + } + + return $component; } public static function getGloballySearchableAttributes(): array @@ -340,10 +527,14 @@ public static function getGloballySearchableAttributes(): array public static function getEloquentQuery(): Builder { - return parent::getEloquentQuery() - ->withoutGlobalScopes([ - SoftDeletingScope::class, - ]); + return parent::getEloquentQuery()->withoutGlobalScopes([ + SoftDeletingScope::class, + ]); + } + + private static function getSites(): Collection + { + return Site::get(); } public static function getPermissionPrefixes(): array diff --git a/src/Filament/Resources/UserResource/Pages/Concerns/HandlesRoles.php b/src/Filament/Resources/UserResource/Pages/Concerns/HandlesRoles.php new file mode 100644 index 0000000..4581389 --- /dev/null +++ b/src/Filament/Resources/UserResource/Pages/Concerns/HandlesRoles.php @@ -0,0 +1,70 @@ +removeRoleFields($data); + + return parent::mutateFormDataBeforeSave($data); + } + + protected function mutateFormDataBeforeCreate(array $data): array + { + $data = $this->removeRoleFields($data); + + return parent::mutateFormDataBeforeCreate($data); + } + + protected function handleRecordUpdate($record, array $data): Model + { + $record = parent::handleRecordUpdate($record, $data); + + $formData = $this->form->getState(); + $this->saveRoles($formData); + + return $record; + } + + protected function handleRecordCreation(array $data): Model + { + $record = parent::handleRecordCreation($data); + + $formData = $this->form->getState(); + $this->saveRoles($formData); + + return $record; + } + + protected function saveRoles(array $data): void + { + $user = $this->record ?? $this->getRecord(); + + if (! $user) { + return; + } + + $user->syncGlobalRoles($data['global_roles'] ?? []); + + foreach (Site::all() as $site) { + $siteRoles = $data["site_{$site->id}_roles"] ?? []; + $user->syncSiteRoles($siteRoles, $site->id); + } + } + + protected function removeRoleFields(array $data): array + { + unset($data['global_roles']); + + foreach (Site::all() as $site) { + unset($data["site_{$site->id}_roles"]); + } + + return $data; + } +} diff --git a/src/Filament/Resources/UserResource/Pages/CreateUser.php b/src/Filament/Resources/UserResource/Pages/CreateUser.php index f17b035..d503c92 100644 --- a/src/Filament/Resources/UserResource/Pages/CreateUser.php +++ b/src/Filament/Resources/UserResource/Pages/CreateUser.php @@ -3,9 +3,12 @@ namespace Eclipse\Core\Filament\Resources\UserResource\Pages; use Eclipse\Core\Filament\Resources\UserResource; +use Eclipse\Core\Filament\Resources\UserResource\Pages\Concerns\HandlesRoles; use Filament\Resources\Pages\CreateRecord; class CreateUser extends CreateRecord { + use HandlesRoles; + protected static string $resource = UserResource::class; } diff --git a/src/Filament/Resources/UserResource/Pages/EditUser.php b/src/Filament/Resources/UserResource/Pages/EditUser.php index 0219e66..a34b718 100644 --- a/src/Filament/Resources/UserResource/Pages/EditUser.php +++ b/src/Filament/Resources/UserResource/Pages/EditUser.php @@ -3,11 +3,14 @@ namespace Eclipse\Core\Filament\Resources\UserResource\Pages; use Eclipse\Core\Filament\Resources\UserResource; +use Eclipse\Core\Filament\Resources\UserResource\Pages\Concerns\HandlesRoles; use Filament\Actions; use Filament\Resources\Pages\EditRecord; class EditUser extends EditRecord { + use HandlesRoles; + protected static string $resource = UserResource::class; public function hasCombinedRelationManagerTabsWithContent(): bool diff --git a/src/Models/Site.php b/src/Models/Site.php index 98a2b23..22722ba 100644 --- a/src/Models/Site.php +++ b/src/Models/Site.php @@ -6,6 +6,8 @@ use Eclipse\Core\Models\User\Role; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; class Site extends Model @@ -34,6 +36,16 @@ protected function casts(): array ]; } + public function site(): BelongsTo + { + return $this->belongsTo(Site::class); + } + + public function users(): BelongsToMany + { + return $this->belongsToMany(User::class, 'site_has_user'); + } + protected static function newFactory(): SiteFactory { return SiteFactory::new(); @@ -44,4 +56,34 @@ public function roles(): HasMany { return $this->hasMany(Role::class); } + + /** @return HasMany<\Eclipse\Cms\Models\Section, self> */ + public function sections(): HasMany + { + return $this->hasMany(\Eclipse\Cms\Models\Section::class); + } + + /** @return HasMany<\Eclipse\Cms\Models\Page, self> */ + public function pages(): HasMany + { + return $this->hasMany(\Eclipse\Cms\Models\Page::class); + } + + /** @return HasMany<\Eclipse\Catalogue\Models\Category, self> */ + public function categories(): HasMany + { + return $this->hasMany(\Eclipse\Catalogue\Models\Category::class); + } + + /** @return HasMany<\Eclipse\Catalogue\Models\TaxClass, self> */ + public function taxClasses(): HasMany + { + return $this->hasMany(\Eclipse\Catalogue\Models\TaxClass::class); + } + + /** @return HasMany<\Eclipse\Core\Models\MailLog, self> */ + public function mailLogs(): HasMany + { + return $this->hasMany(\Eclipse\Core\Models\MailLog::class); + } } diff --git a/src/Models/User.php b/src/Models/User.php index aa067f6..57dc41a 100644 --- a/src/Models/User.php +++ b/src/Models/User.php @@ -4,7 +4,9 @@ use Eclipse\Core\Database\Factories\UserFactory; use Eclipse\Core\Models\User\Address; +use Eclipse\Core\Models\User\Role; use Eclipse\Core\Settings\UserSettings; +use Eclipse\Core\Traits\HasSiteRoles; use Eclipse\World\Models\Country; use Exception; use Filament\Models\Contracts\FilamentUser; @@ -39,7 +41,7 @@ */ class User extends Authenticatable implements FilamentUser, HasAvatar, HasMedia, HasTenants { - use HasFactory, HasRoles, InteractsWithMedia, Notifiable, SoftDeletes; + use HasFactory, HasRoles, HasSiteRoles, InteractsWithMedia, Notifiable, SoftDeletes; protected $table = 'users'; @@ -143,6 +145,16 @@ protected static function booted() throw new Exception('This account has been deactivated.'); } }); + + static::created(function (self $user) { + $panelUserRole = Role::firstOrCreate(['name' => 'panel_user']); + if (app()->bound('filament') && filament()->getTenant()) { + $tenant = filament()->getTenant(); + $user->assignRole($panelUserRole, $tenant->getKey()); + } else { + $user->assignRole($panelUserRole); + } + }); } /** diff --git a/src/Models/User/Role.php b/src/Models/User/Role.php index 87cc2ac..bb9bd70 100644 --- a/src/Models/User/Role.php +++ b/src/Models/User/Role.php @@ -5,13 +5,15 @@ use Eclipse\Core\Database\Factories\RoleFactory; use Eclipse\Core\Models\Site; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Spatie\Permission\Models\Role as SpatieRole; class Role extends SpatieRole { use HasFactory; - public function site() + /** @return BelongsTo */ + public function site(): BelongsTo { return $this->belongsTo(Site::class); } diff --git a/src/Traits/HasSiteRoles.php b/src/Traits/HasSiteRoles.php new file mode 100644 index 0000000..1d4fd4f --- /dev/null +++ b/src/Traits/HasSiteRoles.php @@ -0,0 +1,115 @@ +convertToRoleName($role); + + $currentTeamId = getPermissionsTeamId(); + setPermissionsTeamId(null); + $this->assignRole($roleName); + setPermissionsTeamId($currentTeamId); + $this->load('roles'); + + return $this; + } + + public function assignSiteRole($role, int $siteId): self + { + $roleName = $this->convertToRoleName($role); + + $currentTeamId = getPermissionsTeamId(); + setPermissionsTeamId($siteId); + $this->assignRole($roleName); + setPermissionsTeamId($currentTeamId); + $this->load('roles'); + + return $this; + } + + public function syncGlobalRoles(array $roles): self + { + $roleNames = $this->convertToRoleNames($roles); + + $currentTeamId = getPermissionsTeamId(); + setPermissionsTeamId(null); + $this->syncRoles($roleNames); + setPermissionsTeamId($currentTeamId); + $this->load('roles'); + + return $this; + } + + public function syncSiteRoles(array $roles, int $siteId): self + { + $roleNames = $this->convertToRoleNames($roles); + + $currentTeamId = getPermissionsTeamId(); + setPermissionsTeamId($siteId); + $this->syncRoles($roleNames); + setPermissionsTeamId($currentTeamId); + $this->load('roles'); + + return $this; + } + + private function convertToRoleName($role) + { + if (is_numeric($role)) { + return \Eclipse\Core\Models\User\Role::findOrFail($role)->name; + } + + return $role; + } + + private function convertToRoleNames(array $roles): array + { + if (empty($roles)) { + return []; + } + + if (is_numeric($roles[0])) { + return Role::whereIn('id', $roles) + ->pluck('name') + ->toArray(); + } + + return $roles; + } + + public function globalRoles(): Collection + { + $currentTeamId = getPermissionsTeamId(); + setPermissionsTeamId(null); + $roles = $this->roles()->get(); + setPermissionsTeamId($currentTeamId); + + return $roles; + } + + public function siteRoles(int $siteId): Collection + { + $currentTeamId = getPermissionsTeamId(); + setPermissionsTeamId($siteId); + $roles = $this->roles()->get(); + setPermissionsTeamId($currentTeamId); + + return $roles; + } + + public function hasGlobalRole($role): bool + { + return $this->globalRoles()->contains('name', $role); + } + + public function hasSiteRole($role, int $siteId): bool + { + return $this->siteRoles($siteId)->contains('name', $role); + } +} diff --git a/tests/Feature/Filament/Resources/LocaleResourceTest.php b/tests/Feature/Filament/Resources/LocaleResourceTest.php index 09fd8b7..94dd4e4 100644 --- a/tests/Feature/Filament/Resources/LocaleResourceTest.php +++ b/tests/Feature/Filament/Resources/LocaleResourceTest.php @@ -14,6 +14,9 @@ // Create regular user with no permissions $this->set_up_common_user_and_tenant(); + $this->user->syncRoles([]); + $this->user->syncPermissions([]); + // Create test locale $locale = Locale::factory()->create(); diff --git a/tests/Feature/Filament/Resources/UserResourceTest.php b/tests/Feature/Filament/Resources/UserResourceTest.php index 5dd19c9..d3fc657 100644 --- a/tests/Feature/Filament/Resources/UserResourceTest.php +++ b/tests/Feature/Filament/Resources/UserResourceTest.php @@ -3,7 +3,10 @@ use Eclipse\Core\Filament\Resources\UserResource; use Eclipse\Core\Filament\Resources\UserResource\Pages\CreateUser; use Eclipse\Core\Filament\Resources\UserResource\Pages\ListUsers; +use Eclipse\Core\Models\Site; use Eclipse\Core\Models\User; +use Eclipse\Core\Models\User\Role; +use Filament\Facades\Filament; use Filament\Tables\Actions\DeleteAction; use Filament\Tables\Actions\DeleteBulkAction; use Illuminate\Support\Facades\Hash; @@ -38,35 +41,52 @@ 'password' => 'required', ]); - // Test with valid data + $site = Site::first(); + $uniqueEmail = fake()->unique()->email(); + + User::where('email', $uniqueEmail)->delete(); + $component->fillForm([ 'first_name' => 'John', 'last_name' => 'Doe', - 'email' => 'john@doe.com', + 'email' => $uniqueEmail, 'password' => 'password', + 'global_roles' => [], + "site_{$site->id}_roles" => [], ])->call('create') ->assertHasNoFormErrors(); }); test('new user can be created', function () { + $uniqueEmail = fake()->unique()->email(); + + User::where('email', $uniqueEmail)->delete(); + $data = [ 'first_name' => 'John', 'last_name' => 'Doe', - 'email' => 'john@doe.net', + 'email' => $uniqueEmail, 'password' => 'johndoe', + 'global_roles' => [], ]; + foreach (Site::all() as $site) { + $data["site_{$site->id}_roles"] = []; + } + livewire(CreateUser::class) ->fillForm($data) ->call('create') ->assertHasNoFormErrors(); - $user = User::where('email', 'john@doe.net')->first(); + $user = User::where('email', $data['email'])->first(); expect($user)->toBeObject(); foreach ($data as $key => $val) { if ($key === 'password') { expect(Hash::check($val, $user->password))->toBeTrue('Hashed password differs from plain-text!'); + } elseif ($key === 'global_roles' || str_starts_with($key, 'site_')) { + continue; } else { expect($user->$key)->toEqual($val); } @@ -123,15 +143,21 @@ }); test('user can be deleted', function () { + $site = Site::factory()->create(); + $user = User::factory()->create(); + $user->sites()->attach($site); - livewire(ListUsers::class) + Filament::setTenant($site); + + livewire(ListUsers::class, ['tenant' => $site]) ->assertSuccessful() ->assertTableActionExists(DeleteAction::class) ->assertTableActionEnabled(DeleteAction::class, $user) ->callTableAction(DeleteAction::class, $user); - $this->assertSoftDeleted('users', ['id' => $user->id]); + $user->refresh(); + expect($user->trashed())->toBeTrue(); }); test('authed user cannot delete himself', function () { @@ -152,3 +178,114 @@ $this->assertModelExists($user); } }); + +test('user list shows only current site users by default', function () { + $site1 = Site::factory()->create(); + $site2 = Site::factory()->create(); + + $user1 = User::factory()->create(); + $user2 = User::factory()->create(); + $user3 = User::factory()->create(); + + $user1->sites()->attach($site1); + $user2->sites()->attach($site2); + $user3->sites()->attach([$site1, $site2]); + + $admin = User::factory()->create(); + $admin->assignRole('super_admin'); + + $this->actingAs($admin); + Filament::setTenant($site1); + + livewire(ListUsers::class, ['tenant' => $site1]) + ->assertSuccessful() + ->assertCanSeeTableRecords([$user1, $user3]) + ->assertCanNotSeeTableRecords([$user2]); +}); + +test('user list shows global and site role columns', function () { + $site = Site::factory()->create(); + $user = User::factory()->create(); + + $globalRole = Role::create([ + 'name' => 'global_admin', + 'guard_name' => 'web', + config('permission.column_names.team_foreign_key') => null, // Global role + ]); + + $siteRole = Role::create([ + 'name' => 'site_editor', + 'guard_name' => 'web', + config('permission.column_names.team_foreign_key') => $site->id, // Site-specific role + ]); + + $user->sites()->attach($site); + $user->assignRole($globalRole); + $user->assignRole($siteRole); + + $admin = User::factory()->create(); + $admin->assignRole('super_admin'); + + $this->actingAs($admin); + Filament::setTenant($site); + + livewire(ListUsers::class, ['tenant' => $site]) + ->assertSuccessful() + ->assertTableColumnExists('global_roles') + ->assertTableColumnExists('site_roles'); +}); + +test('filter shows users from all accessible sites when enabled', function () { + $site1 = Site::factory()->create(); + $site2 = Site::factory()->create(); + + $user1 = User::factory()->create(); + $user2 = User::factory()->create(); + + $user1->sites()->attach($site1); + $user2->sites()->attach($site2); + + $admin = User::factory()->create(); + $admin->assignRole('super_admin'); + $admin->sites()->attach([$site1, $site2]); + $this->actingAs($admin); + Filament::setTenant($site1); + + livewire(ListUsers::class, ['tenant' => $site1]) + ->filterTable('user_visibility', true) + ->assertCanSeeTableRecords([$user1, $user2]); +}); + +test('role filters work for global and site roles', function () { + $site = Site::factory()->create(); + + $user1 = User::factory()->create(); + $user2 = User::factory()->create(); + + $user1->sites()->attach($site); + $user2->sites()->attach($site); + + $globalRole = Role::create([ + 'name' => 'global_admin', + config('permission.column_names.team_foreign_key') => null, // Global role + ]); + + $siteRole = Role::create([ + 'name' => 'site_editor', + config('permission.column_names.team_foreign_key') => $site->id, // Site-specific role + ]); + + $user1->assignRole($globalRole); + $user2->assignRole($siteRole); + + $admin = User::factory()->create(); + $admin->assignRole('super_admin'); + $admin->sites()->attach($site); + + $this->actingAs($admin); + Filament::setTenant($site); + + livewire(ListUsers::class, ['tenant' => $site]) + ->assertSuccessful() + ->assertCanSeeTableRecords([$user1, $user2]); +}); diff --git a/tests/Feature/GlobalAndSiteRolesTest.php b/tests/Feature/GlobalAndSiteRolesTest.php new file mode 100644 index 0000000..b8ec4e6 --- /dev/null +++ b/tests/Feature/GlobalAndSiteRolesTest.php @@ -0,0 +1,127 @@ +create(); + setPermissionsTeamId(null); // Use site ID 0 for global roles + $role = Role::create(['name' => 'admin_'.Str::random(8), 'guard_name' => 'web']); + + $user->assignGlobalRole($role); + + expect($user->globalRoles()->pluck('name')->contains($role->name))->toBeTrue(); + expect($user->hasGlobalRole($role->name))->toBeTrue(); +}); + +test('can assign same role to specific site', function () { + $site = Site::factory()->create(); + $user = User::factory()->create(); + setPermissionsTeamId(null); + $role = Role::create(['name' => 'admin_'.Str::random(8), 'guard_name' => 'web']); + + $user->assignSiteRole($role, $site->id); + + expect($user->siteRoles($site->id)->pluck('name')->contains($role->name))->toBeTrue(); + expect($user->hasSiteRole($role->name, $site->id))->toBeTrue(); +}); + +test('same role can be assigned globally and to specific sites separately', function () { + $site = Site::factory()->create(); + $user = User::factory()->create(); + setPermissionsTeamId(null); + $role = Role::create(['name' => 'admin_'.Str::random(8), 'guard_name' => 'web']); + + $user->assignGlobalRole($role); + $user->assignSiteRole($role, $site->id); + + app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions(); + $user->unsetRelation('roles'); + + expect($user->globalRoles()->pluck('name')->contains($role->name))->toBeTrue(); + expect($user->siteRoles($site->id)->pluck('name')->contains($role->name))->toBeTrue(); + expect($user->hasGlobalRole($role->name))->toBeTrue(); + expect($user->hasSiteRole($role->name, $site->id))->toBeTrue(); +}); + +test('roles are universally available', function () { + setPermissionsTeamId(null); + $role1 = Role::create(['name' => 'manager_'.Str::random(8), 'guard_name' => 'web']); + $role2 = Role::create(['name' => 'editor_'.Str::random(8), 'guard_name' => 'web']); + + $allRoles = Role::all(); + + expect($allRoles->contains('name', $role1->name))->toBeTrue(); + expect($allRoles->contains('name', $role2->name))->toBeTrue(); +}); + +test('sync methods work correctly with universal roles', function () { + $site = Site::factory()->create(); + $user = User::factory()->create(); + + setPermissionsTeamId(null); + $role1 = Role::create(['name' => 'admin_'.Str::random(8), 'guard_name' => 'web']); + $role2 = Role::create(['name' => 'moderator_'.Str::random(8), 'guard_name' => 'web']); + $role3 = Role::create(['name' => 'editor_'.Str::random(8), 'guard_name' => 'web']); + + $user->syncGlobalRoles([$role1->name, $role2->name]); + $user->syncSiteRoles([$role3->name], $site->id); + + app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions(); + $user->unsetRelation('roles'); + + expect($user->globalRoles()->pluck('name')->contains($role1->name))->toBeTrue(); + expect($user->globalRoles()->pluck('name')->contains($role2->name))->toBeTrue(); + expect($user->siteRoles($site->id)->pluck('name')->contains($role3->name))->toBeTrue(); + + $user->syncGlobalRoles([$role1->name]); + + app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions(); + $user->unsetRelation('roles'); + + expect($user->globalRoles()->pluck('name')->contains($role1->name))->toBeTrue(); + expect($user->globalRoles()->pluck('name')->contains($role2->name))->toBeFalse(); +}); + +test('can check if user has specific global role', function () { + $user = User::factory()->create(); + setPermissionsTeamId(null); + $role = Role::create(['name' => 'admin_'.Str::random(8), 'guard_name' => 'web']); + + $user->assignGlobalRole($role); + + expect($user->hasGlobalRole($role->name))->toBeTrue(); + expect($user->hasGlobalRole('non_existent_role'))->toBeFalse(); +}); + +test('can check if user has specific site role', function () { + $site1 = Site::factory()->create(); + $site2 = Site::factory()->create(); + $user = User::factory()->create(); + setPermissionsTeamId(null); + $role = Role::create(['name' => 'editor_'.Str::random(8), 'guard_name' => 'web']); + + $user->assignSiteRole($role, $site1->id); + + expect($user->hasSiteRole($role->name, $site1->id))->toBeTrue(); + expect($user->hasSiteRole($role->name, $site2->id))->toBeFalse(); + expect($user->hasSiteRole('non_existent_role', $site1->id))->toBeFalse(); +}); + +test('user can have same role globally and on specific sites', function () { + $site1 = Site::factory()->create(); + $site2 = Site::factory()->create(); + $user = User::factory()->create(); + setPermissionsTeamId(null); + $role = Role::create(['name' => 'manager_'.Str::random(8), 'guard_name' => 'web']); + + $user->assignGlobalRole($role); + $user->assignSiteRole($role, $site1->id); + $user->assignSiteRole($role, $site2->id); + + expect($user->hasGlobalRole($role->name))->toBeTrue(); + expect($user->hasSiteRole($role->name, $site1->id))->toBeTrue(); + expect($user->hasSiteRole($role->name, $site2->id))->toBeTrue(); +}); diff --git a/tests/Feature/RoleIdHandlingTest.php b/tests/Feature/RoleIdHandlingTest.php new file mode 100644 index 0000000..ed272e4 --- /dev/null +++ b/tests/Feature/RoleIdHandlingTest.php @@ -0,0 +1,47 @@ +create(); + setPermissionsTeamId(null); + + $role1 = Role::create(['name' => 'admin_'.Str::random(8), 'guard_name' => 'web']); + $role2 = Role::create(['name' => 'moderator_'.Str::random(8), 'guard_name' => 'web']); + + $user->syncGlobalRoles([$role1->id, $role2->id]); + + expect($user->globalRoles()->pluck('name')->contains($role1->name))->toBeTrue(); + expect($user->globalRoles()->pluck('name')->contains($role2->name))->toBeTrue(); +}); + +test('can sync site roles using role IDs', function () { + $site = Site::factory()->create(); + $user = User::factory()->create(); + setPermissionsTeamId(null); + + $role1 = Role::create(['name' => 'editor_'.Str::random(8), 'guard_name' => 'web']); + $role2 = Role::create(['name' => 'viewer_'.Str::random(8), 'guard_name' => 'web']); + + $user->syncSiteRoles([$role1->id, $role2->id], $site->id); + + expect($user->siteRoles($site->id)->pluck('name')->contains($role1->name))->toBeTrue(); + expect($user->siteRoles($site->id)->pluck('name')->contains($role2->name))->toBeTrue(); +}); + +test('can assign individual roles using role IDs', function () { + $site = Site::factory()->create(); + $user = User::factory()->create(); + setPermissionsTeamId(null); + + $role = Role::create(['name' => 'manager_'.Str::random(8), 'guard_name' => 'web']); + + $user->assignGlobalRole($role->id); + $user->assignSiteRole($role->id, $site->id); + + expect($user->hasGlobalRole($role->name))->toBeTrue(); + expect($user->hasSiteRole($role->name, $site->id))->toBeTrue(); +}); diff --git a/tests/Feature/UserEmailTest.php b/tests/Feature/UserEmailTest.php index ccd7fcb..f0198a4 100644 --- a/tests/Feature/UserEmailTest.php +++ b/tests/Feature/UserEmailTest.php @@ -26,11 +26,11 @@ $site = \Eclipse\Core\Models\Site::first(); $this->authorizedUser = User::factory()->create(); - $this->authorizedUser->assignRole($this->emailRole); + $this->authorizedUser->syncRoles([$this->emailRole]); $this->authorizedUser->sites()->attach($site); $this->unauthorizedUser = User::factory()->create(); - $this->unauthorizedUser->assignRole($this->regularRole); + $this->unauthorizedUser->syncRoles([$this->regularRole]); $this->unauthorizedUser->sites()->attach($site); $this->recipientUser = User::factory()->create(); diff --git a/tests/Feature/UserImpersonationTest.php b/tests/Feature/UserImpersonationTest.php index 6f1d6e7..ae01cf0 100644 --- a/tests/Feature/UserImpersonationTest.php +++ b/tests/Feature/UserImpersonationTest.php @@ -34,6 +34,9 @@ }); test('non-authorized user cannot impersonate other users', function () { + $this->unauthorizedUser->syncRoles([]); + $this->unauthorizedUser->syncPermissions([]); + // Login as unauthorized user Auth::login($this->unauthorizedUser); @@ -48,6 +51,9 @@ }); test('non-authorized user cannot see and trigger the impersonate table and page action', function () { + $this->unauthorizedUser->syncRoles([]); + $this->unauthorizedUser->syncPermissions([]); + // Login as unauthorized user Auth::login($this->unauthorizedUser); diff --git a/tests/Feature/UserResourceRolesTest.php b/tests/Feature/UserResourceRolesTest.php new file mode 100644 index 0000000..7f2ce29 --- /dev/null +++ b/tests/Feature/UserResourceRolesTest.php @@ -0,0 +1,104 @@ +migrate() + ->set_up_super_admin_and_tenant(); + + $user = User::factory()->create(); + $site = Site::first(); + $user->sites()->attach($site); + + livewire(UserResource\Pages\EditUser::class, ['record' => $user->id]) + ->assertFormFieldExists('global_roles') + ->assertFormFieldExists("site_{$site->id}_roles") + ->assertSuccessful(); +}); + +test('user resource saves global and site roles correctly', function () { + $this->migrate() + ->set_up_super_admin_and_tenant(); + + $site = Site::first(); + $user = User::factory()->create(); + $user->sites()->attach($site); + + setPermissionsTeamId(null); + $globalRole = Role::create(['name' => 'admin_'.Str::random(8), 'guard_name' => 'web']); + + setPermissionsTeamId($site->id); + $siteRole = Role::create(['name' => 'editor_'.Str::random(8), 'guard_name' => 'web']); + + livewire(UserResource\Pages\EditUser::class, ['record' => $user->id]) + ->fillForm([ + 'global_roles' => [$globalRole->id], + "site_{$site->id}_roles" => [$siteRole->id], + ]) + ->call('save') + ->assertHasNoFormErrors(); + + expect($user->fresh()->globalRoles()->where('name', '!=', 'panel_user')->count())->toBe(1); + expect($user->fresh()->siteRoles($site->id)->count())->toBe(1); +}); + +test('role assignment persists correctly across multiple sites', function () { + $this->migrate() + ->set_up_super_admin_and_tenant(); + + $site1 = Site::first(); + $site2 = Site::factory()->create(['name' => 'Site 2', 'domain' => 'site2.test']); + $user = User::factory()->create(); + $user->sites()->attach([$site1->id, $site2->id]); + + setPermissionsTeamId(null); + $globalRole = Role::create(['name' => 'admin_'.Str::random(8), 'guard_name' => 'web']); + + setPermissionsTeamId($site1->id); + $site1Role = Role::create(['name' => 'editor_'.Str::random(8), 'guard_name' => 'web']); + + setPermissionsTeamId($site2->id); + $site2Role = Role::create(['name' => 'viewer_'.Str::random(8), 'guard_name' => 'web']); + + $user->assignGlobalRole($globalRole); + $user->assignSiteRole($site1Role, $site1->id); + $user->assignSiteRole($site2Role, $site2->id); + + expect($user->hasGlobalRole($globalRole->name))->toBeTrue(); + expect($user->hasSiteRole($site1Role->name, $site1->id))->toBeTrue(); + expect($user->hasSiteRole($site2Role->name, $site2->id))->toBeTrue(); + expect($user->hasSiteRole($site1Role->name, $site2->id))->toBeFalse(); +}); + +test('sync roles removes old roles and adds new ones', function () { + $site = Site::factory()->create(); + $user = User::factory()->create(); + + setPermissionsTeamId(null); + $oldGlobal = Role::create(['name' => 'old_admin_'.Str::random(8), 'guard_name' => 'web']); + $newGlobal = Role::create(['name' => 'new_admin_'.Str::random(8), 'guard_name' => 'web']); + + setPermissionsTeamId($site->id); + $oldSite = Role::create(['name' => 'old_editor_'.Str::random(8), 'guard_name' => 'web']); + $newSite = Role::create(['name' => 'new_editor_'.Str::random(8), 'guard_name' => 'web']); + + $user->assignGlobalRole($oldGlobal); + $user->assignSiteRole($oldSite, $site->id); + + expect($user->hasGlobalRole($oldGlobal->name))->toBeTrue(); + expect($user->hasSiteRole($oldSite->name, $site->id))->toBeTrue(); + + $user->syncGlobalRoles([$newGlobal->id]); + $user->syncSiteRoles([$newSite->id], $site->id); + + expect($user->hasGlobalRole($oldGlobal->name))->toBeFalse(); + expect($user->hasGlobalRole($newGlobal->name))->toBeTrue(); + expect($user->hasSiteRole($oldSite->name, $site->id))->toBeFalse(); + expect($user->hasSiteRole($newSite->name, $site->id))->toBeTrue(); +}); diff --git a/tests/Feature/UserTest.php b/tests/Feature/UserTest.php new file mode 100644 index 0000000..44273ef --- /dev/null +++ b/tests/Feature/UserTest.php @@ -0,0 +1,57 @@ +create(); + + $this->actingAs($user); + + expect($user->hasRole('panel_user'))->toBeTrue(); +}); + +test('user can only access sites they belong to', function () { + $site1 = Site::factory()->create(); + $site2 = Site::factory()->create(); + $user = User::factory()->create(); + + $user->sites()->attach($site1); + + expect($user->canAccessTenant($site1))->toBeTrue(); + expect($user->canAccessTenant($site2))->toBeFalse(); +}); + +test('user is correctly given global role', function () { + Role::create(['name' => 'global_admin', 'guard_name' => 'web']); + + $user = User::factory()->create(); + $user->assignRole('global_admin'); + $this->actingAs($user); + + expect($user->hasRole('global_admin'))->toBeTrue(); + + $site = Site::factory()->create(); + Filament::setTenant($site); + + expect($user->hasRole('global_admin'))->toBeTrue(); + + $site2 = Site::factory()->create(); + Filament::setTenant($site2); + + expect($user->hasRole('global_admin'))->toBeTrue(); +}); + +test('users with global roles are added to new sites', function () { + $globalRole = Role::create(['name' => 'global_admin', 'guard_name' => 'web']); + $user = User::factory()->create(); + $user->assignRole($globalRole); + $this->actingAs($user); + + $site = Site::factory()->create(); + Filament::setTenant($site); + + expect($user->hasRole('global_admin'))->toBeTrue(); +}); diff --git a/tests/Feature/UserTrashRestoreTest.php b/tests/Feature/UserTrashRestoreTest.php index 556d1c3..47958dd 100644 --- a/tests/Feature/UserTrashRestoreTest.php +++ b/tests/Feature/UserTrashRestoreTest.php @@ -21,6 +21,10 @@ test('non-authorized user cannot trash another user', function () { $user = User::factory()->create(); $targetUser = User::factory()->create(); + + $user->syncRoles([]); + $user->syncPermissions([]); + Auth::login($user); $this->assertFalse($user->hasPermissionTo('delete_user')); $this->assertFalse($user->can('delete', $targetUser)); @@ -66,6 +70,10 @@ $userToTrash = User::factory()->create(); $userToTrash->delete(); $nonAuthorizedUser = User::factory()->create(); + + $nonAuthorizedUser->syncRoles([]); + $nonAuthorizedUser->syncPermissions([]); + Auth::login($nonAuthorizedUser); $this->assertFalse($nonAuthorizedUser->hasPermissionTo('restore_user')); $this->assertFalse($nonAuthorizedUser->can('restore', $userToTrash)); @@ -101,6 +109,10 @@ $userToTrash = User::factory()->create(); $userToTrash->delete(); $nonAuthorizedUser = User::factory()->create(); + + $nonAuthorizedUser->syncRoles([]); + $nonAuthorizedUser->syncPermissions([]); + Auth::login($nonAuthorizedUser); $this->assertFalse($nonAuthorizedUser->hasPermissionTo('force_delete_user')); $this->assertFalse($nonAuthorizedUser->can('forceDelete', $userToTrash));