Мультипользовательский режим в Filament. Настройка, примеры, использование. Документация по-русски.
Настройка мультипользовательского режима в Filament. Документация с примерами на русском языке

Настройка мультипользовательского режима в Filament



Обзор мультипользовательского режима работы в Filament

Многопользовательский доступ - это концепция, при которой один экземпляр приложения обслуживает несколько клиентов. Каждый клиент имеет свои собственные данные и правила доступа, которые не позволяют ему просматривать или изменять данные других клиентов. Такая схема часто встречается в SaaS-приложениях. Пользователи часто входят в группы пользователей (часто называемые командами или организациями). Записи принадлежат группе, и пользователи могут быть членами нескольких групп. Это подходит для приложений, в которых пользователям необходимо совместно работать над данными.

Многопользовательские отношения - очень деликатная тема. Важно понимать последствия многопользовательской работы для безопасности и то, как правильно ее реализовать. При частичной или неправильной реализации данные, принадлежащие одному пользователю, могут быть открыты другому пользователю. Filament предоставляет набор инструментов, помогающих реализовать многопользовательский режим в вашем приложении, однако понимание того, как их использовать, зависит от вас. Filament не предоставляет никаких гарантий безопасности вашего приложения. Ответственность за обеспечение безопасности приложения лежит на вас. Более подробная информация приведена в разделе Безопасность.

Простой вариант мультипользовательского режима "один ко многим"

Мультипользовательность - понятие широкое и в разных контекстах может означать разные вещи. Система прав доступа Filament подразумевает, что пользователь входит в состав многих партнеров (организаций, команд, компаний и т.д.) и может переключаться между ними.

Если ваш случай более простой и вам не нужны отношения "многие-ко-многим", то настраивать систему многопользовательского доступа в Filament не нужно. Вместо этого можно использовать observers и global scopes.

Допустим, у вас есть столбец базы данных users.team_id, вы можете обойти все записи, чтобы они имели тот же самый team_id, что и пользователь, используя глобальную область видимости:

use Illuminate\Database\Eloquent\Builder;
 
class Post extends Model
{
    protected static function booted(): void
    {
        if (auth()->check()) {
            static::addGlobalScope('team', function (Builder $query) {
                $query->where('team_id', auth()->user()->team_id);
                // или с определенными отношениями 'team':
                $query->whereBelongsTo(auth()->user()->team);
            });
        }
    }
}

Для автоматической установки team_id в записи при ее создании можно создать наблюдателя:

class PostObserver
{
    public function creating(Post $post): void
    {
        if (auth()->check()) {
            $post->team_id = auth()->user()->team_id;
            // или с определенными отношениями 'team':
            $post->team()->associate(auth()->user()->team);
        }
    }
}

Настройка многопользовательского режима

Для настройки многопользовательского режима в конфигурации необходимо указать модель "tenant" (например, team или organization):

use App\Models\Team;
use Filament\Panel;
 
public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->tenant(Team::class);
}

Также необходимо указать Filament, к какому типу группы (tenants) относится тот или иной пользователь. Это можно сделать, реализовав интерфейс HasTenants в модели App\Models\User:

<?php
 
namespace App\Models;
 
use Filament\Models\Contracts\FilamentUser;
use Filament\Models\Contracts\HasTenants;
use Filament\Panel;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Support\Collection;
 
class User extends Authenticatable implements FilamentUser, HasTenants
{
    // ...
 
    public function getTenants(Panel $panel): Collection
    {
        return $this->teams;
    }
    
    public function teams(): BelongsToMany
    {
        return $this->belongsToMany(Team::class);
    }
 
    public function canAccessTenant(Model $tenant): bool
    {
        return $this->teams->contains($tenant);
    }
}

В данном примере пользователи принадлежат ко многим группам, поэтому существует отношение teams(). Метод getTenants() возвращает группы, к которым принадлежит пользователь. Filament использует его для получения списка групп пользователей, к которым пользователь имеет доступ.

