Implement multi-tenancy support with OAuth2 authentication for tenant admins, Stripe integration for event purchases and credits ledger, new Filament resources for event purchases, updated API routes and middleware for tenant isolation and token guarding, added factories/seeders/migrations for new models (Tenant, EventPurchase, OAuth entities, etc.), enhanced tests, and documentation updates. Removed outdated DemoAchievementsSeeder.
This commit is contained in:
634
docs/plan-superadmin-filament.md
Normal file
634
docs/plan-superadmin-filament.md
Normal file
@@ -0,0 +1,634 @@
|
||||
# SuperAdmin Filament Resource Spezifikationen
|
||||
|
||||
## 1. Erweiterte TenantResource
|
||||
|
||||
### Form Schema (Erweiterung der bestehenden)
|
||||
```php
|
||||
// In TenantResource::form()
|
||||
TextInput::make('name')->required()->maxLength(255),
|
||||
TextInput::make('slug')->required()->unique()->maxLength(255),
|
||||
TextInput::make('contact_email')->email()->required()->maxLength(255),
|
||||
TextInput::make('event_credits_balance')->numeric()->default(1), // Free tier
|
||||
Select::make('subscription_tier')
|
||||
->options([
|
||||
'free' => 'Free',
|
||||
'starter' => 'Starter (€4.99/mo)',
|
||||
'pro' => 'Pro (€14.99/mo)',
|
||||
'agency' => 'Agency (€19.99/mo)',
|
||||
'lifetime' => 'Lifetime (€49.99)'
|
||||
])
|
||||
->default('free'),
|
||||
DateTimePicker::make('subscription_expires_at'),
|
||||
TextInput::make('total_revenue')->money('EUR')->readOnly(),
|
||||
KeyValue::make('features')->keyLabel('Feature')->valueLabel('Enabled'),
|
||||
Toggle::make('is_active')->label('Account Active'),
|
||||
Toggle::make('is_suspended')->label('Suspended')->default(false),
|
||||
```
|
||||
|
||||
### Table Columns (Erweiterung)
|
||||
```php
|
||||
// In TenantResource::table()
|
||||
Tables\Columns\TextColumn::make('name')->searchable()->sortable(),
|
||||
Tables\Columns\TextColumn::make('slug')->badge()->color('primary'),
|
||||
Tables\Columns\TextColumn::make('contact_email')->copyable(),
|
||||
Tables\Columns\TextColumn::make('event_credits_balance')
|
||||
->label('Credits')
|
||||
->badge()
|
||||
->color(fn ($state) => $state < 5 ? 'warning' : 'success'),
|
||||
Tables\Columns\TextColumn::make('subscription_tier')
|
||||
->badge()
|
||||
->color(fn (string $state): string => match($state) {
|
||||
'free' => 'gray',
|
||||
'starter' => 'info',
|
||||
'pro' => 'success',
|
||||
'agency' => 'warning',
|
||||
'lifetime' => 'danger',
|
||||
}),
|
||||
Tables\Columns\TextColumn::make('total_revenue')
|
||||
->money('EUR')
|
||||
->sortable(),
|
||||
Tables\Columns\IconColumn::make('is_active')
|
||||
->boolean()
|
||||
->color(fn (bool $state): string => $state ? 'success' : 'danger'),
|
||||
Tables\Columns\TextColumn::make('last_activity_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(),
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(),
|
||||
```
|
||||
|
||||
### Relations Manager (Purchase History)
|
||||
```php
|
||||
// In TenantResource\RelationManagers\PurchasesRelationManager
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->recordTitleAttribute('package_id')
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('package_id')->badge(),
|
||||
Tables\Columns\TextColumn::make('credits_added')->badge(),
|
||||
Tables\Columns\TextColumn::make('price')->money('EUR'),
|
||||
Tables\Columns\TextColumn::make('platform')->badge(),
|
||||
Tables\Columns\TextColumn::make('purchased_at')->dateTime(),
|
||||
Tables\Columns\TextColumn::make('transaction_id')->copyable(),
|
||||
])
|
||||
->filters([
|
||||
Tables\Filters\SelectFilter::make('platform')
|
||||
->options(['ios' => 'iOS', 'android' => 'Android', 'web' => 'Web']),
|
||||
Tables\Filters\SelectFilter::make('package_id')
|
||||
->options(['starter' => 'Starter', 'pro' => 'Pro', 'lifetime' => 'Lifetime']),
|
||||
])
|
||||
->headerActions([])
|
||||
->actions([Tables\Actions\ViewAction::make()])
|
||||
->bulkActions([Tables\Actions\BulkActionGroup::make([])]);
|
||||
}
|
||||
```
|
||||
|
||||
### Actions
|
||||
```php
|
||||
// In TenantResource::table()
|
||||
->actions([
|
||||
Actions\ViewAction::make(),
|
||||
Actions\EditAction::make(),
|
||||
Actions\Action::make('add_credits')
|
||||
->label('Credits hinzufügen')
|
||||
->icon('heroicon-o-plus')
|
||||
->form([
|
||||
Forms\Components\TextInput::make('credits')->numeric()->required()->minValue(1),
|
||||
Forms\Components\Textarea::make('reason')->label('Grund')->rows(3),
|
||||
])
|
||||
->action(function (Tenant $record, array $data) {
|
||||
$record->increment('event_credits_balance', $data['credits']);
|
||||
// Log purchase_history entry
|
||||
PurchaseHistory::create([
|
||||
'tenant_id' => $record->id,
|
||||
'package_id' => 'manual_adjustment',
|
||||
'credits_added' => $data['credits'],
|
||||
'price' => 0,
|
||||
'reason' => $data['reason'],
|
||||
]);
|
||||
}),
|
||||
Actions\Action::make('suspend')
|
||||
->label('Suspendieren')
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->action(fn (Tenant $record) => $record->update(['is_suspended' => true])),
|
||||
Actions\Action::make('export')
|
||||
->label('Daten exportieren')
|
||||
->icon('heroicon-o-arrow-down-tray')
|
||||
->url(fn (Tenant $record) => route('admin.tenants.export', $record))
|
||||
->openUrlInNewTab(),
|
||||
])
|
||||
```
|
||||
|
||||
## 2. Neue PurchaseHistoryResource
|
||||
|
||||
### Model (app/Models/PurchaseHistory.php)
|
||||
```php
|
||||
class PurchaseHistory extends Model
|
||||
{
|
||||
protected $table = 'purchase_history';
|
||||
protected $guarded = [];
|
||||
protected $casts = [
|
||||
'price' => 'decimal:2',
|
||||
'purchased_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
}
|
||||
|
||||
public function scopeByTenant($query, $tenantId)
|
||||
{
|
||||
return $query->where('tenant_id', $tenantId);
|
||||
}
|
||||
|
||||
public function scopeByPlatform($query, $platform)
|
||||
{
|
||||
return $query->where('platform', $platform);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Resource (app/Filament/Resources/PurchaseHistoryResource.php)
|
||||
```php
|
||||
class PurchaseHistoryResource extends Resource
|
||||
{
|
||||
protected static ?string $model = PurchaseHistory::class;
|
||||
protected static ?string $navigationIcon = 'heroicon-o-shopping-cart';
|
||||
protected static ?string $navigationGroup = 'Billing';
|
||||
protected static ?int $navigationSort = 10;
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Select::make('tenant_id')
|
||||
->label('Tenant')
|
||||
->relationship('tenant', 'name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->required(),
|
||||
Select::make('package_id')
|
||||
->label('Paket')
|
||||
->options([
|
||||
'starter_pack' => 'Starter Pack (5 Events)',
|
||||
'pro_pack' => 'Pro Pack (20 Events)',
|
||||
'lifetime_unlimited' => 'Lifetime Unlimited',
|
||||
'monthly_pro' => 'Pro Subscription',
|
||||
'monthly_agency' => 'Agency Subscription',
|
||||
])
|
||||
->required(),
|
||||
TextInput::make('credits_added')
|
||||
->label('Credits hinzugefügt')
|
||||
->numeric()
|
||||
->required()
|
||||
->minValue(0),
|
||||
TextInput::make('price')
|
||||
->label('Preis')
|
||||
->money('EUR')
|
||||
->required(),
|
||||
Select::make('platform')
|
||||
->label('Plattform')
|
||||
->options([
|
||||
'ios' => 'iOS',
|
||||
'android' => 'Android',
|
||||
'web' => 'Web',
|
||||
'manual' => 'Manuell',
|
||||
])
|
||||
->required(),
|
||||
TextInput::make('transaction_id')
|
||||
->label('Transaktions-ID')
|
||||
->maxLength(255),
|
||||
Textarea::make('reason')
|
||||
->label('Beschreibung')
|
||||
->maxLength(65535)
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->columns(2);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('tenant.name')
|
||||
->label('Tenant')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('package_id')
|
||||
->label('Paket')
|
||||
->badge()
|
||||
->color(fn (string $state): string => match($state) {
|
||||
'starter_pack' => 'info',
|
||||
'pro_pack' => 'success',
|
||||
'lifetime_unlimited' => 'danger',
|
||||
'monthly_pro' => 'warning',
|
||||
default => 'gray',
|
||||
}),
|
||||
Tables\Columns\TextColumn::make('credits_added')
|
||||
->label('Credits')
|
||||
->badge()
|
||||
->color('success'),
|
||||
Tables\Columns\TextColumn::make('price')
|
||||
->label('Preis')
|
||||
->money('EUR')
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('platform')
|
||||
->badge()
|
||||
->color(fn (string $state): string => match($state) {
|
||||
'ios' => 'info',
|
||||
'android' => 'success',
|
||||
'web' => 'warning',
|
||||
'manual' => 'gray',
|
||||
}),
|
||||
Tables\Columns\TextColumn::make('purchased_at')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('transaction_id')
|
||||
->copyable()
|
||||
->toggleable(),
|
||||
])
|
||||
->filters([
|
||||
Tables\Filters\Filter::make('created_at')
|
||||
->form([
|
||||
Forms\Components\DatePicker::make('started_from'),
|
||||
Forms\Components\DatePicker::make('ended_before'),
|
||||
])
|
||||
->query(function ($query, array $data) {
|
||||
return $query
|
||||
->when($data['started_from'], fn ($q) => $q->whereDate('created_at', '>=', $data['started_from']))
|
||||
->when($data['ended_before'], fn ($q) => $q->whereDate('created_at', '<=', $data['ended_before']));
|
||||
}),
|
||||
Tables\Filters\SelectFilter::make('platform')
|
||||
->options([
|
||||
'ios' => 'iOS',
|
||||
'android' => 'Android',
|
||||
'web' => 'Web',
|
||||
'manual' => 'Manuell',
|
||||
]),
|
||||
Tables\Filters\SelectFilter::make('package_id')
|
||||
->options([
|
||||
'starter_pack' => 'Starter Pack',
|
||||
'pro_pack' => 'Pro Pack',
|
||||
'lifetime_unlimited' => 'Lifetime',
|
||||
'monthly_pro' => 'Pro Subscription',
|
||||
'monthly_agency' => 'Agency Subscription',
|
||||
]),
|
||||
Tables\Filters\TernaryFilter::make('successful')
|
||||
->label('Erfolgreich')
|
||||
->trueLabel('Ja')
|
||||
->falseLabel('Nein')
|
||||
->placeholder('Alle')
|
||||
->query(fn ($query) => $query->whereNotNull('transaction_id')),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\ViewAction::make(),
|
||||
Tables\Actions\Action::make('refund')
|
||||
->label('Rückerstattung')
|
||||
->color('danger')
|
||||
->icon('heroicon-o-arrow-uturn-left')
|
||||
->requiresConfirmation()
|
||||
->visible(fn ($record) => $record->transaction_id && !$record->refunded)
|
||||
->action(function (PurchaseHistory $record) {
|
||||
// Stripe/RevenueCat Refund API call
|
||||
$record->update(['refunded' => true, 'refunded_at' => now()]);
|
||||
$record->tenant->decrement('event_credits_balance', $record->credits_added);
|
||||
}),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
ExportBulkAction::make()
|
||||
->label('Export CSV')
|
||||
->exporter(PurchaseHistoryExporter::class),
|
||||
]),
|
||||
])
|
||||
->emptyStateHeading('Keine Käufe gefunden')
|
||||
->emptyStateDescription('Erstelle den ersten Kauf oder überprüfe die Filter.');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. OAuthClientResource (für SuperAdmin)
|
||||
|
||||
### Resource (app/Filament/Resources/OAuthClientResource.php)
|
||||
```php
|
||||
class OAuthClientResource extends Resource
|
||||
{
|
||||
protected static ?string $model = OAuthClient::class;
|
||||
protected static ?string $navigationIcon = 'heroicon-o-key';
|
||||
protected static ?string $navigationGroup = 'Security';
|
||||
protected static ?int $navigationSort = 20;
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('client_id')
|
||||
->label('Client ID')
|
||||
->required()
|
||||
->unique(ignoreRecord: true)
|
||||
->maxLength(255),
|
||||
TextInput::make('client_secret')
|
||||
->label('Client Secret')
|
||||
->password()
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->dehydrateStateUsing(fn ($state) => Hash::make($state)),
|
||||
TextInput::make('name')
|
||||
->label('Name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Textarea::make('redirect_uris')
|
||||
->label('Redirect URIs')
|
||||
->rows(3)
|
||||
->placeholder('https://tenant-admin-app.example.com/callback')
|
||||
->required(),
|
||||
KeyValue::make('scopes')
|
||||
->label('Scopes')
|
||||
->keyLabel('Scope')
|
||||
->valueLabel('Beschreibung')
|
||||
->default([
|
||||
'tenant:read' => 'Lesen von Tenant-Daten',
|
||||
'tenant:write' => 'Schreiben von Tenant-Daten',
|
||||
'tenant:admin' => 'Administrative Aktionen',
|
||||
]),
|
||||
Toggle::make('is_active')
|
||||
->label('Aktiv')
|
||||
->default(true),
|
||||
Textarea::make('description')
|
||||
->label('Beschreibung')
|
||||
->maxLength(65535)
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->columns(2);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('client_id')
|
||||
->copyable()
|
||||
->sortable(),
|
||||
Tables\Columns\IconColumn::make('is_active')
|
||||
->boolean()
|
||||
->color('success'),
|
||||
Tables\Columns\TextColumn::make('redirect_uris')
|
||||
->limit(50)
|
||||
->tooltip(function (Tables\Columns\Column $column): ?string {
|
||||
$state = $column->getState();
|
||||
if (is_array($state)) {
|
||||
return implode("\n", $state);
|
||||
}
|
||||
return $state;
|
||||
}),
|
||||
Tables\Columns\TextColumn::make('scopes')
|
||||
->badge()
|
||||
->color('info'),
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(),
|
||||
])
|
||||
->filters([
|
||||
Tables\Filters\TernaryFilter::make('is_active')
|
||||
->label('Status')
|
||||
->trueLabel('Aktiv')
|
||||
->falseLabel('Inaktiv')
|
||||
->placeholder('Alle'),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\ViewAction::make(),
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\Action::make('regenerate_secret')
|
||||
->label('Secret neu generieren')
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->color('warning')
|
||||
->requiresConfirmation()
|
||||
->action(function (OAuthClient $record) {
|
||||
$record->update(['client_secret' => Str::random(40)]);
|
||||
}),
|
||||
Tables\Actions\DeleteAction::make()
|
||||
->requiresConfirmation()
|
||||
->before(function (OAuthClient $record) {
|
||||
// Revoke all associated tokens
|
||||
RefreshToken::where('client_id', $record->client_id)->delete();
|
||||
}),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. SuperAdmin Dashboard Widgets
|
||||
|
||||
### RevenueChart Widget
|
||||
```php
|
||||
// app/Filament/Widgets/RevenueChart.php
|
||||
class RevenueChart extends LineChartWidget
|
||||
{
|
||||
protected static ?string $heading = 'Monatliche Einnahmen';
|
||||
protected static ?int $sort = 1;
|
||||
|
||||
protected function getData(): array
|
||||
{
|
||||
$data = PurchaseHistory::selectRaw('
|
||||
DATE_FORMAT(purchased_at, "%Y-%m") as month,
|
||||
SUM(price) as revenue
|
||||
')
|
||||
->groupBy('month')
|
||||
->orderBy('month')
|
||||
->limit(12)
|
||||
->get();
|
||||
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => 'Einnahmen (€)',
|
||||
'data' => $data->pluck('revenue')->values(),
|
||||
'borderColor' => '#3B82F6',
|
||||
'backgroundColor' => 'rgba(59, 130, 246, 0.1)',
|
||||
],
|
||||
],
|
||||
'labels' => $data->pluck('month')->values(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFilters(): ?array
|
||||
{
|
||||
return [
|
||||
'12_months' => 'Letzte 12 Monate',
|
||||
'6_months' => 'Letzte 6 Monate',
|
||||
'3_months' => 'Letzte 3 Monate',
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### TopTenantsWidget
|
||||
```php
|
||||
// app/Filament/Widgets/TopTenantsByRevenue.php
|
||||
class TopTenantsByRevenue extends TableWidget
|
||||
{
|
||||
protected static ?string $heading = 'Top Tenants nach Einnahmen';
|
||||
protected static ?int $sort = 2;
|
||||
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return Tenant::withCount('purchases')
|
||||
->withSum('purchases', 'price')
|
||||
->orderByDesc('purchases_sum_price')
|
||||
->limit(10);
|
||||
}
|
||||
|
||||
protected function getTableColumns(): array
|
||||
{
|
||||
return [
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->label('Tenant')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('purchases_sum_price')
|
||||
->label('Gesamt (€)')
|
||||
->money('EUR')
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('purchases_count')
|
||||
->label('Käufe')
|
||||
->badge()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('event_credits_balance')
|
||||
->label('Aktuelle Credits')
|
||||
->badge(),
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### CreditAlertsWidget
|
||||
```php
|
||||
// app/Filament/Widgets/CreditAlerts.php
|
||||
class CreditAlerts extends StatsOverviewWidget
|
||||
{
|
||||
protected function getCards(): array
|
||||
{
|
||||
$lowBalanceTenants = Tenant::where('event_credits_balance', '<', 5)
|
||||
->where('is_active', true)
|
||||
->count();
|
||||
|
||||
$totalRevenueThisMonth = PurchaseHistory::whereMonth('purchased_at', now()->month)
|
||||
->sum('price');
|
||||
|
||||
$activeSubscriptions = Tenant::whereNotNull('subscription_expires_at')
|
||||
->where('subscription_expires_at', '>', now())
|
||||
->count();
|
||||
|
||||
return [
|
||||
StatsOverviewWidget\Stat::make('Tenants mit niedrigen Credits', $lowBalanceTenants)
|
||||
->description('Benötigen möglicherweise Support')
|
||||
->descriptionIcon('heroicon-m-exclamation-triangle')
|
||||
->color('warning')
|
||||
->url(route('filament.admin.resources.tenants.index')),
|
||||
StatsOverviewWidget\Stat::make('Einnahmen diesen Monat', $totalRevenueThisMonth)
|
||||
->description('€')
|
||||
->descriptionIcon('heroicon-m-currency-euro')
|
||||
->color('success'),
|
||||
StatsOverviewWidget\Stat::make('Aktive Abos', $activeSubscriptions)
|
||||
->description('Recurring Revenue')
|
||||
->descriptionIcon('heroicon-m-arrow-trending-up')
|
||||
->color('info'),
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Permissions und Middleware
|
||||
|
||||
### Policy für SuperAdmin
|
||||
```php
|
||||
// app/Policies/TenantPolicy.php
|
||||
class TenantPolicy
|
||||
{
|
||||
public function before(User $user): ?bool
|
||||
{
|
||||
if ($user->role === 'superadmin') {
|
||||
return true;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $user->role === 'superadmin';
|
||||
}
|
||||
|
||||
public function view(User $user, Tenant $tenant): bool
|
||||
{
|
||||
return $user->role === 'superadmin' || $user->tenant_id === $tenant->id;
|
||||
}
|
||||
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return $user->role === 'superadmin';
|
||||
}
|
||||
|
||||
public function update(User $user, Tenant $tenant): bool
|
||||
{
|
||||
return $user->role === 'superadmin';
|
||||
}
|
||||
|
||||
public function delete(User $user, Tenant $tenant): bool
|
||||
{
|
||||
return $user->role === 'superadmin';
|
||||
}
|
||||
|
||||
public function addCredits(User $user, Tenant $tenant): bool
|
||||
{
|
||||
return $user->role === 'superadmin';
|
||||
}
|
||||
|
||||
public function suspend(User $user, Tenant $tenant): bool
|
||||
{
|
||||
return $user->role === 'superadmin';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### SuperAdmin Middleware
|
||||
```php
|
||||
// app/Http/Middleware/SuperAdminMiddleware.php
|
||||
class SuperAdminMiddleware
|
||||
{
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if (!auth()->check() || auth()->user()->role !== 'superadmin') {
|
||||
abort(403, 'SuperAdmin-Zugriff erforderlich');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Implementierungsreihenfolge
|
||||
1. **Model & Migration:** PurchaseHistory, OAuthClient Tabellen
|
||||
2. **TenantResource:** Erweiterung mit neuen Feldern, Relations, Actions
|
||||
3. **PurchaseHistoryResource:** Vollständige CRUD mit Filtern und Export
|
||||
4. **OAuthClientResource:** Management für OAuth-Clients
|
||||
5. **Widgets:** Dashboard-Übersicht mit Charts und Stats
|
||||
6. **Policies & Middleware:** Security für SuperAdmin-Funktionen
|
||||
7. **Tests:** Feature-Tests für Credit-Management, Permissions
|
||||
|
||||
Dieser Plan erweitert den SuperAdmin-Bereich um umfassende Billing- und Security-Management-Funktionen.
|
||||
Reference in New Issue
Block a user