first commit
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "mrsleeps/vexim-groups",
|
||||
"description": "Groups management for VExim Web UI",
|
||||
"type": "filament-plugin",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Your Name",
|
||||
"email": "your@email.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.4",
|
||||
"filament/filament": "^3.2",
|
||||
"illuminate/support": "^11.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Vexim\\Groups\\": "src/"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Vexim\\Groups\\GroupServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace Vexim\Groups\Filament\Resources;
|
||||
|
||||
|
||||
use App\Filament\Resources\Groups\Pages\CreateGroup;
|
||||
use App\Filament\Resources\Groups\Pages\EditGroup;
|
||||
use App\Filament\Resources\Groups\Pages\ListGroups;
|
||||
use App\Filament\Resources\Groups\Schemas\GroupForm;
|
||||
use App\Filament\Resources\Groups\Tables\GroupsTable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Vexim\Groups\Models\Group;
|
||||
use App\Models\Domain;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class GroupResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Group::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-user-group';
|
||||
|
||||
protected static string|\UnitEnum|null $navigationGroup = 'Account Management';
|
||||
|
||||
protected static ?string $navigationLabel = 'Groups';
|
||||
|
||||
protected static ?int $navigationSort = 2;
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'name';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return GroupForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return GroupsTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getEloquentQuery(): Builder
|
||||
{
|
||||
$query = parent::getEloquentQuery()->with('domain');
|
||||
$user = auth()->user();
|
||||
|
||||
if (!$user) {
|
||||
return $query->whereRaw('1 = 0');
|
||||
}
|
||||
|
||||
if ($user->isSystemAdmin()) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
if ($user->isDomainAdmin()) {
|
||||
$domainIds = Domain::whereHas('administrators', function ($q) use ($user) {
|
||||
$q->where('user_id', $user->id)
|
||||
->where('role', 'domain_admin');
|
||||
})->pluck('domain_id');
|
||||
|
||||
return $query->whereIn('domain_id', $domainIds);
|
||||
}
|
||||
|
||||
return $query->whereRaw('1 = 0');
|
||||
}
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
if (!$user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($user->isSystemAdmin()) {
|
||||
return (string) Group::count();
|
||||
}
|
||||
|
||||
if ($user->isDomainAdmin()) {
|
||||
$domainIds = Domain::whereHas('administrators', function ($q) use ($user) {
|
||||
$q->where('user_id', $user->id)
|
||||
->where('role', 'domain_admin');
|
||||
})->pluck('domain_id');
|
||||
|
||||
$count = Group::whereIn('domain_id', $domainIds)->count();
|
||||
return (string) $count;
|
||||
}
|
||||
|
||||
// Domain-user sees nothing
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function getNavigationBadgeColor(): ?string
|
||||
{
|
||||
return 'primary';
|
||||
}
|
||||
|
||||
public static function getNavigationBadgeTooltip(): ?string
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
if (!$user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($user->isSystemAdmin()) {
|
||||
return 'Total number of groups in the system';
|
||||
}
|
||||
|
||||
if ($user->isDomainAdmin()) {
|
||||
return 'Total number of groups in domains you administer';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Hide navigation item for domain-users
|
||||
public static function shouldRegisterNavigation(): bool
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
if (!$user || $user->isDomainUser()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListGroups::route('/'),
|
||||
'create' => CreateGroup::route('/create'),
|
||||
'edit' => EditGroup::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
namespace Vexim\Groups;
|
||||
|
||||
use Filament\Panel;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Vexim\Groups\Console\InstallGroupModule;
|
||||
|
||||
class GroupServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
// Register the plugin with Filament
|
||||
$this->app->resolving(Panel::class, function (Panel $panel) {
|
||||
$panel->plugin(new GroupPlugin());
|
||||
});
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
// Load migrations
|
||||
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
|
||||
|
||||
// Load views if you have any
|
||||
$this->loadViewsFrom(__DIR__ . '/../resources/views', 'groups');
|
||||
|
||||
// Load routes if you have any
|
||||
$this->loadRoutesFrom(__DIR__ . '/../routes/web.php');
|
||||
|
||||
// Publish config
|
||||
$this->publishes([
|
||||
__DIR__ . '/../config/groups.php' => config_path('groups.php'),
|
||||
], 'groups-config');
|
||||
|
||||
// Register console commands
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->commands([
|
||||
InstallGroupModule::class,
|
||||
]);
|
||||
}
|
||||
|
||||
// Merge config
|
||||
$this->mergeConfigFrom(
|
||||
__DIR__ . '/../config/groups.php',
|
||||
'groups'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace Vexim\Groups\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use App\Models\EximUser;
|
||||
|
||||
class Group extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'groups';
|
||||
|
||||
protected $fillable = [
|
||||
'domain_id',
|
||||
'name',
|
||||
'is_public',
|
||||
'enabled'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'domain_id' => 'integer',
|
||||
'enabled' => 'boolean',
|
||||
'is_public' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the domain that owns this group.
|
||||
*/
|
||||
public function domain()
|
||||
{
|
||||
return $this->belongsTo(Domain::class, 'domain_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the members (users/aliases) in this group.
|
||||
* Assuming member_id references another table like users or aliases.
|
||||
*/
|
||||
public function members(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(
|
||||
EximUser::class,
|
||||
'group_contents',
|
||||
'group_id',
|
||||
'member_id'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope for enabled groups only.
|
||||
*/
|
||||
public function scopeEnabled($query)
|
||||
{
|
||||
return $query->where('enabled', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope for public groups only.
|
||||
*/
|
||||
public function scopePublic($query)
|
||||
{
|
||||
return $query->where('is_public', '1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if group is public.
|
||||
*/
|
||||
public function isPublic(): bool
|
||||
{
|
||||
return $this->is_public === '1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all administrators assigned to this domain.
|
||||
*
|
||||
* Administrators have domain_admin privileges and can manage this domain's
|
||||
* settings, users, and blocklists.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||
*/
|
||||
public function administrators()
|
||||
{
|
||||
return $this->belongsToMany(User::class, 'vw_domain_user', 'domain_id', 'user_id')
|
||||
->withPivot('role')
|
||||
->withTimestamps()
|
||||
->wherePivot('role', 'domain_admin');
|
||||
}
|
||||
|
||||
// If you need to access the pivot directly
|
||||
public function groupContents()
|
||||
{
|
||||
return $this->hasMany(GroupContent::class, 'group_id');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Vexim\Groups\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||
use App\Models\EximUser;
|
||||
use App\Models\Group;
|
||||
|
||||
class GroupContent extends Pivot
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'group_contents';
|
||||
|
||||
protected $fillable = [
|
||||
'group_id',
|
||||
'member_id'
|
||||
];
|
||||
|
||||
protected $primaryKey = ['group_id', 'member_id'];
|
||||
public $incrementing = false;
|
||||
|
||||
public function member()
|
||||
{
|
||||
return $this->belongsTo(EximUser::class, 'member_id');
|
||||
}
|
||||
|
||||
public function group()
|
||||
{
|
||||
return $this->belongsTo(Group::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Vexim\Groups\Filament\Resources\Pages;
|
||||
|
||||
use App\Filament\Resources\Groups\GroupResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateGroup extends CreateRecord
|
||||
{
|
||||
protected static string $resource = GroupResource::class;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Vexim\Groups\Filament\Resources\Pages;
|
||||
|
||||
use App\Filament\Resources\Groups\GroupResource;
|
||||
use App\Filament\Resources\Groups\RelationManagers\MembersRelationManager;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditGroup extends EditRecord
|
||||
{
|
||||
protected static string $resource = GroupResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getRelationManagers(): array
|
||||
{
|
||||
return [
|
||||
MembersRelationManager::class,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Vexim\Groups\Filament\Resources\Pages;
|
||||
|
||||
use App\Filament\Resources\Groups\GroupResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListGroups extends ListRecords
|
||||
{
|
||||
protected static string $resource = GroupResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Vexim\Groups\Filament\Resources\RelationManagers;
|
||||
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Actions\AttachAction;
|
||||
use Filament\Actions\DetachAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Schemas\Schema;
|
||||
use App\Models\EximUser;
|
||||
|
||||
class MembersRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'members';
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'username';
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('username')
|
||||
->label('Username')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
])
|
||||
->headerActions([
|
||||
AttachAction::make()
|
||||
->label('Add Member')
|
||||
->model(EximUser::class)
|
||||
->recordSelectOptionsQuery(function ($query) {
|
||||
$user = auth()->user();
|
||||
$group = $this->getOwnerRecord();
|
||||
|
||||
$query = $query->whereIn('type', ['alias', 'local']);
|
||||
|
||||
if ($user->isSystemAdmin()) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
if ($user->isDomainAdmin()) {
|
||||
$domainIds = $user->domains()->pluck('domain_id');
|
||||
|
||||
if ($group && $group->domain_id) {
|
||||
$domainIds->push($group->domain_id);
|
||||
}
|
||||
|
||||
return $query->whereIn('domain_id', $domainIds);
|
||||
}
|
||||
|
||||
return $query->whereRaw('1 = 0');
|
||||
})
|
||||
->recordTitleAttribute('username')
|
||||
->preloadRecordSelect(),
|
||||
])
|
||||
->actions([
|
||||
DetachAction::make()
|
||||
->label('Remove'),
|
||||
])
|
||||
->emptyStateHeading('No group members')
|
||||
->emptyStateDescription('Add members to this group by clicking the "Add Member" button above.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Vexim\Groups\Filament\Resources\Schemas;
|
||||
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Schemas\Schema;
|
||||
use App\Models\Domain;
|
||||
|
||||
class GroupForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
$record = $schema->getRecord();
|
||||
$isCreating = $record === null;
|
||||
|
||||
return $schema
|
||||
->components([
|
||||
Select::make('domain_id')
|
||||
->label('Domain')
|
||||
->placeholder('Select a domain')
|
||||
->options(function () use ($isCreating) {
|
||||
$user = auth()->user();
|
||||
|
||||
if ($isCreating) {
|
||||
$domainsQuery = $user->isSystemAdmin() ? Domain::query() : $user->domains();
|
||||
$domainsQuery->whereNotExists(function ($query) {
|
||||
$query->select('users.domain_id')
|
||||
->from('users')
|
||||
->whereColumn('domains.domain_id', 'users.domain_id')
|
||||
->where('users.type', 'catch');
|
||||
});
|
||||
return $domainsQuery->pluck('domain', 'domains.domain_id');
|
||||
} else {
|
||||
$domainsQuery = $user->isSystemAdmin() ? Domain::query() : $user->domains();
|
||||
return $domainsQuery->pluck('domain', 'domains.domain_id');
|
||||
}
|
||||
})
|
||||
->required()
|
||||
->searchable()
|
||||
->preload(),
|
||||
TextInput::make('name')
|
||||
->helperText('This forms part of the email, the part before the @')
|
||||
->regex('/^[a-zA-Z0-9._-]+$/')
|
||||
->validationMessages([
|
||||
'regex' => 'Only letters, numbers, dots, underscores, and hyphens are allowed.',
|
||||
])
|
||||
->required(),
|
||||
Toggle::make('is_public')
|
||||
->label('Public group')
|
||||
->helperText('Anyone can email the group')
|
||||
->required()
|
||||
->default(true),
|
||||
Toggle::make('enabled')
|
||||
->required(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Vexim\Groups\Filament\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class GroupsTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('domain.domain')
|
||||
->label('Domain')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
IconColumn::make('is_public')
|
||||
->label('Public')
|
||||
->boolean(),
|
||||
IconColumn::make('enabled')
|
||||
->boolean(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user