Для обеспечения безопасности необходимо также реализовать метод canAccessTenant() интерфейса HasTenants, чтобы пользователи не могли получить доступ к данным других пользователей, угадав идентификатор пользователя и подставив его в URL.

Также необходимо, чтобы пользователи могли регистрировать новые группы.

Добавление страницы регистрации группы пользователей

Страница регистрации позволит пользователям создать новую группу пользователей.

При посещении вашего приложения после входа в систему пользователи будут перенаправлены на эту страницу, если у них еще нет группы.

Чтобы создать страницу регистрации, необходимо создать новый класс страницы, расширяющий Filament\Pages\Tenancy\RegisterTenant. Это полностраничный компонент Livewire. Его можно разместить в любом месте, например, в app/Filament/Pages/Tenancy/RegisterTeam.php:

namespace App\Filament\Pages\Tenancy;
 
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Pages\Tenancy\RegisterTenant;
use Illuminate\Database\Eloquent\Model;
 
class RegisterTeam extends RegisterTenant
{
    public static function getLabel(): string
    {
        return 'Register team';
    }
    
    public function form(Form $form): Form
    {
        return $form
            ->schema([
                TextInput::make('name'),
                // ...
            ]);
    }
    
    protected function handleRegistration(array $data): Team
    {
        $team = Team::create($data);
        
        $team->members()->attach(auth()->user());
        
        return $team;
    }
}

В метод form() можно добавить любые компоненты формы, а в методе handleRegistration() создать группу.

Теперь нам необходимо указать Filament на возможность использования этой страницы. Это можно сделать в конфигурации:

use App\Filament\Pages\Tenancy\RegisterTeam;
use Filament\Panel;
 
public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->tenantRegistration(RegisterTeam::class);
}

Настройка страницы регистрации группы пользователей

Можно переопределить любой метод базового класса страницы регистрации группы пользователей, чтобы добиться желаемого результата. Даже свойство $view может быть переопределено для использования пользовательского представления по вашему выбору.

Добавление страницы профиля группы пользователей

Страница профиля позволит пользователям редактировать информацию о своей группе.

Для настройки страницы профиля необходимо создать новый класс страницы, расширяющий Filament\Pages\Tenancy\EditTenantProfile. Это полностраничный компонент Livewire. Его можно разместить в любом месте, например, в app/Filament/Pages/Tenancy/EditTeamProfile.php:

namespace App\Filament\Pages\Tenancy;
 
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Pages\Tenancy\EditTenantProfile;
use Illuminate\Database\Eloquent\Model;
 
class EditTeamProfile extends EditTenantProfile
{
    public static function getLabel(): string
    {
        return 'Team profile';
    }
    
    public function form(Form $form): Form
    {
        return $form
            ->schema([
                TextInput::make('name'),
                // ...
            ]);
    }
}

В метод form() можно добавлять любые компоненты формы. Они будут сохранены непосредственно в модели группы пользователей.

Теперь нам необходимо указать Filament на использование этой страницы. Это можно сделать в конфигурации:

use App\Filament\Pages\Tenancy\EditTeamProfile;
use Filament\Panel;
 
public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->tenantProfile(EditTeamProfile::class);
}

Настройка страницы профиля группы пользователей

Можно переопределить любой метод базового класса страницы профиля группы пользователей, чтобы добиться желаемого результата. Даже свойство $view может быть переопределено для использования индивидуального представления на свой лад.

Доступ к текущей группе пользователей

В любом месте приложения можно получить доступ к модели группы пользователей для текущего запроса с помощью функции Filament::getTenant():

use Filament\Facades\Filament;
 
$tenant = Filament::getTenant();

Биллинг (billing)

Использование Laravel Spark

Filament обеспечивает интеграцию биллинга с Laravel Spark. Ваши пользователи могут заводить подписки и управлять своей биллинговой информацией.

Чтобы установить интеграцию, сначала установите Spark и настройте его для вашей модели групп пользователей.

