From 7b3c664f3f402418251144d9211ae0928d298ab3 Mon Sep 17 00:00:00 2001 From: thapacodes4u Date: Wed, 11 Jun 2025 08:20:21 +0545 Subject: [PATCH 1/9] refactor(tests): update middleware reference to SetupSite --- tests/TestCase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TestCase.php b/tests/TestCase.php index bbfac22..226977c 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -28,7 +28,7 @@ protected function setUp(): void $this->withoutVite(); - // Ensure SetupTenant middleware is applied during tests + // Ensure SetupSite middleware is applied during tests // This is done here since the "withMiddleware" method in workbench/bootstrap/app.php does not seem to work // $this->withMiddleware(SetupTenant::class) also does not work app(Kernel::class)->pushMiddleware(SetupSite::class); From dfd28772400cc97402f45e47ca43f9d074636e51 Mon Sep 17 00:00:00 2001 From: thapacodes4u Date: Thu, 12 Jun 2025 14:26:09 +0545 Subject: [PATCH 2/9] fix: improve user role and site management --- .../filament-shield/en/filament-shield.php | 2 +- src/EclipseServiceProvider.php | 19 +- src/Filament/Resources/RoleResource.php | 217 ++++++++++++++ .../RoleResource/Pages/CreateRole.php | 47 +++ .../Resources/RoleResource/Pages/EditRole.php | 54 ++++ .../RoleResource/Pages/ListRoles.php | 19 ++ .../Resources/RoleResource/Pages/ViewRole.php | 19 ++ src/Filament/Resources/UserResource.php | 282 +++++++++++++----- src/Models/Site.php | 29 ++ src/Models/User.php | 11 + src/Models/User/Role.php | 10 + src/Providers/AdminPanelProvider.php | 2 +- tests/Feature/AccessTest.php | 2 +- .../Filament/Resources/LocaleResourceTest.php | 5 +- .../Filament/Resources/RoleResourceTest.php | 55 ++++ .../Filament/Resources/UserResourceTest.php | 185 +++++++++++- tests/Feature/UserImpersonationTest.php | 6 + tests/Feature/UserTest.php | 23 ++ tests/Feature/UserTrashRestoreTest.php | 12 + 19 files changed, 920 insertions(+), 79 deletions(-) create mode 100644 src/Filament/Resources/RoleResource.php create mode 100644 src/Filament/Resources/RoleResource/Pages/CreateRole.php create mode 100644 src/Filament/Resources/RoleResource/Pages/EditRole.php create mode 100644 src/Filament/Resources/RoleResource/Pages/ListRoles.php create mode 100644 src/Filament/Resources/RoleResource/Pages/ViewRole.php create mode 100644 tests/Feature/Filament/Resources/RoleResourceTest.php create mode 100644 tests/Feature/UserTest.php diff --git a/resources/lang/vendor/filament-shield/en/filament-shield.php b/resources/lang/vendor/filament-shield/en/filament-shield.php index a1c1c2c..3f044bd 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', diff --git a/src/EclipseServiceProvider.php b/src/EclipseServiceProvider.php index fdb7ec9..7711279 100644 --- a/src/EclipseServiceProvider.php +++ b/src/EclipseServiceProvider.php @@ -17,6 +17,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; @@ -59,7 +61,7 @@ public function register(): self { parent::register(); - require_once __DIR__.'/Helpers/helpers.php'; + require_once __DIR__ . '/Helpers/helpers.php'; Event::listen(Login::class, function ($event) { if ($event->user instanceof User) { @@ -94,7 +96,7 @@ public function boot(): void } // Enable Model strictness when not in production - Model::shouldBeStrict(! app()->isProduction()); + Model::shouldBeStrict(!app()->isProduction()); // Do not allow destructive DB commands in production DB::prohibitDestructiveCommands(app()->isProduction()); @@ -110,9 +112,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/RoleResource.php b/src/Filament/Resources/RoleResource.php new file mode 100644 index 0000000..d8acde8 --- /dev/null +++ b/src/Filament/Resources/RoleResource.php @@ -0,0 +1,217 @@ +schema([ + Forms\Components\Grid::make() + ->schema([ + Forms\Components\Section::make() + ->schema([ + Forms\Components\TextInput::make('name') + ->label(__('filament-shield::filament-shield.field.name')) + ->unique( + ignoreRecord: true, /** @phpstan-ignore-next-line */ + modifyRuleUsing: fn (Unique $rule) => ! Utils::isTenancyEnabled() ? $rule : $rule->where(Utils::getTenantModelForeignKey(), Filament::getTenant()?->id) + ) + ->required() + ->maxLength(255), + + Forms\Components\TextInput::make('guard_name') + ->label(__('filament-shield::filament-shield.field.guard_name')) + ->default(Utils::getFilamentAuthGuard()) + ->nullable() + ->maxLength(255), + + Forms\Components\Select::make(config('permission.column_names.team_foreign_key')) + ->label(__('filament-shield::filament-shield.field.team')) + ->placeholder(__('filament-shield::filament-shield.field.team.placeholder')) + /** @phpstan-ignore-next-line */ + ->default([Filament::getTenant()?->id]) + ->options(fn (): Arrayable => Utils::getTenantModel() ? Utils::getTenantModel()::pluck('name', 'id') : collect()) + ->dehydrated(fn (): bool => ! (static::shield()->isCentralApp() && Utils::isTenancyEnabled())), + ShieldSelectAllToggle::make('select_all') + ->onIcon('heroicon-s-shield-check') + ->offIcon('heroicon-s-shield-exclamation') + ->label(__('filament-shield::filament-shield.field.select_all.name')) + ->helperText(fn (): HtmlString => new HtmlString(__('filament-shield::filament-shield.field.select_all.message'))) + ->dehydrated(fn (bool $state): bool => $state), + + ]) + ->columns([ + 'sm' => 2, + 'lg' => 3, + ]), + ]), + static::getShieldFormComponents(), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('name') + ->weight('font-medium') + ->label(__('filament-shield::filament-shield.column.name')) + ->formatStateUsing(fn ($state): string => Str::headline($state)) + ->searchable(), + Tables\Columns\TextColumn::make('guard_name') + ->badge() + ->color('warning') + ->label(__('filament-shield::filament-shield.column.guard_name')), + Tables\Columns\TextColumn::make('site.name') + ->default('Global') + ->badge() + ->color(fn (mixed $state): string => str($state)->contains('Global') ? 'gray' : 'primary') + ->label(__('filament-shield::filament-shield.column.team')) + ->searchable(), + Tables\Columns\TextColumn::make('permissions_count') + ->badge() + ->label(__('filament-shield::filament-shield.column.permissions')) + ->counts('permissions') + ->colors(['success']), + Tables\Columns\TextColumn::make('updated_at') + ->label(__('filament-shield::filament-shield.column.updated_at')) + ->dateTime(), + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + Tables\Actions\DeleteAction::make(), + ]) + ->bulkActions([ + Tables\Actions\DeleteBulkAction::make(), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListRoles::route('/'), + 'create' => Pages\CreateRole::route('/create'), + 'view' => Pages\ViewRole::route('/{record}'), + 'edit' => Pages\EditRole::route('/{record}/edit'), + ]; + } + + public static function getCluster(): ?string + { + return Utils::getResourceCluster() ?? static::$cluster; + } + + public static function getModel(): string + { + return Utils::getRoleModel(); + } + + public static function getModelLabel(): string + { + return __('filament-shield::filament-shield.resource.label.role'); + } + + public static function getPluralModelLabel(): string + { + return __('filament-shield::filament-shield.resource.label.roles'); + } + + public static function shouldRegisterNavigation(): bool + { + return Utils::isResourceNavigationRegistered(); + } + + public static function getNavigationGroup(): ?string + { + return Utils::isResourceNavigationGroupEnabled() + ? __('filament-shield::filament-shield.nav.group') + : ''; + } + + public static function getNavigationLabel(): string + { + return __('filament-shield::filament-shield.nav.role.label'); + } + + public static function getNavigationIcon(): string + { + return __('filament-shield::filament-shield.nav.role.icon'); + } + + public static function getNavigationSort(): ?int + { + return Utils::getResourceNavigationSort(); + } + + public static function getSubNavigationPosition(): SubNavigationPosition + { + return Utils::getSubNavigationPosition() ?? static::$subNavigationPosition; + } + + public static function getSlug(): string + { + return Utils::getResourceSlug(); + } + + public static function getNavigationBadge(): ?string + { + return Utils::isResourceNavigationBadgeEnabled() + ? strval(static::getEloquentQuery()->count()) + : null; + } + + public static function isScopedToTenant(): bool + { + return Utils::isScopedToTenant(); + } + + public static function canGloballySearch(): bool + { + return Utils::isResourceGloballySearchable() && count(static::getGloballySearchableAttributes()) && static::canViewAny(); + } +} diff --git a/src/Filament/Resources/RoleResource/Pages/CreateRole.php b/src/Filament/Resources/RoleResource/Pages/CreateRole.php new file mode 100644 index 0000000..7c5e724 --- /dev/null +++ b/src/Filament/Resources/RoleResource/Pages/CreateRole.php @@ -0,0 +1,47 @@ +permissions = collect($data) + ->filter(function ($permission, $key) { + return ! in_array($key, ['name', 'guard_name', 'select_all', Utils::getTenantModelForeignKey()]); + }) + ->values() + ->flatten() + ->unique(); + + if (Arr::has($data, Utils::getTenantModelForeignKey())) { + return Arr::only($data, ['name', 'guard_name', Utils::getTenantModelForeignKey()]); + } + + return Arr::only($data, ['name', 'guard_name']); + } + + protected function afterCreate(): void + { + $permissionModels = collect(); + $this->permissions->each(function ($permission) use ($permissionModels) { + $permissionModels->push(Utils::getPermissionModel()::firstOrCreate([ + /** @phpstan-ignore-next-line */ + 'name' => $permission, + 'guard_name' => $this->data['guard_name'], + ])); + }); + + $this->record->syncPermissions($permissionModels); + } +} diff --git a/src/Filament/Resources/RoleResource/Pages/EditRole.php b/src/Filament/Resources/RoleResource/Pages/EditRole.php new file mode 100644 index 0000000..fdd33be --- /dev/null +++ b/src/Filament/Resources/RoleResource/Pages/EditRole.php @@ -0,0 +1,54 @@ +permissions = collect($data) + ->filter(function ($permission, $key) { + return ! in_array($key, ['name', 'guard_name', 'select_all', Utils::getTenantModelForeignKey()]); + }) + ->values() + ->flatten() + ->unique(); + + if (Arr::has($data, Utils::getTenantModelForeignKey())) { + return Arr::only($data, ['name', 'guard_name', Utils::getTenantModelForeignKey()]); + } + + return Arr::only($data, ['name', 'guard_name']); + } + + protected function afterSave(): void + { + $permissionModels = collect(); + $this->permissions->each(function ($permission) use ($permissionModels) { + $permissionModels->push(Utils::getPermissionModel()::firstOrCreate([ + 'name' => $permission, + 'guard_name' => $this->data['guard_name'], + ])); + }); + + $this->record->syncPermissions($permissionModels); + } +} diff --git a/src/Filament/Resources/RoleResource/Pages/ListRoles.php b/src/Filament/Resources/RoleResource/Pages/ListRoles.php new file mode 100644 index 0000000..70f068f --- /dev/null +++ b/src/Filament/Resources/RoleResource/Pages/ListRoles.php @@ -0,0 +1,19 @@ +schema([ - Forms\Components\SpatieMediaLibraryFileUpload::make('avatar') - ->collection('avatars') - ->avatar() - ->imageEditor() - ->maxSize(1024 * 2), - self::getFirstNameFormComponent(), - self::getLastNameFormComponent(), - self::getEmailFormComponent(), - 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'), - 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(), + Forms\Components\Section::make(__('Personal Information')) + ->columns(2) + ->compact() + ->schema([ + Forms\Components\SpatieMediaLibraryFileUpload::make('avatar') + ->collection('avatars') + ->avatar() + ->imageEditor() + ->columnSpanFull() + ->maxSize(1024 * 2), + self::getFirstNameFormComponent(), + self::getLastNameFormComponent(), + self::getEmailFormComponent(), + 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'), + ]), + + Forms\Components\Section::make(__('Access Control')) + ->compact() + ->schema([ + Forms\Components\Select::make('sites') + ->relationship('sites', 'name') + ->getOptionLabelFromRecordUsing(fn(Model $record): string => + "{$record->name} ({$record->domain})") + ->multiple() + ->preload(), + + Forms\Components\Select::make('roles') + ->relationship('roles', 'name') + ->getOptionLabelFromRecordUsing(function ($record): string { + $suffix = $record->site_id ? ' (Site-Specific)' : ' (Global)'; + return "{$record->name}{$suffix}"; + }) + ->saveRelationshipsUsing(function (User $record, $state) { + $record->roles()->syncWithPivotValues($state, [config('permission.column_names.team_foreign_key') => getPermissionsTeamId()]); + }) + ->multiple() + ->preload() + ->searchable(), + ]) ]); } @@ -76,7 +108,7 @@ public static function table(Table $table): Table ->toggleable() ->size(50) ->circular() - ->defaultImageUrl(fn (User $user) => 'https://ui-avatars.com/api/?name='.urlencode($user->name)), + ->defaultImageUrl(fn(User $user) => 'https://ui-avatars.com/api/?name=' . urlencode($user->name)), Tables\Columns\TextColumn::make('first_name') ->searchable() ->sortable() @@ -99,7 +131,7 @@ public static function table(Table $table): Table ->label('Total Logins') ->sortable() ->numeric() - ->formatStateUsing(fn (?int $state) => $state ?? 0), + ->formatStateUsing(fn(?int $state) => $state ?? 0), ]; if (config('eclipse.email_verification')) { @@ -107,9 +139,9 @@ public static function table(Table $table): Table ->searchable() ->sortable() ->width(150) - ->icon(fn (User $user) => $user->email_verified_at ? 'heroicon-s-check-circle' : 'heroicon-s-x-circle') - ->iconColor(fn (User $user) => $user->email_verified_at ? Color::Green : Color::Red) - ->tooltip(fn (User $user) => $user->email_verified_at ? 'Verified' : 'Not verified'); + ->icon(fn(User $user) => $user->email_verified_at ? 'heroicon-s-check-circle' : 'heroicon-s-x-circle') + ->iconColor(fn(User $user) => $user->email_verified_at ? Color::Green : Color::Red) + ->tooltip(fn(User $user) => $user->email_verified_at ? 'Verified' : 'Not verified'); } else { $columns[] = Tables\Columns\TextColumn::make('email') ->searchable() @@ -126,6 +158,37 @@ 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 + ->roles() + ->whereNull('roles.' . config('permission.column_names.team_foreign_key')) + ->pluck('name') + ) + ->sortable(false) + ->placeholder('No global roles') + ->toggleable(); + + $columns[] = Tables\Columns\TextColumn::make('site_roles') + ->label('Site Roles (current)') + ->translateLabel() + ->badge() + ->color('warning') + ->getStateUsing(function (User $record) { + if (!Filament::getTenant()) + return 'No site context'; + + return $record->roles() + ->where('roles.' . config('permission.column_names.team_foreign_key'), Filament::getTenant()->id) + ->pluck('name'); + }) + ->sortable(false) + ->placeholder('No site roles') + ->toggleable(); + $columns[] = Tables\Columns\TextColumn::make('created_at') ->dateTime() ->sortable() @@ -138,38 +201,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\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(), @@ -178,10 +213,10 @@ public static function table(Table $table): Table ->grouped() ->redirectTo(route('filament.admin.tenant')), Tables\Actions\DeleteAction::make() - ->authorize(fn (User $record) => auth()->user()->can('delete_user') && auth()->id() !== $record->id) + ->authorize(fn(User $record) => auth()->user()->can('delete_user') && auth()->id() !== $record->id) ->requiresConfirmation(), Tables\Actions\RestoreAction::make() - ->visible(fn (User $user) => $user->trashed() && auth()->user()->can('restore_user')) + ->visible(fn(User $user) => $user->trashed() && auth()->user()->can('restore_user')) ->requiresConfirmation(), ]), ]) @@ -207,36 +242,139 @@ 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\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(), TextEntry::make('updated_at') ->dateTime(), ]), - Section::make('Personal information') + Section::make(__('Personal information')) ->columns(3) + ->compact() ->schema([ SpatieMediaLibraryImageEntry::make('avatar') ->collection('avatars') - ->defaultImageUrl(fn (User $user) => 'https://ui-avatars.com/api/?name='.urlencode($user->name)) + ->defaultImageUrl(fn(User $user) => 'https://ui-avatars.com/api/?name=' . urlencode($user->name)) ->circular(), Group::make() ->schema([ TextEntry::make('name') ->label('Full name'), TextEntry::make('email') - ->icon(config('eclipse.email_verification') ? fn (User $user) => $user->email_verified_at ? 'heroicon-s-check-circle' : 'heroicon-s-x-circle' : null) - ->iconColor(fn (User $user) => $user->email_verified_at ? Color::Green : Color::Red), + ->icon(config('eclipse.email_verification') ? fn(User $user) => $user->email_verified_at ? 'heroicon-s-check-circle' : 'heroicon-s-x-circle' : null) + ->iconColor(fn(User $user) => $user->email_verified_at ? Color::Green : Color::Red), ]), ]), + Section::make(__('Access Information')) + ->compact() + ->columns(2) + ->schema([ + TextEntry::make('sites') + ->label('Accessable Sites') + ->weight(FontWeight::Medium) + ->listWithLineBreaks() + ->placeholder(__(' No sites accessible')) + ->formatStateUsing(fn($state) => "✓ {$state->name} ({$state->domain})"), + + TextEntry::make('roles') + ->listWithLineBreaks() + ->weight(FontWeight::Medium) + ->placeholder(__('No roles assigned')) + ->formatStateUsing(function ($state): string { + $suffix = $state->site_id ? ' (Site-Specific)' : ' (Global)'; + return "✓ {$state->name}{$suffix}"; + }) + + ]) ]); } + public static function getRelations(): array { return [ @@ -290,10 +428,12 @@ public static function getGloballySearchableAttributes(): array public static function getEloquentQuery(): Builder { - return parent::getEloquentQuery() - ->withoutGlobalScopes([ - SoftDeletingScope::class, - ]); + + $query = parent::getEloquentQuery()->withoutGlobalScopes([ + SoftDeletingScope::class, + ]); + + return $query; } public static function getPermissionPrefixes(): array diff --git a/src/Models/Site.php b/src/Models/Site.php index f824a6a..9898c5d 100644 --- a/src/Models/Site.php +++ b/src/Models/Site.php @@ -3,8 +3,11 @@ namespace Eclipse\Core\Models; use Eclipse\Core\Database\Factories\SiteFactory; +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; class Site extends Model { @@ -32,6 +35,32 @@ 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 booted(): void + { + static::created(function ($site): void { + $allUserIDs = Role::whereNull('site_id') + ->with('users') + ->get() + ->pluck('users.*.id') + ->flatten() + ->unique(); + + if ($allUserIDs->isNotEmpty()) { + $site->users()->syncWithoutDetaching($allUserIDs); + } + }); + } + protected static function newFactory(): SiteFactory { return SiteFactory::new(); diff --git a/src/Models/User.php b/src/Models/User.php index 5d99bd5..dd46aec 100644 --- a/src/Models/User.php +++ b/src/Models/User.php @@ -2,6 +2,7 @@ namespace Eclipse\Core\Models; +use Eclipse\Core\Models\User\Role; use Eclipse\Core\Database\Factories\UserFactory; use Exception; use Filament\Models\Contracts\FilamentUser; @@ -123,6 +124,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..fe12727 100644 --- a/src/Models/User/Role.php +++ b/src/Models/User/Role.php @@ -4,7 +4,9 @@ use Eclipse\Core\Database\Factories\RoleFactory; use Eclipse\Core\Models\Site; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Support\Str; use Spatie\Permission\Models\Role as SpatieRole; class Role extends SpatieRole @@ -16,6 +18,14 @@ public function site() return $this->belongsTo(Site::class); } + protected function name(): Attribute { + + + return Attribute::make( + get: fn (string $value) => Str::headline($value) + ); + } + protected static function newFactory() { return RoleFactory::new(); diff --git a/src/Providers/AdminPanelProvider.php b/src/Providers/AdminPanelProvider.php index de83609..537834d 100644 --- a/src/Providers/AdminPanelProvider.php +++ b/src/Providers/AdminPanelProvider.php @@ -68,7 +68,7 @@ public function panel(Panel $panel): Panel 'gray' => Color::Slate, ]) ->topNavigation() - ->brandName(fn () => Registry::getSite()->name) + ->brandName(fn () => Registry::getSite()?->name) ->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources') ->discoverResources(in: $package_src.'Filament/Resources', for: 'Eclipse\\Core\\Filament\\Resources') ->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages') diff --git a/tests/Feature/AccessTest.php b/tests/Feature/AccessTest.php index 9ff5cee..e557808 100644 --- a/tests/Feature/AccessTest.php +++ b/tests/Feature/AccessTest.php @@ -71,4 +71,4 @@ // Test access $this->actingAs($user); $this->get(config('log-viewer.route_path', 'log-viewer'))->assertStatus(200); -}); +}); \ No newline at end of file diff --git a/tests/Feature/Filament/Resources/LocaleResourceTest.php b/tests/Feature/Filament/Resources/LocaleResourceTest.php index 09fd8b7..e6df018 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(); @@ -79,7 +82,7 @@ expect($locale)->toBeObject(); foreach ($data as $key => $val) { - expect($locale->$key)->toEqual($val, "Failed asserting that attribute $key value ".$locale->$key.' is equal to '.$val); + expect($locale->$key)->toEqual($val, "Failed asserting that attribute $key value " . $locale->$key . ' is equal to ' . $val); } }); diff --git a/tests/Feature/Filament/Resources/RoleResourceTest.php b/tests/Feature/Filament/Resources/RoleResourceTest.php new file mode 100644 index 0000000..9174999 --- /dev/null +++ b/tests/Feature/Filament/Resources/RoleResourceTest.php @@ -0,0 +1,55 @@ +create(); + $admin = User::factory()->create(); + $admin->assignRole('super_admin'); + + $this->actingAs($admin); + Filament::setTenant($site); + + livewire(CreateRole::class, ['tenant' => $site]) + ->fillForm([ + 'name' => 'Site Manager', + 'site_id' => $site->id, + ]) + ->call('create') + ->assertHasNoFormErrors(); + + $this->assertDatabaseHas('roles', [ + 'name' => 'Site Manager', + 'site_id' => $site->id, + ]); +}); + +test('role can be edited to change site assignment', function () { + $site1 = Site::factory()->create(); + $site2 = Site::factory()->create(); + $role = Role::factory()->create(['site_id' => $site1->id]); + + $admin = User::factory()->create(); + $admin->assignRole('super_admin'); + + $this->actingAs($admin); + Filament::setTenant($site1); + + livewire(EditRole::class, ['record' => $role->id, 'tenant' => $site1]) + ->fillForm([ + 'site_id' => $site2->id, + ]) + ->call('save') + ->assertHasNoFormErrors(); + + $this->assertDatabaseHas('roles', [ + 'id' => $role->id, + 'site_id' => $site2->id, + ]); +}); \ No newline at end of file diff --git a/tests/Feature/Filament/Resources/UserResourceTest.php b/tests/Feature/Filament/Resources/UserResourceTest.php index 5dd19c9..9fb0c3e 100644 --- a/tests/Feature/Filament/Resources/UserResourceTest.php +++ b/tests/Feature/Filament/Resources/UserResourceTest.php @@ -2,8 +2,12 @@ use Eclipse\Core\Filament\Resources\UserResource; use Eclipse\Core\Filament\Resources\UserResource\Pages\CreateUser; +use Eclipse\Core\Filament\Resources\UserResource\Pages\EditUser; 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; @@ -123,17 +127,24 @@ }); 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 () { $superAdmin = User::withTrashed()->find($this->superAdmin->id); @@ -152,3 +163,173 @@ $this->assertModelExists($user); } }); + + +test('user can be created with sites multi-select', function () { + $site1 = Site::factory()->create(); + $site2 = Site::factory()->create(); + + $admin = User::factory()->create(); + $admin->assignRole('super_admin'); + + $this->actingAs($admin); + Filament::setTenant($site1); + + $email = fake()->unique()->safeEmail(); + + livewire(CreateUser::class, ['tenant' => $site1]) + ->fillForm([ + 'first_name' => fake()->firstName(), + 'last_name' => fake()->lastName(), + 'email' => $email, + 'password' => 'password123', + 'sites' => [$site1->id, $site2->id], + ]) + ->call('create') + ->assertHasNoFormErrors(); + + $user = User::where('email', $email)->first(); + + expect($user->sites)->toHaveCount(2); + expect($user->sites->pluck('id'))->toContain($site1->id, $site2->id); +}); + +test('user sites can be updated via multi-select in edit', function () { + $site1 = Site::factory()->create(); + $site2 = Site::factory()->create(); + $site3 = Site::factory()->create(); + $user = User::factory()->create(); + + $user->sites()->attach($site1); + + $admin = User::factory()->create(); + $admin->assignRole('super_admin'); + + $this->actingAs($admin); + Filament::setTenant($site1); + + livewire(EditUser::class, ['record' => $user->id, 'tenant' => $site1]) + ->fillForm([ + 'sites' => [$site2->id, $site3->id], + ]) + ->call('save') + ->assertHasNoFormErrors(); + + $user->refresh(); + + expect($user->sites)->toHaveCount(2); + expect($user->sites->pluck('id'))->toContain($site2->id, $site3->id); + expect($user->sites->pluck('id'))->not->toContain($site1->id); +}); + +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]); +}); \ No newline at end of file diff --git a/tests/Feature/UserImpersonationTest.php b/tests/Feature/UserImpersonationTest.php index 6f1d6e7..05c8dcd 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/UserTest.php b/tests/Feature/UserTest.php new file mode 100644 index 0000000..ba627b5 --- /dev/null +++ b/tests/Feature/UserTest.php @@ -0,0 +1,23 @@ +create(); + $site2 = Site::factory()->create(); + $user = User::factory()->create(); + + $user->sites()->attach($site1); + + expect($user->canAccessTenant($site1))->toBeTrue(); + expect($user->canAccessTenant($site2))->toBeFalse(); +}); \ No newline at end of file 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)); From 4bfefec16cfb3003b16708a2608c7558f8be6c89 Mon Sep 17 00:00:00 2001 From: thapacodes4u Date: Fri, 13 Jun 2025 16:36:47 +0545 Subject: [PATCH 3/9] fix: resolving formatting issue --- src/EclipseServiceProvider.php | 4 +- src/Filament/Resources/RoleResource.php | 2 +- .../RoleResource/Pages/CreateRole.php | 2 +- .../Resources/RoleResource/Pages/EditRole.php | 2 +- src/Filament/Resources/UserResource.php | 67 +++++++++---------- src/Models/User.php | 2 +- src/Models/User/Role.php | 4 +- .../Filament/Resources/LocaleResourceTest.php | 2 +- .../Filament/Resources/RoleResourceTest.php | 17 ++--- .../Filament/Resources/UserResourceTest.php | 13 ++-- tests/Feature/UserImpersonationTest.php | 4 +- tests/Feature/UserTest.php | 10 +-- 12 files changed, 61 insertions(+), 68 deletions(-) diff --git a/src/EclipseServiceProvider.php b/src/EclipseServiceProvider.php index efa8557..db1237b 100644 --- a/src/EclipseServiceProvider.php +++ b/src/EclipseServiceProvider.php @@ -72,7 +72,7 @@ public function register(): self { parent::register(); - require_once __DIR__ . '/Helpers/helpers.php'; + require_once __DIR__.'/Helpers/helpers.php'; Event::listen(Login::class, function ($event) { if ($event->user instanceof User) { @@ -107,7 +107,7 @@ public function boot(): void } // Enable Model strictness when not in production - Model::shouldBeStrict(!app()->isProduction()); + Model::shouldBeStrict(! app()->isProduction()); // Do not allow destructive DB commands in production DB::prohibitDestructiveCommands(app()->isProduction()); diff --git a/src/Filament/Resources/RoleResource.php b/src/Filament/Resources/RoleResource.php index d8acde8..98ea07f 100644 --- a/src/Filament/Resources/RoleResource.php +++ b/src/Filament/Resources/RoleResource.php @@ -4,9 +4,9 @@ use BezhanSalleh\FilamentShield\Contracts\HasShieldPermissions; use BezhanSalleh\FilamentShield\Forms\ShieldSelectAllToggle; -use Eclipse\Core\Filament\Resources\RoleResource\Pages; use BezhanSalleh\FilamentShield\Support\Utils; use BezhanSalleh\FilamentShield\Traits\HasShieldFormComponents; +use Eclipse\Core\Filament\Resources\RoleResource\Pages; use Filament\Facades\Filament; use Filament\Forms; use Filament\Forms\Form; diff --git a/src/Filament/Resources/RoleResource/Pages/CreateRole.php b/src/Filament/Resources/RoleResource/Pages/CreateRole.php index 7c5e724..8cd0749 100644 --- a/src/Filament/Resources/RoleResource/Pages/CreateRole.php +++ b/src/Filament/Resources/RoleResource/Pages/CreateRole.php @@ -2,8 +2,8 @@ namespace Eclipse\Core\Filament\Resources\RoleResource\Pages; -use Eclipse\Core\Filament\Resources\RoleResource; use BezhanSalleh\FilamentShield\Support\Utils; +use Eclipse\Core\Filament\Resources\RoleResource; use Filament\Resources\Pages\CreateRecord; use Illuminate\Support\Arr; use Illuminate\Support\Collection; diff --git a/src/Filament/Resources/RoleResource/Pages/EditRole.php b/src/Filament/Resources/RoleResource/Pages/EditRole.php index fdd33be..e26c846 100644 --- a/src/Filament/Resources/RoleResource/Pages/EditRole.php +++ b/src/Filament/Resources/RoleResource/Pages/EditRole.php @@ -2,8 +2,8 @@ namespace Eclipse\Core\Filament\Resources\RoleResource\Pages; -use Eclipse\Core\Filament\Resources\RoleResource; use BezhanSalleh\FilamentShield\Support\Utils; +use Eclipse\Core\Filament\Resources\RoleResource; use Filament\Actions; use Filament\Resources\Pages\EditRecord; use Illuminate\Support\Arr; diff --git a/src/Filament/Resources/UserResource.php b/src/Filament/Resources/UserResource.php index 022012a..958e37b 100644 --- a/src/Filament/Resources/UserResource.php +++ b/src/Filament/Resources/UserResource.php @@ -3,7 +3,6 @@ namespace Eclipse\Core\Filament\Resources; use BezhanSalleh\FilamentShield\Contracts\HasShieldPermissions; -use Blade; use Eclipse\Core\Filament\Exports\TableExport; use Eclipse\Core\Filament\Resources; use Eclipse\Core\Models\User; @@ -68,10 +67,10 @@ public static function form(Form $form): Form 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'), + ->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'), ]), Forms\Components\Section::make(__('Access Control')) @@ -79,8 +78,7 @@ public static function form(Form $form): Form ->schema([ Forms\Components\Select::make('sites') ->relationship('sites', 'name') - ->getOptionLabelFromRecordUsing(fn(Model $record): string => - "{$record->name} ({$record->domain})") + ->getOptionLabelFromRecordUsing(fn (Model $record): string => "{$record->name} ({$record->domain})") ->multiple() ->preload(), @@ -88,6 +86,7 @@ public static function form(Form $form): Form ->relationship('roles', 'name') ->getOptionLabelFromRecordUsing(function ($record): string { $suffix = $record->site_id ? ' (Site-Specific)' : ' (Global)'; + return "{$record->name}{$suffix}"; }) ->saveRelationshipsUsing(function (User $record, $state) { @@ -96,7 +95,7 @@ public static function form(Form $form): Form ->multiple() ->preload() ->searchable(), - ]) + ]), ]); } @@ -108,7 +107,7 @@ public static function table(Table $table): Table ->toggleable() ->size(50) ->circular() - ->defaultImageUrl(fn(User $user) => 'https://ui-avatars.com/api/?name=' . urlencode($user->name)), + ->defaultImageUrl(fn (User $user) => 'https://ui-avatars.com/api/?name='.urlencode($user->name)), Tables\Columns\TextColumn::make('first_name') ->searchable() ->sortable() @@ -131,7 +130,7 @@ public static function table(Table $table): Table ->label('Total Logins') ->sortable() ->numeric() - ->formatStateUsing(fn(?int $state) => $state ?? 0), + ->formatStateUsing(fn (?int $state) => $state ?? 0), ]; if (config('eclipse.email_verification')) { @@ -139,9 +138,9 @@ public static function table(Table $table): Table ->searchable() ->sortable() ->width(150) - ->icon(fn(User $user) => $user->email_verified_at ? 'heroicon-s-check-circle' : 'heroicon-s-x-circle') - ->iconColor(fn(User $user) => $user->email_verified_at ? Color::Green : Color::Red) - ->tooltip(fn(User $user) => $user->email_verified_at ? 'Verified' : 'Not verified'); + ->icon(fn (User $user) => $user->email_verified_at ? 'heroicon-s-check-circle' : 'heroicon-s-x-circle') + ->iconColor(fn (User $user) => $user->email_verified_at ? Color::Green : Color::Red) + ->tooltip(fn (User $user) => $user->email_verified_at ? 'Verified' : 'Not verified'); } else { $columns[] = Tables\Columns\TextColumn::make('email') ->searchable() @@ -163,9 +162,9 @@ public static function table(Table $table): Table ->translateLabel() ->badge() ->getStateUsing( - fn(User $record): Collection => $record + fn (User $record): Collection => $record ->roles() - ->whereNull('roles.' . config('permission.column_names.team_foreign_key')) + ->whereNull('roles.'.config('permission.column_names.team_foreign_key')) ->pluck('name') ) ->sortable(false) @@ -178,11 +177,12 @@ public static function table(Table $table): Table ->badge() ->color('warning') ->getStateUsing(function (User $record) { - if (!Filament::getTenant()) + if (! Filament::getTenant()) { return 'No site context'; + } return $record->roles() - ->where('roles.' . config('permission.column_names.team_foreign_key'), Filament::getTenant()->id) + ->where('roles.'.config('permission.column_names.team_foreign_key'), Filament::getTenant()->id) ->pluck('name'); }) ->sortable(false) @@ -213,10 +213,10 @@ public static function table(Table $table): Table ->grouped() ->redirectTo(route('filament.admin.tenant')), Tables\Actions\DeleteAction::make() - ->authorize(fn(User $record) => auth()->user()->can('delete_user') && auth()->id() !== $record->id) + ->authorize(fn (User $record) => auth()->user()->can('delete_user') && auth()->id() !== $record->id) ->requiresConfirmation(), Tables\Actions\RestoreAction::make() - ->visible(fn(User $user) => $user->trashed() && auth()->user()->can('restore_user')) + ->visible(fn (User $user) => $user->trashed() && auth()->user()->can('restore_user')) ->requiresConfirmation(), ]), ]) @@ -251,8 +251,7 @@ private static function getTableFilters(): array ->trueLabel(__('All accessible sites')) ->falseLabel(__('Current site only')) ->queries( - true: function (Builder $query): void { - }, + true: function (Builder $query): void {}, false: function (Builder $query): void { if (Filament::getTenant()) { $query->whereHas('sites', function ($subQuery) { @@ -273,7 +272,7 @@ private static function getTableFilters(): array ->label('Global Roles') ->relationship('roles', 'name', function (Builder $query): void { $query - ->whereNull('roles.' . config('permission.column_names.team_foreign_key')); + ->whereNull('roles.'.config('permission.column_names.team_foreign_key')); }) ->multiple() ->searchable() @@ -283,13 +282,13 @@ private static function getTableFilters(): array ->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); + $query->where('roles.'.config('permission.column_names.team_foreign_key'), Filament::getTenant()->id); } }) ->multiple() ->searchable() ->preload() - ->visible(fn() => Filament::getTenant() !== null), + ->visible(fn () => Filament::getTenant() !== null), Tables\Filters\TernaryFilter::make('email_verified_at') ->label('Email verification') @@ -298,9 +297,9 @@ private static function getTableFilters(): array ->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, + 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\QueryBuilder::make() @@ -339,15 +338,15 @@ public static function infolist(Infolist $infolist): Infolist ->schema([ SpatieMediaLibraryImageEntry::make('avatar') ->collection('avatars') - ->defaultImageUrl(fn(User $user) => 'https://ui-avatars.com/api/?name=' . urlencode($user->name)) + ->defaultImageUrl(fn (User $user) => 'https://ui-avatars.com/api/?name='.urlencode($user->name)) ->circular(), Group::make() ->schema([ TextEntry::make('name') ->label('Full name'), TextEntry::make('email') - ->icon(config('eclipse.email_verification') ? fn(User $user) => $user->email_verified_at ? 'heroicon-s-check-circle' : 'heroicon-s-x-circle' : null) - ->iconColor(fn(User $user) => $user->email_verified_at ? Color::Green : Color::Red), + ->icon(config('eclipse.email_verification') ? fn (User $user) => $user->email_verified_at ? 'heroicon-s-check-circle' : 'heroicon-s-x-circle' : null) + ->iconColor(fn (User $user) => $user->email_verified_at ? Color::Green : Color::Red), ]), ]), Section::make(__('Access Information')) @@ -359,7 +358,7 @@ public static function infolist(Infolist $infolist): Infolist ->weight(FontWeight::Medium) ->listWithLineBreaks() ->placeholder(__(' No sites accessible')) - ->formatStateUsing(fn($state) => "✓ {$state->name} ({$state->domain})"), + ->formatStateUsing(fn ($state) => "✓ {$state->name} ({$state->domain})"), TextEntry::make('roles') ->listWithLineBreaks() @@ -367,14 +366,14 @@ public static function infolist(Infolist $infolist): Infolist ->placeholder(__('No roles assigned')) ->formatStateUsing(function ($state): string { $suffix = $state->site_id ? ' (Site-Specific)' : ' (Global)'; + return "✓ {$state->name}{$suffix}"; - }) + }), - ]) + ]), ]); } - public static function getRelations(): array { return [ diff --git a/src/Models/User.php b/src/Models/User.php index dd46aec..71172ec 100644 --- a/src/Models/User.php +++ b/src/Models/User.php @@ -2,8 +2,8 @@ namespace Eclipse\Core\Models; -use Eclipse\Core\Models\User\Role; use Eclipse\Core\Database\Factories\UserFactory; +use Eclipse\Core\Models\User\Role; use Exception; use Filament\Models\Contracts\FilamentUser; use Filament\Models\Contracts\HasAvatar; diff --git a/src/Models/User/Role.php b/src/Models/User/Role.php index fe12727..f2705be 100644 --- a/src/Models/User/Role.php +++ b/src/Models/User/Role.php @@ -18,8 +18,8 @@ public function site() return $this->belongsTo(Site::class); } - protected function name(): Attribute { - + protected function name(): Attribute + { return Attribute::make( get: fn (string $value) => Str::headline($value) diff --git a/tests/Feature/Filament/Resources/LocaleResourceTest.php b/tests/Feature/Filament/Resources/LocaleResourceTest.php index e6df018..94dd4e4 100644 --- a/tests/Feature/Filament/Resources/LocaleResourceTest.php +++ b/tests/Feature/Filament/Resources/LocaleResourceTest.php @@ -82,7 +82,7 @@ expect($locale)->toBeObject(); foreach ($data as $key => $val) { - expect($locale->$key)->toEqual($val, "Failed asserting that attribute $key value " . $locale->$key . ' is equal to ' . $val); + expect($locale->$key)->toEqual($val, "Failed asserting that attribute $key value ".$locale->$key.' is equal to '.$val); } }); diff --git a/tests/Feature/Filament/Resources/RoleResourceTest.php b/tests/Feature/Filament/Resources/RoleResourceTest.php index 9174999..1f71120 100644 --- a/tests/Feature/Filament/Resources/RoleResourceTest.php +++ b/tests/Feature/Filament/Resources/RoleResourceTest.php @@ -6,16 +6,17 @@ use Eclipse\Core\Models\User; use Eclipse\Core\Models\User\Role; use Filament\Facades\Filament; + use function Pest\Livewire\livewire; test('role can be created with site assignment', function () { $site = Site::factory()->create(); $admin = User::factory()->create(); $admin->assignRole('super_admin'); - + $this->actingAs($admin); Filament::setTenant($site); - + livewire(CreateRole::class, ['tenant' => $site]) ->fillForm([ 'name' => 'Site Manager', @@ -23,7 +24,7 @@ ]) ->call('create') ->assertHasNoFormErrors(); - + $this->assertDatabaseHas('roles', [ 'name' => 'Site Manager', 'site_id' => $site->id, @@ -34,22 +35,22 @@ $site1 = Site::factory()->create(); $site2 = Site::factory()->create(); $role = Role::factory()->create(['site_id' => $site1->id]); - + $admin = User::factory()->create(); $admin->assignRole('super_admin'); - + $this->actingAs($admin); Filament::setTenant($site1); - + livewire(EditRole::class, ['record' => $role->id, 'tenant' => $site1]) ->fillForm([ 'site_id' => $site2->id, ]) ->call('save') ->assertHasNoFormErrors(); - + $this->assertDatabaseHas('roles', [ 'id' => $role->id, 'site_id' => $site2->id, ]); -}); \ No newline at end of file +}); diff --git a/tests/Feature/Filament/Resources/UserResourceTest.php b/tests/Feature/Filament/Resources/UserResourceTest.php index 9fb0c3e..8315403 100644 --- a/tests/Feature/Filament/Resources/UserResourceTest.php +++ b/tests/Feature/Filament/Resources/UserResourceTest.php @@ -144,7 +144,6 @@ expect($user->trashed())->toBeTrue(); }); - test('authed user cannot delete himself', function () { $superAdmin = User::withTrashed()->find($this->superAdmin->id); @@ -164,7 +163,6 @@ } }); - test('user can be created with sites multi-select', function () { $site1 = Site::factory()->create(); $site2 = Site::factory()->create(); @@ -253,13 +251,13 @@ $globalRole = Role::create([ 'name' => 'global_admin', 'guard_name' => 'web', - config('permission.column_names.team_foreign_key') => null // Global role + 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 + config('permission.column_names.team_foreign_key') => $site->id, // Site-specific role ]); $user->sites()->attach($site); @@ -278,7 +276,6 @@ ->assertTableColumnExists('site_roles'); }); - test('filter shows users from all accessible sites when enabled', function () { $site1 = Site::factory()->create(); $site2 = Site::factory()->create(); @@ -311,12 +308,12 @@ $globalRole = Role::create([ 'name' => 'global_admin', - config('permission.column_names.team_foreign_key') => null // Global role + 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 + config('permission.column_names.team_foreign_key') => $site->id, // Site-specific role ]); $user1->assignRole($globalRole); @@ -332,4 +329,4 @@ livewire(ListUsers::class, ['tenant' => $site]) ->assertSuccessful() ->assertCanSeeTableRecords([$user1, $user2]); -}); \ No newline at end of file +}); diff --git a/tests/Feature/UserImpersonationTest.php b/tests/Feature/UserImpersonationTest.php index 05c8dcd..ae01cf0 100644 --- a/tests/Feature/UserImpersonationTest.php +++ b/tests/Feature/UserImpersonationTest.php @@ -36,7 +36,7 @@ test('non-authorized user cannot impersonate other users', function () { $this->unauthorizedUser->syncRoles([]); $this->unauthorizedUser->syncPermissions([]); - + // Login as unauthorized user Auth::login($this->unauthorizedUser); @@ -53,7 +53,7 @@ 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/UserTest.php b/tests/Feature/UserTest.php index ba627b5..bc5d944 100644 --- a/tests/Feature/UserTest.php +++ b/tests/Feature/UserTest.php @@ -3,13 +3,9 @@ use Eclipse\Core\Models\Site; use Eclipse\Core\Models\User; -test('new user automatically gets panel_user role', function () { - -}); - -test('user from seeder gets panel_user role', function () { +test('new user automatically gets panel_user role', function () {}); -}); +test('user from seeder gets panel_user role', function () {}); test('user can only access sites they belong to', function () { $site1 = Site::factory()->create(); @@ -20,4 +16,4 @@ expect($user->canAccessTenant($site1))->toBeTrue(); expect($user->canAccessTenant($site2))->toBeFalse(); -}); \ No newline at end of file +}); From 1dece7aa8f17712a89e2bdf29c15b21787f36d44 Mon Sep 17 00:00:00 2001 From: thapacodes4u Date: Fri, 13 Jun 2025 17:06:16 +0545 Subject: [PATCH 4/9] fix: removing commented codes & improving site selector in role-resource --- .../lang/vendor/filament-shield/en/filament-shield.php | 2 +- src/Filament/Resources/RoleResource.php | 6 ++++++ src/Filament/Resources/RoleResource/Pages/EditRole.php | 2 +- src/Filament/Resources/UserResource.php | 7 +------ 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/resources/lang/vendor/filament-shield/en/filament-shield.php b/resources/lang/vendor/filament-shield/en/filament-shield.php index 3f044bd..e67e86c 100644 --- a/resources/lang/vendor/filament-shield/en/filament-shield.php +++ b/resources/lang/vendor/filament-shield/en/filament-shield.php @@ -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/Filament/Resources/RoleResource.php b/src/Filament/Resources/RoleResource.php index 98ea07f..cc5ba31 100644 --- a/src/Filament/Resources/RoleResource.php +++ b/src/Filament/Resources/RoleResource.php @@ -60,11 +60,17 @@ public static function form(Form $form): Form ->nullable() ->maxLength(255), + Forms\Components\TextInput::make(config('permission.column_names.team_foreign_key')) + ->label(__('filament-shield::filament-shield.field.team')) + ->placeholder(__('filament-shield::filament-shield.field.team.placeholder')) + ->visibleOn('view'), + Forms\Components\Select::make(config('permission.column_names.team_foreign_key')) ->label(__('filament-shield::filament-shield.field.team')) ->placeholder(__('filament-shield::filament-shield.field.team.placeholder')) /** @phpstan-ignore-next-line */ ->default([Filament::getTenant()?->id]) + ->visibleOn(['create', 'edit']) ->options(fn (): Arrayable => Utils::getTenantModel() ? Utils::getTenantModel()::pluck('name', 'id') : collect()) ->dehydrated(fn (): bool => ! (static::shield()->isCentralApp() && Utils::isTenancyEnabled())), ShieldSelectAllToggle::make('select_all') diff --git a/src/Filament/Resources/RoleResource/Pages/EditRole.php b/src/Filament/Resources/RoleResource/Pages/EditRole.php index e26c846..f461190 100644 --- a/src/Filament/Resources/RoleResource/Pages/EditRole.php +++ b/src/Filament/Resources/RoleResource/Pages/EditRole.php @@ -18,7 +18,7 @@ class EditRole extends EditRecord protected function getActions(): array { return [ - Actions\DeleteAction::make(), + Actions\ViewAction::make(), ]; } diff --git a/src/Filament/Resources/UserResource.php b/src/Filament/Resources/UserResource.php index 958e37b..ede949e 100644 --- a/src/Filament/Resources/UserResource.php +++ b/src/Filament/Resources/UserResource.php @@ -33,8 +33,6 @@ class UserResource extends Resource implements HasShieldPermissions { - // protected static bool $isScopedToTenant = false; - protected static ?string $tenantOwnershipRelationshipName = 'sites'; protected static ?string $model = User::class; @@ -427,12 +425,9 @@ public static function getGloballySearchableAttributes(): array public static function getEloquentQuery(): Builder { - - $query = parent::getEloquentQuery()->withoutGlobalScopes([ + return parent::getEloquentQuery()->withoutGlobalScopes([ SoftDeletingScope::class, ]); - - return $query; } public static function getPermissionPrefixes(): array From 01fbf73c779a8e977dd15514983516a9dc4ba204 Mon Sep 17 00:00:00 2001 From: thapacodes4u Date: Fri, 13 Jun 2025 19:14:55 +0545 Subject: [PATCH 5/9] fix: site id showing in role view page --- src/Filament/Resources/RoleResource.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Filament/Resources/RoleResource.php b/src/Filament/Resources/RoleResource.php index cc5ba31..4820181 100644 --- a/src/Filament/Resources/RoleResource.php +++ b/src/Filament/Resources/RoleResource.php @@ -63,14 +63,26 @@ public static function form(Form $form): Form Forms\Components\TextInput::make(config('permission.column_names.team_foreign_key')) ->label(__('filament-shield::filament-shield.field.team')) ->placeholder(__('filament-shield::filament-shield.field.team.placeholder')) - ->visibleOn('view'), + ->visible(function ($state, $context): bool { + if (empty($state) && $context === 'view') { + return true; + } + + return false; + }), Forms\Components\Select::make(config('permission.column_names.team_foreign_key')) ->label(__('filament-shield::filament-shield.field.team')) ->placeholder(__('filament-shield::filament-shield.field.team.placeholder')) /** @phpstan-ignore-next-line */ ->default([Filament::getTenant()?->id]) - ->visibleOn(['create', 'edit']) + ->visible(function ($state, $context): bool { + if (empty($state) && $context === 'view') { + return false; + } + + return true; + }) ->options(fn (): Arrayable => Utils::getTenantModel() ? Utils::getTenantModel()::pluck('name', 'id') : collect()) ->dehydrated(fn (): bool => ! (static::shield()->isCentralApp() && Utils::isTenancyEnabled())), ShieldSelectAllToggle::make('select_all') From 4073a068e541daa41e65cfd63022bc7bc22e9685 Mon Sep 17 00:00:00 2001 From: thapacodes4u Date: Fri, 13 Jun 2025 19:17:14 +0545 Subject: [PATCH 6/9] fix: completing UserTest --- src/Models/User.php | 21 ++++++++++++++++++--- tests/Feature/UserTest.php | 6 ++++-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/Models/User.php b/src/Models/User.php index 71172ec..9382e06 100644 --- a/src/Models/User.php +++ b/src/Models/User.php @@ -15,6 +15,7 @@ use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\DB; use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\InteractsWithMedia; use Spatie\Permission\Traits\HasRoles; @@ -162,9 +163,23 @@ public function delete(): ?bool return parent::delete(); } - /** - * Determine if the user can impersonate other users. - */ + public function hasRoleGlobally($roles): bool + { + $roles = is_array($roles) ? $roles : [$roles]; + + $modelHasRolesTable = config('permission.table_names.model_has_roles', 'model_has_roles'); + $rolesTable = config('permission.table_names.roles', 'roles'); + $modelMorphKey = config('permission.column_names.model_morph_key', 'model_id'); + $rolePivotKey = config('permission.column_names.role_pivot_key', 'role_id') ?: 'role_id'; + + return DB::table($modelHasRolesTable) + ->join($rolesTable, "{$rolesTable}.id", '=', "{$modelHasRolesTable}.{$rolePivotKey}") + ->where("{$modelHasRolesTable}.{$modelMorphKey}", $this->id) + ->where("{$modelHasRolesTable}.model_type", static::class) + ->whereIn("{$rolesTable}.name", $roles) + ->exists(); + } + public function canImpersonate(): bool { return $this->can('impersonate', User::class); diff --git a/tests/Feature/UserTest.php b/tests/Feature/UserTest.php index bc5d944..41c4d25 100644 --- a/tests/Feature/UserTest.php +++ b/tests/Feature/UserTest.php @@ -3,9 +3,11 @@ use Eclipse\Core\Models\Site; use Eclipse\Core\Models\User; -test('new user automatically gets panel_user role', function () {}); +test('new user automatically gets panel_user role', function () { + $user = User::factory()->create(); -test('user from seeder gets panel_user role', function () {}); + expect($user->hasRoleGlobally('panel_user'))->toBeTrue(); +}); test('user can only access sites they belong to', function () { $site1 = Site::factory()->create(); From 6e149067cef799e45addea9203784d2b2e4429f6 Mon Sep 17 00:00:00 2001 From: thapacodes4u Date: Fri, 13 Jun 2025 19:39:54 +0545 Subject: [PATCH 7/9] fix: failing test --- src/Models/Site.php | 7 +++++++ tests/Feature/AccessTest.php | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Models/Site.php b/src/Models/Site.php index 9898c5d..b82de95 100644 --- a/src/Models/Site.php +++ b/src/Models/Site.php @@ -8,6 +8,7 @@ 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 { @@ -65,4 +66,10 @@ protected static function newFactory(): SiteFactory { return SiteFactory::new(); } + + /** @return HasMany<\Eclipse\Core\Models\User\Role, self> */ + public function roles(): HasMany + { + return $this->hasMany(\Eclipse\Core\Models\User\Role::class); + } } diff --git a/tests/Feature/AccessTest.php b/tests/Feature/AccessTest.php index a9de4f2..e3fc7ba 100644 --- a/tests/Feature/AccessTest.php +++ b/tests/Feature/AccessTest.php @@ -66,7 +66,7 @@ $user->assignRole('super_admin'); // Assert the user has super_admin role - $this->assertTrue($user->hasRole('super_admin')); + $this->assertTrue($user->hasRoleGlobally('super_admin')); // Test access $this->actingAs($user); From 124f622e94a7c980df8c63f3b60d8f3b2822e98a Mon Sep 17 00:00:00 2001 From: thapacodes4u Date: Tue, 17 Jun 2025 00:53:55 +0545 Subject: [PATCH 8/9] fix: removing sites form field from UserResource --- src/Filament/Resources/UserResource.php | 16 ++--- .../Filament/Resources/UserResourceTest.php | 58 ------------------- 2 files changed, 9 insertions(+), 65 deletions(-) diff --git a/src/Filament/Resources/UserResource.php b/src/Filament/Resources/UserResource.php index ede949e..dca7f22 100644 --- a/src/Filament/Resources/UserResource.php +++ b/src/Filament/Resources/UserResource.php @@ -6,6 +6,7 @@ use Eclipse\Core\Filament\Exports\TableExport; use Eclipse\Core\Filament\Resources; use Eclipse\Core\Models\User; +use Eclipse\Core\Models\User\Role; use Filament\Facades\Filament; use Filament\Forms; use Filament\Forms\Form; @@ -24,7 +25,6 @@ use Filament\Tables\Filters\QueryBuilder\Constraints\TextConstraint; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletingScope; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Hash; @@ -74,13 +74,8 @@ public static function form(Form $form): Form Forms\Components\Section::make(__('Access Control')) ->compact() ->schema([ - Forms\Components\Select::make('sites') - ->relationship('sites', 'name') - ->getOptionLabelFromRecordUsing(fn (Model $record): string => "{$record->name} ({$record->domain})") - ->multiple() - ->preload(), - Forms\Components\Select::make('roles') + ->hiddenLabel() ->relationship('roles', 'name') ->getOptionLabelFromRecordUsing(function ($record): string { $suffix = $record->site_id ? ' (Site-Specific)' : ' (Global)'; @@ -88,6 +83,13 @@ public static function form(Form $form): Form return "{$record->name}{$suffix}"; }) ->saveRelationshipsUsing(function (User $record, $state) { + $siteIDs = Role::whereIn('id', $state) + ->whereNotNull('site_id') + ->pluck('site_id') + ->toArray(); + + $record->sites()->sync($siteIDs); + $record->roles()->syncWithPivotValues($state, [config('permission.column_names.team_foreign_key') => getPermissionsTeamId()]); }) ->multiple() diff --git a/tests/Feature/Filament/Resources/UserResourceTest.php b/tests/Feature/Filament/Resources/UserResourceTest.php index 8315403..07678ee 100644 --- a/tests/Feature/Filament/Resources/UserResourceTest.php +++ b/tests/Feature/Filament/Resources/UserResourceTest.php @@ -2,7 +2,6 @@ use Eclipse\Core\Filament\Resources\UserResource; use Eclipse\Core\Filament\Resources\UserResource\Pages\CreateUser; -use Eclipse\Core\Filament\Resources\UserResource\Pages\EditUser; use Eclipse\Core\Filament\Resources\UserResource\Pages\ListUsers; use Eclipse\Core\Models\Site; use Eclipse\Core\Models\User; @@ -163,63 +162,6 @@ } }); -test('user can be created with sites multi-select', function () { - $site1 = Site::factory()->create(); - $site2 = Site::factory()->create(); - - $admin = User::factory()->create(); - $admin->assignRole('super_admin'); - - $this->actingAs($admin); - Filament::setTenant($site1); - - $email = fake()->unique()->safeEmail(); - - livewire(CreateUser::class, ['tenant' => $site1]) - ->fillForm([ - 'first_name' => fake()->firstName(), - 'last_name' => fake()->lastName(), - 'email' => $email, - 'password' => 'password123', - 'sites' => [$site1->id, $site2->id], - ]) - ->call('create') - ->assertHasNoFormErrors(); - - $user = User::where('email', $email)->first(); - - expect($user->sites)->toHaveCount(2); - expect($user->sites->pluck('id'))->toContain($site1->id, $site2->id); -}); - -test('user sites can be updated via multi-select in edit', function () { - $site1 = Site::factory()->create(); - $site2 = Site::factory()->create(); - $site3 = Site::factory()->create(); - $user = User::factory()->create(); - - $user->sites()->attach($site1); - - $admin = User::factory()->create(); - $admin->assignRole('super_admin'); - - $this->actingAs($admin); - Filament::setTenant($site1); - - livewire(EditUser::class, ['record' => $user->id, 'tenant' => $site1]) - ->fillForm([ - 'sites' => [$site2->id, $site3->id], - ]) - ->call('save') - ->assertHasNoFormErrors(); - - $user->refresh(); - - expect($user->sites)->toHaveCount(2); - expect($user->sites->pluck('id'))->toContain($site2->id, $site3->id); - expect($user->sites->pluck('id'))->not->toContain($site1->id); -}); - test('user list shows only current site users by default', function () { $site1 = Site::factory()->create(); $site2 = Site::factory()->create(); From 9848e202fd4596763c4b846ea88d0fb190e200f3 Mon Sep 17 00:00:00 2001 From: thapacodes4u Date: Tue, 17 Jun 2025 13:19:21 +0545 Subject: [PATCH 9/9] fix: error in testcases --- src/Filament/Resources/UserResource.php | 8 ++++++-- src/Models/User.php | 18 ------------------ src/Models/User/Role.php | 10 ---------- tests/Feature/AccessTest.php | 2 +- tests/Feature/UserTest.php | 4 +++- 5 files changed, 10 insertions(+), 32 deletions(-) diff --git a/src/Filament/Resources/UserResource.php b/src/Filament/Resources/UserResource.php index dca7f22..4dbc499 100644 --- a/src/Filament/Resources/UserResource.php +++ b/src/Filament/Resources/UserResource.php @@ -28,6 +28,7 @@ use Illuminate\Database\Eloquent\SoftDeletingScope; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Str; use pxlrbt\FilamentExcel\Actions\Tables\ExportBulkAction; use STS\FilamentImpersonate\Tables\Actions\Impersonate; @@ -166,6 +167,7 @@ public static function table(Table $table): Table ->roles() ->whereNull('roles.'.config('permission.column_names.team_foreign_key')) ->pluck('name') + ->map(fn ($roleName) => Str::headline($roleName)) ) ->sortable(false) ->placeholder('No global roles') @@ -183,7 +185,8 @@ public static function table(Table $table): Table return $record->roles() ->where('roles.'.config('permission.column_names.team_foreign_key'), Filament::getTenant()->id) - ->pluck('name'); + ->pluck('name') + ->map(fn ($roleName) => Str::headline($roleName)); }) ->sortable(false) ->placeholder('No site roles') @@ -366,8 +369,9 @@ public static function infolist(Infolist $infolist): Infolist ->placeholder(__('No roles assigned')) ->formatStateUsing(function ($state): string { $suffix = $state->site_id ? ' (Site-Specific)' : ' (Global)'; + $roleName = Str::headline($state->name); - return "✓ {$state->name}{$suffix}"; + return "✓ {$roleName}{$suffix}"; }), ]), diff --git a/src/Models/User.php b/src/Models/User.php index 9382e06..2184453 100644 --- a/src/Models/User.php +++ b/src/Models/User.php @@ -15,7 +15,6 @@ use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Illuminate\Support\Collection; -use Illuminate\Support\Facades\DB; use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\InteractsWithMedia; use Spatie\Permission\Traits\HasRoles; @@ -163,23 +162,6 @@ public function delete(): ?bool return parent::delete(); } - public function hasRoleGlobally($roles): bool - { - $roles = is_array($roles) ? $roles : [$roles]; - - $modelHasRolesTable = config('permission.table_names.model_has_roles', 'model_has_roles'); - $rolesTable = config('permission.table_names.roles', 'roles'); - $modelMorphKey = config('permission.column_names.model_morph_key', 'model_id'); - $rolePivotKey = config('permission.column_names.role_pivot_key', 'role_id') ?: 'role_id'; - - return DB::table($modelHasRolesTable) - ->join($rolesTable, "{$rolesTable}.id", '=', "{$modelHasRolesTable}.{$rolePivotKey}") - ->where("{$modelHasRolesTable}.{$modelMorphKey}", $this->id) - ->where("{$modelHasRolesTable}.model_type", static::class) - ->whereIn("{$rolesTable}.name", $roles) - ->exists(); - } - public function canImpersonate(): bool { return $this->can('impersonate', User::class); diff --git a/src/Models/User/Role.php b/src/Models/User/Role.php index f2705be..87cc2ac 100644 --- a/src/Models/User/Role.php +++ b/src/Models/User/Role.php @@ -4,9 +4,7 @@ use Eclipse\Core\Database\Factories\RoleFactory; use Eclipse\Core\Models\Site; -use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; -use Illuminate\Support\Str; use Spatie\Permission\Models\Role as SpatieRole; class Role extends SpatieRole @@ -18,14 +16,6 @@ public function site() return $this->belongsTo(Site::class); } - protected function name(): Attribute - { - - return Attribute::make( - get: fn (string $value) => Str::headline($value) - ); - } - protected static function newFactory() { return RoleFactory::new(); diff --git a/tests/Feature/AccessTest.php b/tests/Feature/AccessTest.php index e3fc7ba..a9de4f2 100644 --- a/tests/Feature/AccessTest.php +++ b/tests/Feature/AccessTest.php @@ -66,7 +66,7 @@ $user->assignRole('super_admin'); // Assert the user has super_admin role - $this->assertTrue($user->hasRoleGlobally('super_admin')); + $this->assertTrue($user->hasRole('super_admin')); // Test access $this->actingAs($user); diff --git a/tests/Feature/UserTest.php b/tests/Feature/UserTest.php index 41c4d25..54fee6f 100644 --- a/tests/Feature/UserTest.php +++ b/tests/Feature/UserTest.php @@ -6,7 +6,9 @@ test('new user automatically gets panel_user role', function () { $user = User::factory()->create(); - expect($user->hasRoleGlobally('panel_user'))->toBeTrue(); + $this->actingAs($user); + + expect($user->hasRole('panel_user'))->toBeTrue(); }); test('user can only access sites they belong to', function () {