После этого можно установить провайдер биллинга Filament для Spark с помощью Composer:

composer require filament/spark-billing-provider

В конфигурации Filament задайте Spark в качестве tenantBillingProvider():

use Filament\Billing\Providers\SparkBillingProvider;
use Filament\Panel;
 
public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->tenantBillingProvider(new SparkBillingProvider());
}

Теперь все готово к работе! Пользователи могут управлять своими счетами, перейдя по ссылке в меню пользователя группы.

Требование наличия подписки

Чтобы потребовать подписку для использования какой-либо части приложения, можно воспользоваться методом конфигурации requiresTenantSubscription():

use Filament\Panel;
 
public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->requiresTenantSubscription();
}

Теперь пользователи будут перенаправляться на страницу выставления счетов, если у них нет активной подписки.

Требование подписки для определенных ресурсов и страниц

Иногда в приложении требуется подписка только на определенные ресурсы и страницы. Это можно сделать, вернув true из метода isTenantSubscriptionRequired() класса ресурса или страницы:

public static function isTenantSubscriptionRequired(Panel $panel): bool
{
    return true;
}

Если вы используете метод конфигурации requiresTenantSubscription(), то можете вернуть false из этого метода, чтобы разрешить доступ к ресурсу или странице в виде исключения.

Написание специальной биллинговой интеграции

Интеграция биллинга довольно проста в написании. Вам просто нужен класс, реализующий интерфейс Filament\Billing\Contracts\Provider. Этот интерфейс имеет два метода.

  • getRouteAction() используется для получения действия маршрута, которое должно выполняться при посещении пользователем страницы биллинга. Это может быть функция обратного вызова, или имя контроллера, или компонента Livewire - все, что работает при обычном использовании Route::get() в Laravel. Например, можно сделать простое перенаправление на собственную страницу биллинга с помощью функции обратного вызова.
  • getSubscribedMiddleware() возвращает имя middleware, которое должно использоваться для проверки наличия активной подписки у пользователя группы. В случае отсутствия активной подписки этот middleware должен перенаправить пользователя на страницу биллинга.

Приведем пример биллингового провайдера, использующего функцию обратного вызова для действия маршрута и middleware для промежуточного ПО подписки:

use App\Http\Middleware\RedirectIfUserNotSubscribed;
use Filament\Billing\Contracts\Provider;
use Illuminate\Http\RedirectResponse;
 
class ExampleBillingProvider implements Provider
{
    public function getRouteAction(): string
    {
        return function (): RedirectResponse {
            return redirect('https://billing.example.com');
        };
    }
    
    public function getSubscribedMiddleware(): string
    {
        return RedirectIfUserNotSubscribed::class;
    }
}

Настройка меню пользователя группы

Меню смены группы пользователей расположено в боковой панели админки. Оно полностью настраиваемо.

Для регистрации новых пунктов в меню пользовательских групп можно воспользоваться конфигурацией:

use App\Filament\Pages\Settings;
use Filament\Navigation\MenuItem;
use Filament\Panel;
 
public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->tenantMenuItems([
            MenuItem::make()
                ->label('Settings')
                ->url(fn (): string => Settings::getUrl())
                ->icon('heroicon-m-cog-8-tooth'),
            // ...
        ]);
}

Чтобы настроить ссылку регистрации в меню пользовательских групп, зарегистрируйте новый элемент с помощью ключа массива register:

use Filament\Navigation\MenuItem;
use Filament\Panel;
 
public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->tenantMenuItems([
            'register' => MenuItem::make()->label('Register new team'),
            // ...
        ]);
}

Чтобы настроить ссылку на профиль в меню пользовательских групп, зарегистрируйте новый элемент с ключом массива profile:

use Filament\Navigation\MenuItem;
use Filament\Panel;
 
public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->tenantMenuItems([
            'profile' => MenuItem::make()->label('Edit team profile'),
            // ...
        ]);
}

Чтобы настроить ссылку на выставление счетов в меню пользовательских групп, зарегистрируйте новый элемент с ключом массива billing:

use Filament\Navigation\MenuItem;
use Filament\Panel;
 
public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->tenantMenuItems([
            'billing' => MenuItem::make()->label('Manage subscription'),
            // ...
        ]);
}

Возможность скрытия пунктов меню пользовательских групп при определенных условиях

Можно также условно скрыть пункт меню пользовательских групп с помощью методов visible() или hidden(), передав в них условие проверки. При передаче функции оценка условия переносится на момент отображения меню:

use Filament\Navigation\MenuItem;
 
MenuItem::make()
    ->label('Settings')
    ->visible(fn (): bool => auth()->user()->can('manage-team'))
    // or
    ->hidden(fn (): bool => ! auth()->user()->can('manage-team'))

Настройка аватаров

Из коробки Filament использует сайт ui-avatars.com для генерации аватаров на основе имени пользователя. Однако если в модели пользователя есть атрибут avatar_url, то он будет использоваться вместо него. Чтобы настроить получение URL-адреса аватара пользователя в Filament, можно реализовать контракт HasAvatar:

<?php
 
namespace App\Models;
 
use Filament\Models\Contracts\FilamentUser;
use Filament\Models\Contracts\HasAvatar;
use Illuminate\Database\Eloquent\Model;
 
class Team extends Model implements HasAvatar
{
    // ...
 
    public function getFilamentAvatarUrl(): ?string
    {
        return $this->avatar_url;
    }
}

Метод getFilamentAvatarUrl() используется для получения аватара текущего пользователя. Если в результате использования этого метода будет возвращен null, Filament вернется к ui-avatars.com.

Вы можете легко заменить ui-avatars.com на другой сервис, создав новый провайдер аватаров. Как это сделать, можно узнать здесь.

Настройка отношений между группами пользователей

При создании и листинге записей, связанных с пользовательской группой, Filament необходим доступ к двум отношениям Eloquent для каждого ресурса - отношению "владение" (ownership), которое определяется в классе модели ресурса, и отношению в классе модели группы пользователей. По умолчанию Filament пытается угадать имена этих отношений, основываясь на стандартных соглашениях Laravel. Например, если модель группы пользователей App\Models\Team, то Filament будет искать отношение team() в классе модели ресурсов. А если класс модели ресурса - App\Models\Post, то он будет искать отношение posts() в классе модели группы пользователей.

Настройка имени отношения прав доступа

С помощью аргумента ownershipRelationship метода конфигурации tenant() можно настроить имя отношения принадлежности, используемого сразу для всех ресурсов. В данном примере для классов модели ресурсов определены зависимости от owner:

use Filament\Panel;
 
public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->tenant(Team::class, ownershipRelationship: 'owner');
}

В качестве альтернативы можно установить статическое свойство $tenantOwnershipRelationshipName для класса ресурса, которое затем можно использовать для настройки имени отношения доступа, используемого только для данного ресурса. В данном примере для класса модели Post определено отношение owner:

use Filament\Resources\Resource;
 
class PostResource extends Resource
{
    protected static ?string $tenantOwnershipRelationshipName = 'owner';
 
    // ...
}

Настройка названия отношения ресурса

Для класса ресурса можно установить статическое свойство $tenantRelationshipName, которое затем можно использовать для настройки имени отношения, используемого для получения этого ресурса. В данном примере для класса модели пользователя группы определено отношение blogPosts:

use Filament\Resources\Resource;
 
class PostResource extends Resource
{
    protected static ?string $tenantRelationshipName = 'blogPosts';
 
    // ...
}

Настройка атрибута slug

При использовании пользователя группы как участника, возможно, потребуется добавить в URL поле slug, а не ID группы. Это можно сделать с помощью аргумента slugAttribute в методе конфигурации tenant():

use Filament\Panel;
 
public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->tenant(Team::class, slugAttribute: 'slug');
}

Настройка атрибута name

По умолчанию Filament будет использовать атрибут name участника группы для отображения его имени в приложении. Чтобы изменить это, можно реализовать контракт HasName:

<?php
 
namespace App\Models;
 
use Filament\Models\Contracts\HasName;
use Illuminate\Database\Eloquent\Model;
 
class Team extends Model implements HasName
{
    // ...
 
    public function getFilamentName(): string
    {
        return "{$this->name} {$this->subscription_plan}";
    }
}

Метод getFilamentName() используется для получения имени текущего пользователя.

Установка метки текущего пользователя

Внутри переключения между группами пользователей на боковой панели можно добавить небольшую метку типа "Выбранная группа" над названием текущей группы. Это можно сделать, реализовав метод HasCurrentTenantLabel в модели участника группы:

<?php
 
namespace App\Models;
 
use Filament\Models\Contracts\HasCurrentTenantLabel;
use Illuminate\Database\Eloquent\Model;
 
class Team extends Model implements HasCurrentTenantLabel
{
    // ...
    
    public function getCurrentTenantLabel(): string
    {
        return 'Выбранная группа';
    }
}

Установка группы пользователей по умолчанию

При входе в систему Filament перенаправляет пользователя в первую группу пользователей, полученную из метода getTenants().

Иногда это можно изменить. Например, можно хранить информацию о том, какая группа была активна последней, и перенаправлять пользователя на неё.

Чтобы настроить это, можно реализовать для пользователя контракт HasDefaultTenant:

namespace App\Models;
 
use Filament\Models\Contracts\FilamentUser;
use Filament\Models\Contracts\HasDefaultTenant;
use Filament\Models\Contracts\HasTenants;
use Filament\Panel;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
class User extends Model implements FilamentUser, HasDefaultTenant, HasTenants
{
    // ...
    
    public function getDefaultTenant(Panel $panel): ?Model
    {
        return $this->latestTeam;
    }
    
    public function latestTeam(): BelongsTo
    {
        return $this->belongsTo(Team::class, 'latest_team_id');
    }
}

Применение middleware к маршрутам с поддержкой пользовательских групп

Можно применять дополнительные middleware ко всем маршрутам с поддержкой пользовательских групп, передавая массив классов middleware в метод tenantMiddleware() в файле конфигурации панели:

use Filament\Panel;
 
public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->tenantMiddleware([
            // ...
        ]);
}

По умолчанию middleware будет запускаться при первой загрузке страницы, но не при последующих AJAX-запросах Livewire. Если необходимо запускать middleware при каждом запросе, можно сделать его постоянным, передав true в качестве второго аргумента в метод tenantMiddleware():

use Filament\Panel;
 
public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->tenantMiddleware([
            // ...
        ], isPersistent: true);
}

Добавление префикса маршрута пользовательской группы

По умолчанию в структуре URL ID или slug пользователя группы помещается сразу после пути к панели. Если требуется префикс к другому сегменту URL, используйте метод tenantRoutePrefix():

use Filament\Panel;
 
public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->path('admin')
        ->tenant(Team::class)
        ->tenantRoutePrefix('team');
}

Раньше структура URL была /admin/1 для группы пользователей 1. Теперь это /admin/team/1.

Безопасность пользователей

Важно понимать последствия реализации пользовательских групп для безопасности и то, как правильно ее реализовать. При частичной или неправильной реализации данные, принадлежащие одному пользователю группы, могут быть открыты для другого пользователя этой группы. Filament предоставляет набор инструментов, помогающих реализовать режим групп пользователей (multi-tenancy) в вашем приложении, однако понимание того, как их использовать, зависит от вас. Filament не предоставляет никаких гарантий безопасности вашего приложения. Ответственность за обеспечение безопасности приложения лежит на вас.

Ниже приведен список возможностей Filament, которые помогут вам реализовать multi-tenancy в вашем приложении:

  • Автоматическая привязка ресурсов к текущей группе пользователей. Базовый запрос Eloquent, используемый для получения записей о ресурсе, автоматически привязывается к текущей группе пользователей. Этот запрос используется для отображения таблицы списков ресурса, а также для обработки записей из текущего URL при редактировании или просмотре записи. Таким образом, если пользователь попытается просмотреть запись, не принадлежащую текущей группе пользователей, он получит ошибку 404.
  • Автоматическая привязка новых записей о ресурсах к текущей группе пользователей.

А вот то, чего Filament в настоящее время не предоставляет:

  • Привязка записей менеджера связей к текущей группе пользователей. При использовании менеджера связей в подавляющем большинстве случаев запрос не требует привязки к текущей группе пользователей, поскольку он уже привязан к родительской записи, которая сама по себе привязана к текущей группе пользователей. Например, если в модели группы пользователей Team имеется ресурс Author, а в этом ресурсе настроены отношения posts и менеджер отношений, и сообщения принадлежат только одному автору, то нет необходимости в масштабировании запроса. Это связано с тем, что пользователь в любом случае сможет видеть только авторов, принадлежащих к текущей группе, и, следовательно, сможет видеть только посты, принадлежащие этим авторам. При желании можно задать область видимости запроса Eloquent.
  • Компоненты форм и фильтры. При использовании компонентов формы Select, CheckboxList или Repeater, фильтра SelectFilter или любого другого подобного компонента Filament, способного автоматически получать "options" или другие данные из базы данных (обычно с помощью метода relationship()), эти данные не привязываются к группе пользователей. Основная причина этого заключается в том, что эти функции часто не принадлежат к пакету Filament Panel Builder и не знают, что они используются в этом контексте и что пользовательские группы вообще существуют. И даже если у них есть доступ к группе пользователей, им негде хранить конфигурацию отношений с ней. Для определения области видимости этих компонентов необходимо передать функцию запроса, которая определяет область видимости запроса для текущей группы пользователей. Например, если вы используете компонент Select формы для выбора author из отношения, то можно поступить следующим образом:
use Filament\Facades\Filament;
use Filament\Forms\Components\Select;
use Illuminate\Database\Eloquent\Builder;
 
Select::make('author_id')
    ->relationship(
        name: 'author',
        titleAttribute: 'name',
        modifyQueryUsing: fn (Builder $query) => $query->whereBelongsTo(Filament::getTenant())),
    );

Использование middleware для применения глобальных ограничений

Может оказаться полезным применять глобальные диапазоны к моделям Eloquent во время их использования в панели. Это позволит вам забыть о привязке запросов к текущей группе пользователей и автоматически применять привязку. Для этого можно создать новый класс middleware, например ApplyTenantScopes:

php artisan make:middleware ApplyTenantScopes

Внутри метода handle() можно применить любые глобальные области видимости:

use App\Models\Author;
use Closure;
use Filament\Facades\Filament;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
 
class ApplyTenantScopes
{
    public function handle(Request $request, Closure $next)
    {
        Author::addGlobalScope(
            fn (Builder $query) => $query->whereBelongsTo(Filament::getTenant()),
        );
        
        return $next($request);
    }
}

Теперь вы можете зарегистрировать этот middleware для всех маршрутов с поддержкой пользовательских групп и гарантировать, что он будет использоваться во всех AJAX-запросах Livewire, сделав его неизменным:

use Filament\Panel;
 
public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->tenantMiddleware([
            ApplyTenantScopes::class,
        ], isPersistent: true);
}

Перевод с английского официальной документации Filament 3:https://filamentphp.com/docs/3.x/panels/tenancy

Заберите ссылку на статью к себе, чтобы потом легко её найти!
Раз уж досюда дочитали, то может может есть желание рассказать об этом месте своим друзьям, знакомым и просто мимо проходящим?
Не надо себя сдерживать! ;)

Старт! Горячий старт на просторы интернета
Старт! Горячий старт на просторы интернета
Старт! Меню