Implement superadmin audit log for mutations
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-01-02 11:57:49 +01:00
parent 8b4950c79d
commit 412ecbe691
82 changed files with 1766 additions and 192 deletions

View File

@@ -59,7 +59,7 @@
{"id":"fotospiel-app-gsv","title":"Localized SEO: validate hreflang via Search Console/Lighthouse","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-01T16:02:36.4821072+01:00","created_by":"soeren","updated_at":"2026-01-01T16:02:36.4821072+01:00"}
{"id":"fotospiel-app-hbt","title":"Moderation queue for guest content","description":"Queue for flagged guest content (photos, feedback). Bulk actions to hide/delete/resolve with audit.","notes":"Land the plane: tests run (FilamentPanelNavigationTest, PhotoModerationQueueTest, TenantFeedbackModerationQueueTest, TenantLifecycle*), migrations added for photo + feedback moderation, no follow-up blockers.","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-01-01T14:18:37.777772819+01:00","updated_at":"2026-01-01T18:50:57.274743566+01:00","closed_at":"2026-01-01T18:46:09.677538603+01:00"}
{"id":"fotospiel-app-ihd","title":"Superadmin control surface spec and access matrix","description":"Define the minimal superadmin control surface, permissions, and mapping to tenant/guest responsibilities. Document scope and non-goals.","notes":"Added superadmin control surface + access matrix to docs/ops/operations-manual.md (Section 1.1), including non-goals and role scope.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T14:18:10.789147344+01:00","updated_at":"2026-01-01T19:52:54.391624328+01:00","closed_at":"2026-01-01T19:52:54.391628452+01:00"}
{"id":"fotospiel-app-iyc","title":"Superadmin audit log for admin actions","description":"Audit trail for superadmin actions without PII payloads.","status":"open","priority":2,"issue_type":"feature","created_at":"2026-01-01T14:20:19.043695952+01:00","updated_at":"2026-01-01T14:20:19.043695952+01:00"}
{"id":"fotospiel-app-iyc","title":"Superadmin audit log for admin actions","description":"Audit trail for superadmin actions without PII payloads.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-01-01T14:20:19.043695952+01:00","updated_at":"2026-01-02T11:57:23.328889123+01:00","closed_at":"2026-01-02T11:57:23.328889123+01:00","close_reason":"Closed"}
{"id":"fotospiel-app-iyh","title":"Security review follow-ups: signed URL TTLs, guest asset throttles, CORS allowlist, logging hygiene","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-01T16:05:42.642109576+01:00","created_by":"soeren","updated_at":"2026-01-01T16:05:42.642109576+01:00"}
{"id":"fotospiel-app-jk4","title":"Checkout refactor: CheckoutController + marketing route alignment","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:06:21.088319132+01:00","created_by":"soeren","updated_at":"2026-01-01T16:06:26.663419594+01:00","closed_at":"2026-01-01T16:06:26.663419594+01:00","close_reason":"Completed in codebase (verified)"}
{"id":"fotospiel-app-jqy","title":"Tenant admin onboarding: Playwright skeleton for welcome flow","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:08:11.226297707+01:00","created_by":"soeren","updated_at":"2026-01-01T16:08:16.827679424+01:00","closed_at":"2026-01-01T16:08:16.827679424+01:00","close_reason":"Completed in codebase (verified)"}

View File

@@ -1 +1 @@
fotospiel-app-z2k
fotospiel-app-iyc

View File

@@ -6,6 +6,7 @@ use App\Filament\Blog\Resources\CategoryResource\Pages;
use App\Filament\Blog\Traits\HasContentEditor;
use App\Filament\Clusters\RareAdmin\RareAdminCluster;
use App\Models\BlogCategory;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
@@ -24,6 +25,7 @@ use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Illuminate\Support\Str;
@@ -116,38 +118,25 @@ class CategoryResource extends Resource
$data['description_de'] = $descArray['de'] ?? '';
$data['description_en'] = $descArray['en'] ?? '';
\Illuminate\Support\Facades\Log::info('BeforeFill Description Extraction:', [
'descJson' => $descJson,
'descArray' => $descArray,
'description_de' => $data['description_de'],
'description_en' => $data['description_en'],
]);
return $data;
}
public static function mutateFormDataBeforeCreate(array $data): array
{
\Illuminate\Support\Facades\Log::info('mutateFormDataBeforeCreate Input Data:', ['data' => $data]);
$nameData = [
'de' => $data['name_de'] ?? '',
'en' => $data['name_en'] ?? '',
];
$data['name'] = json_encode($nameData);
\Illuminate\Support\Facades\Log::info('mutateFormDataBeforeCreate Name JSON:', ['name' => $nameData]);
$descData = [
'de' => $data['description_de'] ?? '',
'en' => $data['description_en'] ?? '',
];
$data['description'] = json_encode($descData);
\Illuminate\Support\Facades\Log::info('mutateFormDataBeforeCreate Description JSON:', ['description' => $descData]);
unset($data['name_de'], $data['name_en'], $data['description_de'], $data['description_en']);
\Illuminate\Support\Facades\Log::info('mutateFormDataBeforeCreate Final Data:', $data);
return $data;
}
@@ -185,11 +174,28 @@ class CategoryResource extends Resource
//
])
->actions([
EditAction::make(),
EditAction::make()
->after(fn (array $data, BlogCategory $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'updated',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
static::class
)),
])
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
DeleteBulkAction::make()
->after(function (Collection $records): void {
$logger = app(SuperAdminAuditLogger::class);
foreach ($records as $record) {
$logger->recordModelMutation(
'deleted',
$record,
source: static::class
);
}
}),
]),
]);
}

View File

@@ -3,9 +3,9 @@
namespace App\Filament\Blog\Resources\CategoryResource\Pages;
use App\Filament\Blog\Resources\CategoryResource;
use Filament\Resources\Pages\CreateRecord;
use App\Filament\Resources\Pages\AuditedCreateRecord;
class CreateCategory extends CreateRecord
class CreateCategory extends AuditedCreateRecord
{
protected static string $resource = CategoryResource::class;

View File

@@ -3,17 +3,23 @@
namespace App\Filament\Blog\Resources\CategoryResource\Pages;
use App\Filament\Blog\Resources\CategoryResource;
use App\Filament\Resources\Pages\AuditedEditRecord;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditCategory extends EditRecord
class EditCategory extends AuditedEditRecord
{
protected static string $resource = CategoryResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
Actions\DeleteAction::make()
->after(fn ($record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'deleted',
$record,
source: static::class
)),
];
}
@@ -24,7 +30,7 @@ class EditCategory extends EditRecord
'description_de' => 'nullable|string',
'name_en' => 'nullable|string|max:255',
'description_en' => 'nullable|string',
'slug' => 'required|string|max:255|unique:blog_categories,slug,' . $this->record->id,
'slug' => 'required|string|max:255|unique:blog_categories,slug,'.$this->record->id,
'is_visible' => 'boolean',
];
}
@@ -40,12 +46,13 @@ class EditCategory extends EditRecord
public function save(bool $shouldRedirect = true, bool $shouldSendSavedNotification = true): void
{
$state = $this->form->getState();
\Illuminate\Support\Facades\Log::info('EditCategory Save - Full State:', $state);
$data = $state['data'] ?? $state;
$data = \App\Filament\Blog\Resources\CategoryResource::mutateFormDataBeforeSave($data);
$this->record->update($data);
parent::afterSave();
}
}

View File

@@ -7,6 +7,7 @@ use App\Filament\Blog\Traits\HasContentEditor;
use App\Filament\Clusters\RareAdmin\RareAdminCluster;
use App\Models\BlogCategory;
use App\Models\BlogPost;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
@@ -29,6 +30,7 @@ use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\TernaryFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Illuminate\Support\Str;
@@ -243,11 +245,27 @@ class PostResource extends Resource
->actions([
DeleteAction::make()
->icon('heroicon-o-trash')
->label(''),
->label('')
->after(fn (BlogPost $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'deleted',
$record,
source: static::class
)),
])
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
DeleteBulkAction::make()
->after(function (Collection $records): void {
$logger = app(SuperAdminAuditLogger::class);
foreach ($records as $record) {
$logger->recordModelMutation(
'deleted',
$record,
source: static::class
);
}
}),
]),
]);
}

View File

@@ -3,9 +3,9 @@
namespace App\Filament\Blog\Resources\PostResource\Pages;
use App\Filament\Blog\Resources\PostResource;
use Filament\Resources\Pages\CreateRecord;
use App\Filament\Resources\Pages\AuditedCreateRecord;
class CreatePost extends CreateRecord
class CreatePost extends AuditedCreateRecord
{
protected static string $resource = PostResource::class;
}

View File

@@ -3,10 +3,11 @@
namespace App\Filament\Blog\Resources\PostResource\Pages;
use App\Filament\Blog\Resources\PostResource;
use App\Filament\Resources\Pages\AuditedEditRecord;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditPost extends EditRecord
class EditPost extends AuditedEditRecord
{
protected static string $resource = PostResource::class;
@@ -14,7 +15,12 @@ class EditPost extends EditRecord
{
return [
Actions\ViewAction::make(),
Actions\DeleteAction::make(),
Actions\DeleteAction::make()
->after(fn ($record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'deleted',
$record,
source: static::class
)),
];
}

View File

@@ -3,11 +3,12 @@
namespace App\Filament\Clusters\DailyOps\Resources\Photos\Pages;
use App\Filament\Clusters\DailyOps\Resources\Photos\PhotoResource;
use App\Filament\Resources\Pages\AuditedEditRecord;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions\DeleteAction;
use Filament\Actions\ViewAction;
use Filament\Resources\Pages\EditRecord;
class EditPhoto extends EditRecord
class EditPhoto extends AuditedEditRecord
{
protected static string $resource = PhotoResource::class;
@@ -15,7 +16,12 @@ class EditPhoto extends EditRecord
{
return [
ViewAction::make(),
DeleteAction::make(),
DeleteAction::make()
->after(fn ($record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'deleted',
$record,
source: static::class
)),
];
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Filament\Clusters\DailyOps\Resources\Photos\Tables;
use App\Models\Event;
use App\Models\Photo;
use App\Models\Tenant;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions\Action;
use Filament\Actions\BulkAction;
use Filament\Actions\BulkActionGroup;
@@ -208,6 +209,18 @@ class PhotosTable
'moderated_at' => now(),
'moderated_by' => Filament::auth()->id(),
]);
app(SuperAdminAuditLogger::class)->record(
'photo.'.$status,
$record,
SuperAdminAuditLogger::fieldsMetadata([
'status',
'moderation_notes',
'moderated_at',
'moderated_by',
]),
source: self::class
);
}
private static function applyModerationToRecords(Collection $records, string $status, ?string $notes): int
@@ -215,7 +228,7 @@ class PhotosTable
$moderatedAt = now();
$moderatedBy = Filament::auth()->id();
return Photo::query()
$updated = Photo::query()
->whereIn('id', $records->pluck('id'))
->where('status', 'pending')
->update([
@@ -224,6 +237,24 @@ class PhotosTable
'moderated_at' => $moderatedAt,
'moderated_by' => $moderatedBy,
]);
$logger = app(SuperAdminAuditLogger::class);
foreach ($records as $record) {
$logger->record(
'photo.'.$status,
$record,
SuperAdminAuditLogger::fieldsMetadata([
'status',
'moderation_notes',
'moderated_at',
'moderated_by',
]),
source: self::class
);
}
return $updated;
}
private static function statusLabels(): array

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Filament\Clusters\RareAdmin\Resources\SuperAdminActionLogs\Pages;
use App\Filament\Clusters\RareAdmin\Resources\SuperAdminActionLogs\SuperAdminActionLogResource;
use Filament\Resources\Pages\ManageRecords;
class ManageSuperAdminActionLogs extends ManageRecords
{
protected static string $resource = SuperAdminActionLogResource::class;
protected function getHeaderActions(): array
{
return [];
}
}

View File

@@ -0,0 +1,123 @@
<?php
namespace App\Filament\Clusters\RareAdmin\Resources\SuperAdminActionLogs;
use App\Filament\Clusters\RareAdmin\RareAdminCluster;
use App\Filament\Clusters\RareAdmin\Resources\SuperAdminActionLogs\Pages\ManageSuperAdminActionLogs;
use App\Models\SuperAdminActionLog;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use UnitEnum;
class SuperAdminActionLogResource extends Resource
{
protected static ?string $model = SuperAdminActionLog::class;
protected static ?string $cluster = RareAdminCluster::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedClipboardDocumentList;
protected static ?int $navigationSort = 95;
protected static ?string $recordTitleAttribute = 'action';
public static function getNavigationGroup(): UnitEnum|string|null
{
return __('admin.nav.infrastructure');
}
public static function getNavigationLabel(): string
{
return 'Audit log';
}
public static function canCreate(): bool
{
return false;
}
public static function canEdit($record): bool
{
return false;
}
public static function canDelete($record): bool
{
return false;
}
public static function canDeleteAny(): bool
{
return false;
}
public static function form(Schema $schema): Schema
{
return $schema
->components([
//
]);
}
public static function table(Table $table): Table
{
return $table
->recordTitleAttribute('action')
->columns([
TextColumn::make('action')
->badge()
->searchable()
->sortable(),
TextColumn::make('actor.fullName')
->label('Actor')
->sortable()
->searchable(),
TextColumn::make('subject_type')
->label('Subject')
->formatStateUsing(fn (?string $state) => $state ? class_basename($state) : '—')
->searchable()
->toggleable(),
TextColumn::make('subject_id')
->label('Subject ID')
->sortable()
->toggleable(),
TextColumn::make('metadata')
->label('Fields')
->formatStateUsing(function ($state): string {
if (! is_array($state)) {
return '—';
}
$fields = $state['fields'] ?? [];
return $fields ? implode(', ', $fields) : '—';
})
->toggleable(isToggledHiddenByDefault: true)
->wrap(),
TextColumn::make('source')
->label('Source')
->limit(40)
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('occurred_at')
->label('Timestamp')
->dateTime()
->sortable(),
])
->filters([
//
])
->recordActions([])
->toolbarActions([]);
}
public static function getPages(): array
{
return [
'index' => ManageSuperAdminActionLogs::route('/'),
];
}
}

View File

@@ -3,15 +3,17 @@
namespace App\Filament\Resources\Coupons\Pages;
use App\Filament\Resources\Coupons\CouponResource;
use App\Filament\Resources\Pages\AuditedCreateRecord;
use App\Jobs\SyncCouponToPaddle;
use Filament\Resources\Pages\CreateRecord;
class CreateCoupon extends CreateRecord
class CreateCoupon extends AuditedCreateRecord
{
protected static string $resource = CouponResource::class;
protected function afterCreate(): void
{
parent::afterCreate();
SyncCouponToPaddle::dispatch($this->record);
}
}

View File

@@ -3,14 +3,15 @@
namespace App\Filament\Resources\Coupons\Pages;
use App\Filament\Resources\Coupons\CouponResource;
use App\Filament\Resources\Pages\AuditedEditRecord;
use App\Jobs\SyncCouponToPaddle;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions\DeleteAction;
use Filament\Actions\ForceDeleteAction;
use Filament\Actions\RestoreAction;
use Filament\Actions\ViewAction;
use Filament\Resources\Pages\EditRecord;
class EditCoupon extends EditRecord
class EditCoupon extends AuditedEditRecord
{
protected static string $resource = CouponResource::class;
@@ -19,14 +20,34 @@ class EditCoupon extends EditRecord
return [
ViewAction::make(),
DeleteAction::make()
->after(fn ($record) => SyncCouponToPaddle::dispatch($record, true)),
ForceDeleteAction::make(),
RestoreAction::make(),
->after(function ($record): void {
app(SuperAdminAuditLogger::class)->recordModelMutation(
'deleted',
$record,
source: static::class
);
SyncCouponToPaddle::dispatch($record, true);
}),
ForceDeleteAction::make()
->after(fn ($record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'force_deleted',
$record,
source: static::class
)),
RestoreAction::make()
->after(fn ($record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'restored',
$record,
source: static::class
)),
];
}
protected function afterSave(): void
{
parent::afterSave();
SyncCouponToPaddle::dispatch($this->record);
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Filament\Resources\Coupons\Pages;
use App\Filament\Resources\Coupons\CouponResource;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord;
@@ -13,7 +14,13 @@ class ViewCoupon extends ViewRecord
protected function getHeaderActions(): array
{
return [
EditAction::make(),
EditAction::make()
->after(fn (array $data, $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'updated',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
static::class
)),
];
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Filament\Resources\Coupons\Tables;
use App\Enums\CouponStatus;
use App\Enums\CouponType;
use App\Jobs\SyncCouponToPaddle;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions\Action;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
@@ -18,6 +19,7 @@ use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Filters\TernaryFilter;
use Filament\Tables\Filters\TrashedFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
class CouponsTable
@@ -95,7 +97,13 @@ class CouponsTable
])
->recordActions([
ViewAction::make(),
EditAction::make(),
EditAction::make()
->after(fn (array $data, $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'updated',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
static::class
)),
Action::make('sync')
->label(__('Sync to Paddle'))
->icon('heroicon-m-arrow-path')
@@ -104,9 +112,42 @@ class CouponsTable
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
ForceDeleteBulkAction::make(),
RestoreBulkAction::make(),
DeleteBulkAction::make()
->after(function (Collection $records): void {
$logger = app(SuperAdminAuditLogger::class);
foreach ($records as $record) {
$logger->recordModelMutation(
'deleted',
$record,
source: static::class
);
}
}),
ForceDeleteBulkAction::make()
->after(function (Collection $records): void {
$logger = app(SuperAdminAuditLogger::class);
foreach ($records as $record) {
$logger->recordModelMutation(
'force_deleted',
$record,
source: static::class
);
}
}),
RestoreBulkAction::make()
->after(function (Collection $records): void {
$logger = app(SuperAdminAuditLogger::class);
foreach ($records as $record) {
$logger->recordModelMutation(
'restored',
$record,
source: static::class
);
}
}),
]),
]);
}

View File

@@ -5,6 +5,7 @@ namespace App\Filament\Resources;
use App\Filament\Clusters\WeeklyOps\WeeklyOpsCluster;
use App\Filament\Resources\EmotionResource\Pages;
use App\Models\Emotion;
use App\Services\Audit\SuperAdminAuditLogger;
use BackedEnum;
use Filament\Actions;
use Filament\Forms\Components\MarkdownEditor;
@@ -17,6 +18,7 @@ use Filament\Schemas\Components\Tabs\Tab as SchemaTab;
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Collection;
use UnitEnum;
class EmotionResource extends Resource
@@ -116,10 +118,27 @@ class EmotionResource extends Resource
])
->filters([])
->actions([
Actions\EditAction::make(),
Actions\EditAction::make()
->after(fn (array $data, Emotion $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'updated',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
static::class
)),
])
->bulkActions([
Actions\DeleteBulkAction::make(),
Actions\DeleteBulkAction::make()
->after(function (Collection $records): void {
$logger = app(SuperAdminAuditLogger::class);
foreach ($records as $record) {
$logger->recordModelMutation(
'deleted',
$record,
source: static::class
);
}
}),
]);
}

View File

@@ -3,6 +3,7 @@
namespace App\Filament\Resources\EmotionResource\Pages;
use App\Filament\Resources\EmotionResource;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions;
use Filament\Resources\Pages\ManageRecords;
@@ -13,7 +14,13 @@ class ManageEmotions extends ManageRecords
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
Actions\CreateAction::make()
->after(fn (array $data, $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'created',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
static::class
)),
Actions\Action::make('import')
->label(__('admin.common.import_csv'))
->icon('heroicon-o-arrow-up-tray')

View File

@@ -6,6 +6,7 @@ use App\Exports\EventPurchaseExporter;
use App\Filament\Clusters\WeeklyOps\WeeklyOpsCluster;
use App\Filament\Resources\EventPurchaseResource\Pages;
use App\Models\EventPurchase;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions\Action;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
@@ -23,6 +24,7 @@ use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Filters\TernaryFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Log;
class EventPurchaseResource extends Resource
@@ -174,11 +176,29 @@ class EventPurchaseResource extends Resource
->action(function (EventPurchase $record) {
$record->update(['refunded_at' => now()]);
Log::info('Refund processed for purchase ID: '.$record->id);
app(SuperAdminAuditLogger::class)->record(
'event_purchase.refunded',
$record,
SuperAdminAuditLogger::fieldsMetadata(['refunded_at']),
source: static::class
);
}),
])
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
DeleteBulkAction::make()
->after(function (Collection $records): void {
$logger = app(SuperAdminAuditLogger::class);
foreach ($records as $record) {
$logger->recordModelMutation(
'deleted',
$record,
source: static::class
);
}
}),
ExportBulkAction::make()
->label('Export CSV')
->exporter(EventPurchaseExporter::class),

View File

@@ -3,9 +3,9 @@
namespace App\Filament\Resources\EventPurchaseResource\Pages;
use App\Filament\Resources\EventPurchaseResource;
use Filament\Resources\Pages\CreateRecord;
use App\Filament\Resources\Pages\AuditedCreateRecord;
class CreateEventPurchase extends CreateRecord
class CreateEventPurchase extends AuditedCreateRecord
{
protected static string $resource = EventPurchaseResource::class;
}

View File

@@ -3,10 +3,11 @@
namespace App\Filament\Resources\EventPurchaseResource\Pages;
use App\Filament\Resources\EventPurchaseResource;
use App\Filament\Resources\Pages\AuditedEditRecord;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditEventPurchase extends EditRecord
class EditEventPurchase extends AuditedEditRecord
{
protected static string $resource = EventPurchaseResource::class;
@@ -14,7 +15,12 @@ class EditEventPurchase extends EditRecord
{
return [
Actions\ViewAction::make(),
Actions\DeleteAction::make(),
Actions\DeleteAction::make()
->after(fn ($record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'deleted',
$record,
source: static::class
)),
];
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Filament\Resources\EventPurchaseResource\Pages;
use App\Filament\Resources\EventPurchaseResource;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions;
use Filament\Resources\Pages\ViewRecord;
@@ -13,7 +14,13 @@ class ViewEventPurchase extends ViewRecord
protected function getHeaderActions(): array
{
return [
Actions\EditAction::make(),
Actions\EditAction::make()
->after(fn (array $data, $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'updated',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
static::class
)),
];
}
}

View File

@@ -9,6 +9,7 @@ use App\Models\Event;
use App\Models\EventJoinTokenEvent;
use App\Models\EventType;
use App\Models\Tenant;
use App\Services\Audit\SuperAdminAuditLogger;
use App\Support\JoinTokenLayoutRegistry;
use BackedEnum;
use Carbon\Carbon;
@@ -22,6 +23,7 @@ use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Collection;
use UnitEnum;
class EventResource extends Resource
@@ -133,11 +135,26 @@ class EventResource extends Resource
])
->filters([])
->actions([
Actions\EditAction::make(),
Actions\EditAction::make()
->after(fn (array $data, Event $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'updated',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
static::class
)),
Actions\Action::make('toggle')
->label(__('admin.events.actions.toggle_active'))
->icon('heroicon-o-power')
->action(fn ($record) => $record->update(['is_active' => ! $record->is_active])),
->action(function (Event $record): void {
$record->update(['is_active' => ! $record->is_active]);
app(SuperAdminAuditLogger::class)->record(
'event.toggled',
$record,
SuperAdminAuditLogger::fieldsMetadata(['is_active']),
source: static::class
);
}),
Actions\Action::make('download_photos')
->label(__('admin.events.actions.download_photos'))
->icon('heroicon-o-arrow-down-tray')
@@ -243,7 +260,18 @@ class EventResource extends Resource
}),
])
->bulkActions([
Actions\DeleteBulkAction::make(),
Actions\DeleteBulkAction::make()
->after(function (Collection $records): void {
$logger = app(SuperAdminAuditLogger::class);
foreach ($records as $record) {
$logger->recordModelMutation(
'deleted',
$record,
source: static::class
);
}
}),
]);
}

View File

@@ -3,9 +3,9 @@
namespace App\Filament\Resources\EventResource\Pages;
use App\Filament\Resources\EventResource;
use Filament\Resources\Pages\CreateRecord;
use App\Filament\Resources\Pages\AuditedCreateRecord;
class CreateEvent extends CreateRecord
class CreateEvent extends AuditedCreateRecord
{
protected static string $resource = EventResource::class;
}

View File

@@ -3,9 +3,9 @@
namespace App\Filament\Resources\EventResource\Pages;
use App\Filament\Resources\EventResource;
use Filament\Resources\Pages\EditRecord;
use App\Filament\Resources\Pages\AuditedEditRecord;
class EditEvent extends EditRecord
class EditEvent extends AuditedEditRecord
{
protected static string $resource = EventResource::class;
}

View File

@@ -3,21 +3,21 @@
namespace App\Filament\Resources\EventResource\RelationManagers;
use App\Models\EventPackage;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
@@ -93,15 +93,43 @@ class EventPackagesRelationManager extends RelationManager
])
->filters([])
->headerActions([
CreateAction::make(),
CreateAction::make()
->after(fn (array $data, EventPackage $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'created',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
static::class
)),
])
->actions([
EditAction::make(),
DeleteAction::make(),
EditAction::make()
->after(fn (array $data, EventPackage $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'updated',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
static::class
)),
DeleteAction::make()
->after(fn (EventPackage $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'deleted',
$record,
source: static::class
)),
])
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
DeleteBulkAction::make()
->after(function (Collection $records): void {
$logger = app(SuperAdminAuditLogger::class);
foreach ($records as $record) {
$logger->recordModelMutation(
'deleted',
$record,
source: static::class
);
}
}),
]),
]);
}

View File

@@ -5,6 +5,7 @@ namespace App\Filament\Resources;
use App\Filament\Clusters\WeeklyOps\WeeklyOpsCluster;
use App\Filament\Resources\EventTypeResource\Pages;
use App\Models\EventType;
use App\Services\Audit\SuperAdminAuditLogger;
use BackedEnum;
use Filament\Actions;
use Filament\Forms\Components\KeyValue;
@@ -16,6 +17,7 @@ use Filament\Schemas\Components\Tabs\Tab as SchemaTab;
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Collection;
use UnitEnum;
class EventTypeResource extends Resource
@@ -104,10 +106,27 @@ class EventTypeResource extends Resource
])
->filters([])
->actions([
Actions\EditAction::make(),
Actions\EditAction::make()
->after(fn (array $data, EventType $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'updated',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
static::class
)),
])
->bulkActions([
Actions\DeleteBulkAction::make(),
Actions\DeleteBulkAction::make()
->after(function (Collection $records): void {
$logger = app(SuperAdminAuditLogger::class);
foreach ($records as $record) {
$logger->recordModelMutation(
'deleted',
$record,
source: static::class
);
}
}),
]);
}

View File

@@ -3,6 +3,7 @@
namespace App\Filament\Resources\EventTypeResource\Pages;
use App\Filament\Resources\EventTypeResource;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions;
use Filament\Resources\Pages\ManageRecords;
@@ -13,7 +14,13 @@ class ManageEventTypes extends ManageRecords
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
Actions\CreateAction::make()
->after(fn (array $data, $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'created',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
static::class
)),
];
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Filament\Resources;
use App\Filament\Clusters\WeeklyOps\WeeklyOpsCluster;
use App\Filament\Resources\GiftVoucherResource\Pages;
use App\Models\GiftVoucher;
use App\Services\Audit\SuperAdminAuditLogger;
use App\Services\GiftVouchers\GiftVoucherService;
use BackedEnum;
use Carbon\Carbon;
@@ -97,6 +98,13 @@ class GiftVoucherResource extends Resource
->visible(fn (GiftVoucher $record): bool => $record->canBeRefunded())
->action(function (GiftVoucher $record, GiftVoucherService $service): void {
$service->refund($record, 'customer_request');
app(SuperAdminAuditLogger::class)->record(
'gift_voucher.refunded',
$record,
SuperAdminAuditLogger::fieldsMetadata(['status', 'refunded_at']),
source: static::class
);
})
->successNotificationTitle('Gutschein erstattet'),
Action::make('resend')
@@ -118,6 +126,13 @@ class GiftVoucherResource extends Resource
$record,
Carbon::parse($data['recipient_delivery_scheduled_at'])
);
app(SuperAdminAuditLogger::class)->record(
'gift_voucher.delivery_scheduled',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
source: static::class
);
})
->visible(fn (GiftVoucher $record): bool => ! empty($record->recipient_email)),
Action::make('mark_redeemed')
@@ -136,6 +151,13 @@ class GiftVoucherResource extends Resource
'manual_marked' => true,
]),
])->save();
app(SuperAdminAuditLogger::class)->record(
'gift_voucher.marked_redeemed',
$record,
SuperAdminAuditLogger::fieldsMetadata(['status', 'redeemed_at', 'metadata']),
source: static::class
);
})
->successNotificationTitle('Als eingelöst markiert'),
]);

View File

@@ -3,11 +3,12 @@
namespace App\Filament\Resources\GiftVoucherResource\Pages;
use App\Filament\Resources\GiftVoucherResource;
use App\Services\Audit\SuperAdminAuditLogger;
use App\Services\GiftVouchers\GiftVoucherService;
use Filament\Actions\Action;
use Filament\Resources\Pages\ListRecords;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Pages\ListRecords;
use Illuminate\Support\Str;
class ListGiftVouchers extends ListRecords
@@ -62,7 +63,20 @@ class ListGiftVouchers extends ListRecords
],
];
$service->issueFromPaddle($payload);
$voucher = $service->issueFromPaddle($payload);
app(SuperAdminAuditLogger::class)->recordModelMutation(
'issued',
$voucher,
SuperAdminAuditLogger::fieldsMetadata([
'amount',
'currency',
'status',
'expires_at',
'coupon_id',
]),
source: static::class
);
})
->modalHeading('Geschenkgutschein ausstellen'),
];

View File

@@ -5,6 +5,7 @@ namespace App\Filament\Resources;
use App\Filament\Clusters\RareAdmin\RareAdminCluster;
use App\Filament\Resources\LegalPageResource\Pages;
use App\Models\LegalPage;
use App\Services\Audit\SuperAdminAuditLogger;
use BackedEnum;
use Filament\Actions;
use Filament\Forms\Components\DatePicker;
@@ -18,6 +19,7 @@ use Filament\Schemas\Components\Tabs\Tab as SchemaTab;
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Collection;
use UnitEnum;
class LegalPageResource extends Resource
@@ -99,10 +101,27 @@ class LegalPageResource extends Resource
])
->filters([])
->actions([
Actions\EditAction::make(),
Actions\EditAction::make()
->after(fn (array $data, LegalPage $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'updated',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
static::class
)),
])
->bulkActions([
Actions\DeleteBulkAction::make(),
Actions\DeleteBulkAction::make()
->after(function (Collection $records): void {
$logger = app(SuperAdminAuditLogger::class);
foreach ($records as $record) {
$logger->recordModelMutation(
'deleted',
$record,
source: static::class
);
}
}),
]);
}

View File

@@ -3,9 +3,9 @@
namespace App\Filament\Resources\LegalPageResource\Pages;
use App\Filament\Resources\LegalPageResource;
use Filament\Resources\Pages\EditRecord;
use App\Filament\Resources\Pages\AuditedEditRecord;
class EditLegalPage extends EditRecord
class EditLegalPage extends AuditedEditRecord
{
protected static string $resource = LegalPageResource::class;
}

View File

@@ -5,6 +5,7 @@ namespace App\Filament\Resources;
use App\Filament\Clusters\RareAdmin\RareAdminCluster;
use App\Filament\Resources\MediaStorageTargetResource\Pages;
use App\Models\MediaStorageTarget;
use App\Services\Audit\SuperAdminAuditLogger;
use BackedEnum;
use Filament\Actions;
use Filament\Forms\Components\KeyValue;
@@ -15,6 +16,7 @@ use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Collection;
use UnitEnum;
class MediaStorageTargetResource extends Resource
@@ -115,10 +117,27 @@ class MediaStorageTargetResource extends Resource
])
->filters([])
->actions([
Actions\EditAction::make(),
Actions\EditAction::make()
->after(fn (array $data, MediaStorageTarget $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'updated',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
static::class
)),
])
->bulkActions([
Actions\DeleteBulkAction::make(),
Actions\DeleteBulkAction::make()
->after(function (Collection $records): void {
$logger = app(SuperAdminAuditLogger::class);
foreach ($records as $record) {
$logger->recordModelMutation(
'deleted',
$record,
source: static::class
);
}
}),
]);
}

View File

@@ -3,10 +3,9 @@
namespace App\Filament\Resources\MediaStorageTargetResource\Pages;
use App\Filament\Resources\MediaStorageTargetResource;
use Filament\Resources\Pages\CreateRecord;
use App\Filament\Resources\Pages\AuditedCreateRecord;
class CreateMediaStorageTarget extends CreateRecord
class CreateMediaStorageTarget extends AuditedCreateRecord
{
protected static string $resource = MediaStorageTargetResource::class;
}

View File

@@ -3,18 +3,23 @@
namespace App\Filament\Resources\MediaStorageTargetResource\Pages;
use App\Filament\Resources\MediaStorageTargetResource;
use App\Filament\Resources\Pages\AuditedEditRecord;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditMediaStorageTarget extends EditRecord
class EditMediaStorageTarget extends AuditedEditRecord
{
protected static string $resource = MediaStorageTargetResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
Actions\DeleteAction::make()
->after(fn ($record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'deleted',
$record,
source: static::class
)),
];
}
}

View File

@@ -6,6 +6,7 @@ use App\Filament\Clusters\WeeklyOps\WeeklyOpsCluster;
use App\Filament\Resources\PackageAddonResource\Pages;
use App\Jobs\SyncPackageAddonToPaddle;
use App\Models\PackageAddon;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
@@ -17,6 +18,7 @@ use Filament\Tables;
use Filament\Tables\Columns\BadgeColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Collection;
class PackageAddonResource extends Resource
{
@@ -130,11 +132,28 @@ class PackageAddonResource extends Resource
->body('Das Add-on wird im Hintergrund mit Paddle abgeglichen.')
->send();
}),
Actions\EditAction::make(),
Actions\EditAction::make()
->after(fn (array $data, PackageAddon $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'updated',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
static::class
)),
])
->bulkActions([
Actions\BulkActionGroup::make([
Actions\DeleteBulkAction::make(),
Actions\DeleteBulkAction::make()
->after(function (Collection $records): void {
$logger = app(SuperAdminAuditLogger::class);
foreach ($records as $record) {
$logger->recordModelMutation(
'deleted',
$record,
source: static::class
);
}
}),
]),
]);
}

View File

@@ -3,9 +3,9 @@
namespace App\Filament\Resources\PackageAddonResource\Pages;
use App\Filament\Resources\PackageAddonResource;
use Filament\Resources\Pages\CreateRecord;
use App\Filament\Resources\Pages\AuditedCreateRecord;
class CreatePackageAddon extends CreateRecord
class CreatePackageAddon extends AuditedCreateRecord
{
protected static string $resource = PackageAddonResource::class;
}

View File

@@ -3,9 +3,9 @@
namespace App\Filament\Resources\PackageAddonResource\Pages;
use App\Filament\Resources\PackageAddonResource;
use Filament\Resources\Pages\EditRecord;
use App\Filament\Resources\Pages\AuditedEditRecord;
class EditPackageAddon extends EditRecord
class EditPackageAddon extends AuditedEditRecord
{
protected static string $resource = PackageAddonResource::class;

View File

@@ -7,6 +7,7 @@ use App\Filament\Resources\PackageResource\Pages;
use App\Jobs\PullPackageFromPaddle;
use App\Jobs\SyncPackageToPaddle;
use App\Models\Package;
use App\Services\Audit\SuperAdminAuditLogger;
use BackedEnum;
use Filament\Actions;
use Filament\Actions\BulkActionGroup;
@@ -37,6 +38,7 @@ use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\TrashedFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\Unique;
@@ -319,20 +321,75 @@ class PackageResource extends Resource
->send();
}),
ViewAction::make(),
EditAction::make(),
EditAction::make()
->after(fn (array $data, Package $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'updated',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
static::class
)),
DeleteAction::make()
->visible(fn (Package $record) => ! $record->trashed()),
->visible(fn (Package $record) => ! $record->trashed())
->after(fn (Package $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'deleted',
$record,
source: static::class
)),
RestoreAction::make()
->visible(fn (Package $record) => $record->trashed()),
->visible(fn (Package $record) => $record->trashed())
->after(fn (Package $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'restored',
$record,
source: static::class
)),
ForceDeleteAction::make()
->visible(fn (Package $record) => $record->trashed())
->requiresConfirmation(),
->requiresConfirmation()
->after(fn (Package $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'force_deleted',
$record,
source: static::class
)),
])
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
RestoreBulkAction::make(),
ForceDeleteBulkAction::make()->requiresConfirmation(),
DeleteBulkAction::make()
->after(function (Collection $records): void {
$logger = app(SuperAdminAuditLogger::class);
foreach ($records as $record) {
$logger->recordModelMutation(
'deleted',
$record,
source: static::class
);
}
}),
RestoreBulkAction::make()
->after(function (Collection $records): void {
$logger = app(SuperAdminAuditLogger::class);
foreach ($records as $record) {
$logger->recordModelMutation(
'restored',
$record,
source: static::class
);
}
}),
ForceDeleteBulkAction::make()
->requiresConfirmation()
->after(function (Collection $records): void {
$logger = app(SuperAdminAuditLogger::class);
foreach ($records as $record) {
$logger->recordModelMutation(
'force_deleted',
$record,
source: static::class
);
}
}),
]),
]);
}

View File

@@ -3,9 +3,9 @@
namespace App\Filament\Resources\PackageResource\Pages;
use App\Filament\Resources\PackageResource;
use Filament\Resources\Pages\CreateRecord;
use App\Filament\Resources\Pages\AuditedCreateRecord;
class CreatePackage extends CreateRecord
class CreatePackage extends AuditedCreateRecord
{
protected static string $resource = PackageResource::class;
}

View File

@@ -3,10 +3,11 @@
namespace App\Filament\Resources\PackageResource\Pages;
use App\Filament\Resources\PackageResource;
use App\Filament\Resources\Pages\AuditedEditRecord;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditPackage extends EditRecord
class EditPackage extends AuditedEditRecord
{
protected static string $resource = PackageResource::class;
@@ -14,7 +15,12 @@ class EditPackage extends EditRecord
{
return [
Actions\ViewAction::make(),
Actions\DeleteAction::make(),
Actions\DeleteAction::make()
->after(fn ($record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'deleted',
$record,
source: static::class
)),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\Pages;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Resources\Pages\CreateRecord;
class AuditedCreateRecord extends CreateRecord
{
protected function afterCreate(): void
{
app(SuperAdminAuditLogger::class)->recordModelMutation(
'created',
$this->record,
SuperAdminAuditLogger::fieldsMetadata($this->form->getState()),
static::class
);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Filament\Resources\Pages;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Resources\Pages\EditRecord;
class AuditedEditRecord extends EditRecord
{
protected function afterSave(): void
{
$changed = array_keys($this->record->getChanges());
if ($changed === []) {
return;
}
app(SuperAdminAuditLogger::class)->recordModelMutation(
'updated',
$this->record,
SuperAdminAuditLogger::fieldsMetadata($changed ?: $this->form->getState()),
static::class
);
}
}

View File

@@ -6,6 +6,7 @@ use App\Filament\Clusters\DailyOps\DailyOpsCluster;
use App\Filament\Resources\PhotoResource\Pages;
use App\Models\Event;
use App\Models\Photo;
use App\Services\Audit\SuperAdminAuditLogger;
use BackedEnum;
use Filament\Actions;
use Filament\Forms\Components\FileUpload;
@@ -16,6 +17,7 @@ use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Collection;
use UnitEnum;
class PhotoResource extends Resource
@@ -78,29 +80,95 @@ class PhotoResource extends Resource
])
->filters([])
->actions([
Actions\EditAction::make(),
Actions\EditAction::make()
->after(fn (array $data, Photo $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'updated',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
static::class
)),
Actions\Action::make('feature')
->label(__('admin.photos.actions.feature'))
->visible(fn (Photo $record) => ! $record->is_featured)
->action(fn (Photo $record) => $record->update(['is_featured' => true]))
->action(function (Photo $record): void {
$record->update(['is_featured' => true]);
app(SuperAdminAuditLogger::class)->record(
'photo.featured',
$record,
SuperAdminAuditLogger::fieldsMetadata(['is_featured']),
source: static::class
);
})
->icon('heroicon-o-star'),
Actions\Action::make('unfeature')
->label(__('admin.photos.actions.unfeature'))
->visible(fn (Photo $record) => $record->is_featured)
->action(fn (Photo $record) => $record->update(['is_featured' => false]))
->action(function (Photo $record): void {
$record->update(['is_featured' => false]);
app(SuperAdminAuditLogger::class)->record(
'photo.unfeatured',
$record,
SuperAdminAuditLogger::fieldsMetadata(['is_featured']),
source: static::class
);
})
->icon('heroicon-o-star'),
Actions\DeleteAction::make(),
Actions\DeleteAction::make()
->after(fn (Photo $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'deleted',
$record,
source: static::class
)),
])
->bulkActions([
Actions\BulkAction::make('feature')
->label(__('admin.photos.actions.feature_selected'))
->icon('heroicon-o-star')
->action(fn ($records) => $records->each->update(['is_featured' => true])),
->action(function ($records): void {
$records->each->update(['is_featured' => true]);
$logger = app(SuperAdminAuditLogger::class);
foreach ($records as $record) {
$logger->record(
'photo.featured',
$record,
SuperAdminAuditLogger::fieldsMetadata(['is_featured']),
source: static::class
);
}
}),
Actions\BulkAction::make('unfeature')
->label(__('admin.photos.actions.unfeature_selected'))
->icon('heroicon-o-star')
->action(fn ($records) => $records->each->update(['is_featured' => false])),
Actions\DeleteBulkAction::make(),
->action(function ($records): void {
$records->each->update(['is_featured' => false]);
$logger = app(SuperAdminAuditLogger::class);
foreach ($records as $record) {
$logger->record(
'photo.unfeatured',
$record,
SuperAdminAuditLogger::fieldsMetadata(['is_featured']),
source: static::class
);
}
}),
Actions\DeleteBulkAction::make()
->after(function (Collection $records): void {
$logger = app(SuperAdminAuditLogger::class);
foreach ($records as $record) {
$logger->recordModelMutation(
'deleted',
$record,
source: static::class
);
}
}),
]);
}

View File

@@ -2,10 +2,10 @@
namespace App\Filament\Resources\PhotoResource\Pages;
use App\Filament\Resources\Pages\AuditedEditRecord;
use App\Filament\Resources\PhotoResource;
use Filament\Resources\Pages\EditRecord;
class EditPhoto extends EditRecord
class EditPhoto extends AuditedEditRecord
{
protected static string $resource = PhotoResource::class;
}

View File

@@ -2,10 +2,10 @@
namespace App\Filament\Resources\PhotoboothSettings\Pages;
use App\Filament\Resources\Pages\AuditedEditRecord;
use App\Filament\Resources\PhotoboothSettings\PhotoboothSettingResource;
use Filament\Resources\Pages\EditRecord;
class EditPhotoboothSetting extends EditRecord
class EditPhotoboothSetting extends AuditedEditRecord
{
protected static string $resource = PhotoboothSettingResource::class;

View File

@@ -2,6 +2,7 @@
namespace App\Filament\Resources\PhotoboothSettings\Tables;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions\EditAction;
use Filament\Tables;
use Filament\Tables\Table;
@@ -29,7 +30,13 @@ class PhotoboothSettingsTable
->label(__('Aktualisiert')),
])
->recordActions([
EditAction::make(),
EditAction::make()
->after(fn (array $data, $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'updated',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
static::class
)),
])
->headerActions([])
->bulkActions([]);

View File

@@ -7,6 +7,7 @@ use App\Filament\Resources\PurchaseResource\Pages;
use App\Models\PackagePurchase;
use App\Notifications\Customer\RefundReceipt;
use App\Notifications\Ops\RefundProcessed;
use App\Services\Audit\SuperAdminAuditLogger;
use App\Services\Paddle\PaddleTransactionService;
use BackedEnum;
use Filament\Actions\Action;
@@ -27,6 +28,7 @@ use Filament\Tables\Filters\Filter;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
@@ -178,7 +180,13 @@ class PurchaseResource extends Resource
])
->actions([
ViewAction::make(),
EditAction::make(),
EditAction::make()
->after(fn (array $data, PackagePurchase $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'updated',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
static::class
)),
Action::make('refund')
->label('Refund')
->color('danger')
@@ -234,11 +242,29 @@ class PurchaseResource extends Resource
if ($opsEmail) {
Notification::route('mail', $opsEmail)->notify(new RefundProcessed($record, $refundSuccess, $reason, $errorMessage));
}
app(SuperAdminAuditLogger::class)->record(
'purchase.refunded',
$record,
SuperAdminAuditLogger::fieldsMetadata(['refunded', 'metadata']),
source: static::class
);
}),
])
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
DeleteBulkAction::make()
->after(function (Collection $records): void {
$logger = app(SuperAdminAuditLogger::class);
foreach ($records as $record) {
$logger->recordModelMutation(
'deleted',
$record,
source: static::class
);
}
}),
]),
])
->emptyStateHeading('No Purchases Found')

View File

@@ -2,10 +2,10 @@
namespace App\Filament\Resources\PurchaseResource\Pages;
use App\Filament\Resources\Pages\AuditedCreateRecord;
use App\Filament\Resources\PurchaseResource;
use Filament\Resources\Pages\CreateRecord;
class CreatePurchase extends CreateRecord
class CreatePurchase extends AuditedCreateRecord
{
protected static string $resource = PurchaseResource::class;
}

View File

@@ -2,11 +2,12 @@
namespace App\Filament\Resources\PurchaseResource\Pages;
use App\Filament\Resources\Pages\AuditedEditRecord;
use App\Filament\Resources\PurchaseResource;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditPurchase extends EditRecord
class EditPurchase extends AuditedEditRecord
{
protected static string $resource = PurchaseResource::class;
@@ -14,7 +15,12 @@ class EditPurchase extends EditRecord
{
return [
Actions\ViewAction::make(),
Actions\DeleteAction::make(),
Actions\DeleteAction::make()
->after(fn ($record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'deleted',
$record,
source: static::class
)),
];
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Filament\Resources\PurchaseResource\Pages;
use App\Filament\Resources\PurchaseResource;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions;
use Filament\Resources\Pages\ViewRecord;
@@ -13,8 +14,19 @@ class ViewPurchase extends ViewRecord
protected function getHeaderActions(): array
{
return [
Actions\EditAction::make(),
Actions\DeleteAction::make(),
Actions\EditAction::make()
->after(fn (array $data, $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'updated',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
static::class
)),
Actions\DeleteAction::make()
->after(fn ($record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'deleted',
$record,
source: static::class
)),
Actions\Action::make('refund')
->label('Refund')
->color('danger')
@@ -24,6 +36,13 @@ class ViewPurchase extends ViewRecord
->action(function ($record) {
$record->update(['refunded' => true]);
// TODO: Call Paddle API for actual refund
app(SuperAdminAuditLogger::class)->record(
'purchase.refunded',
$record,
SuperAdminAuditLogger::fieldsMetadata(['refunded']),
source: static::class
);
}),
];
}

View File

@@ -5,6 +5,7 @@ namespace App\Filament\Resources;
use App\Filament\Clusters\WeeklyOps\WeeklyOpsCluster;
use App\Filament\Resources\TaskResource\Pages;
use App\Models\Task;
use App\Services\Audit\SuperAdminAuditLogger;
use BackedEnum;
use Filament\Actions;
use Filament\Forms\Components\MarkdownEditor;
@@ -17,6 +18,7 @@ use Filament\Schemas\Components\Tabs\Tab as SchemaTab;
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Collection;
use UnitEnum;
class TaskResource extends Resource
@@ -163,11 +165,33 @@ class TaskResource extends Resource
])
->filters([])
->actions([
Actions\EditAction::make(),
Actions\DeleteAction::make(),
Actions\EditAction::make()
->after(fn (array $data, Task $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'updated',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
static::class
)),
Actions\DeleteAction::make()
->after(fn (Task $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'deleted',
$record,
source: static::class
)),
])
->bulkActions([
Actions\DeleteBulkAction::make(),
Actions\DeleteBulkAction::make()
->after(function (Collection $records): void {
$logger = app(SuperAdminAuditLogger::class);
foreach ($records as $record) {
$logger->recordModelMutation(
'deleted',
$record,
source: static::class
);
}
}),
]);
}

View File

@@ -4,6 +4,7 @@ namespace App\Filament\Resources\TaskResource\Pages;
use App\Filament\Resources\TaskResource;
use App\Models\Task;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
@@ -14,7 +15,9 @@ use Illuminate\Support\Facades\Storage;
class ImportTasks extends Page
{
protected static string $resource = TaskResource::class;
protected string $view = 'filament.resources.task-resource.pages.import-tasks';
protected ?string $heading = null;
public ?string $file = null;
@@ -35,8 +38,9 @@ class ImportTasks extends Page
$this->validate();
$path = $this->form->getState()['file'] ?? null;
if (!$path || !Storage::disk('public')->exists($path)) {
if (! $path || ! Storage::disk('public')->exists($path)) {
Notification::make()->danger()->title(__('admin.notifications.file_not_found'))->send();
return;
}
@@ -58,14 +62,14 @@ class ImportTasks extends Page
private function importTasksCsv(string $file): array
{
$handle = fopen($file, 'r');
if (!$handle) {
if (! $handle) {
return [0, 0];
}
$ok = 0;
$fail = 0;
$headers = fgetcsv($handle, 0, ',');
if (!$headers) {
if (! $headers) {
return [0, 0];
}
@@ -87,7 +91,7 @@ class ImportTasks extends Page
$emotionId = DB::table('emotions')->where('name->en', $emotionNameEn)->value('id');
}
if (!$emotionId) {
if (! $emotionId) {
throw new \Exception('Emotion not found.');
}
@@ -97,7 +101,7 @@ class ImportTasks extends Page
$eventTypeId = DB::table('event_types')->where('slug', $eventTypeSlug)->value('id');
}
Task::create([
$task = Task::create([
'emotion_id' => $emotionId,
'event_type_id' => $eventTypeId,
'title' => [
@@ -113,10 +117,17 @@ class ImportTasks extends Page
'de' => $row[$map['example_text_de']] ?? null,
'en' => $row[$map['example_text_en']] ?? null,
],
'sort_order' => (int)($row[$map['sort_order']] ?? 0),
'is_active' => (int)($row[$map['is_active']] ?? 1) ? 1 : 0,
'sort_order' => (int) ($row[$map['sort_order']] ?? 0),
'is_active' => (int) ($row[$map['is_active']] ?? 1) ? 1 : 0,
]);
app(SuperAdminAuditLogger::class)->recordModelMutation(
'created',
$task,
SuperAdminAuditLogger::fieldsMetadata($task->getChanges()),
source: static::class
);
$ok++;
});
} catch (\Throwable $e) {
@@ -125,6 +136,7 @@ class ImportTasks extends Page
}
fclose($handle);
return [$ok, $fail];
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Filament\Resources\TaskResource\Pages;
use App\Filament\Resources\TaskResource;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions;
use Filament\Resources\Pages\ManageRecords;
@@ -13,7 +14,13 @@ class ManageTasks extends ManageRecords
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
Actions\CreateAction::make()
->after(fn (array $data, $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'created',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
static::class
)),
Actions\Action::make('import')
->label(__('admin.common.import_csv'))
->icon('heroicon-o-arrow-up-tray')

View File

@@ -3,6 +3,7 @@
namespace App\Filament\Resources\TenantFeedbackResource\Pages;
use App\Filament\Resources\TenantFeedbackResource;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\ViewRecord;
@@ -13,7 +14,12 @@ class ViewTenantFeedback extends ViewRecord
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
DeleteAction::make()
->after(fn ($record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'deleted',
$record,
source: static::class
)),
];
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Filament\Resources\TenantFeedbackResource\Tables;
use App\Models\TenantFeedback;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions\Action;
use Filament\Actions\BulkAction;
use Filament\Actions\BulkActionGroup;
@@ -182,11 +183,23 @@ class TenantFeedbackTable
'moderated_at' => now(),
'moderated_by' => Filament::auth()->id(),
]);
app(SuperAdminAuditLogger::class)->record(
'tenant_feedback.'.$status,
$record,
SuperAdminAuditLogger::fieldsMetadata([
'status',
'moderation_notes',
'moderated_at',
'moderated_by',
]),
source: self::class
);
}
private static function applyModerationToRecords(Collection $records, string $status, ?string $notes): int
{
return TenantFeedback::query()
$updated = TenantFeedback::query()
->whereIn('id', $records->pluck('id'))
->update([
'status' => $status,
@@ -194,6 +207,24 @@ class TenantFeedbackTable
'moderated_at' => now(),
'moderated_by' => Filament::auth()->id(),
]);
$logger = app(SuperAdminAuditLogger::class);
foreach ($records as $record) {
$logger->record(
'tenant_feedback.'.$status,
$record,
SuperAdminAuditLogger::fieldsMetadata([
'status',
'moderation_notes',
'moderated_at',
'moderated_by',
]),
source: self::class
);
}
return $updated;
}
private static function statusLabels(): array

View File

@@ -5,6 +5,7 @@ namespace App\Filament\Resources;
use App\Filament\Clusters\WeeklyOps\WeeklyOpsCluster;
use App\Filament\Resources\TenantPackageResource\Pages;
use App\Models\TenantPackage;
use App\Services\Audit\SuperAdminAuditLogger;
use BackedEnum;
use Filament\Actions\ActionGroup;
use Filament\Actions\BulkActionGroup;
@@ -20,6 +21,7 @@ use Filament\Schemas\Schema;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Collection;
class TenantPackageResource extends Resource
{
@@ -68,13 +70,35 @@ class TenantPackageResource extends Resource
->actions([
ActionGroup::make([
ViewAction::make(),
EditAction::make(),
DeleteAction::make(),
EditAction::make()
->after(fn (array $data, TenantPackage $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'updated',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
static::class
)),
DeleteAction::make()
->after(fn (TenantPackage $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'deleted',
$record,
source: static::class
)),
]),
])
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
DeleteBulkAction::make()
->after(function (Collection $records): void {
$logger = app(SuperAdminAuditLogger::class);
foreach ($records as $record) {
$logger->recordModelMutation(
'deleted',
$record,
source: static::class
);
}
}),
]),
]);
}

View File

@@ -2,10 +2,10 @@
namespace App\Filament\Resources\TenantPackageResource\Pages;
use App\Filament\Resources\Pages\AuditedCreateRecord;
use App\Filament\Resources\TenantPackageResource;
use Filament\Resources\Pages\CreateRecord;
class CreateTenantPackage extends CreateRecord
class CreateTenantPackage extends AuditedCreateRecord
{
protected static string $resource = TenantPackageResource::class;
}

View File

@@ -2,11 +2,12 @@
namespace App\Filament\Resources\TenantPackageResource\Pages;
use App\Filament\Resources\Pages\AuditedEditRecord;
use App\Filament\Resources\TenantPackageResource;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditTenantPackage extends EditRecord
class EditTenantPackage extends AuditedEditRecord
{
protected static string $resource = TenantPackageResource::class;
@@ -14,7 +15,12 @@ class EditTenantPackage extends EditRecord
{
return [
Actions\ViewAction::make(),
Actions\DeleteAction::make(),
Actions\DeleteAction::make()
->after(fn ($record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'deleted',
$record,
source: static::class
)),
];
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Filament\Resources\TenantPackageResource\Pages;
use App\Filament\Resources\TenantPackageResource;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions;
use Filament\Resources\Pages\ViewRecord;
@@ -13,8 +14,19 @@ class ViewTenantPackage extends ViewRecord
protected function getHeaderActions(): array
{
return [
Actions\EditAction::make(),
Actions\DeleteAction::make(),
Actions\EditAction::make()
->after(fn (array $data, $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'updated',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
static::class
)),
Actions\DeleteAction::make()
->after(fn ($record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'deleted',
$record,
source: static::class
)),
];
}
}

View File

@@ -10,6 +10,7 @@ use App\Filament\Resources\TenantResource\Schemas\TenantInfolist;
use App\Jobs\AnonymizeAccount;
use App\Models\Tenant;
use App\Notifications\InactiveTenantDeletionWarning;
use App\Services\Audit\SuperAdminAuditLogger;
use App\Services\Tenant\TenantLifecycleLogger;
use BackedEnum;
use Carbon\Carbon;
@@ -27,6 +28,7 @@ use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Notification as NotificationFacade;
use Illuminate\Support\Facades\Route;
use UnitEnum;
@@ -180,7 +182,13 @@ class TenantResource extends Resource
])
->filters([])
->actions([
Actions\EditAction::make(),
Actions\EditAction::make()
->after(fn (array $data, Tenant $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'updated',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
static::class
)),
Actions\Action::make('add_package')
->label('Package hinzufügen')
->icon('heroicon-o-plus')
@@ -213,6 +221,13 @@ class TenantResource extends Resource
'price' => 0,
'metadata' => ['reason' => $data['reason'] ?? 'manual assignment'],
]);
app(SuperAdminAuditLogger::class)->record(
'tenant.package_added',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
source: static::class
);
}),
Actions\Action::make('export')
->label('Daten exportieren')
@@ -225,7 +240,18 @@ class TenantResource extends Resource
->icon('heroicon-o-shield-exclamation'),
])
->bulkActions([
Actions\DeleteBulkAction::make(),
Actions\DeleteBulkAction::make()
->after(function (Collection $records): void {
$logger = app(SuperAdminAuditLogger::class);
foreach ($records as $record) {
$logger->recordModelMutation(
'deleted',
$record,
source: static::class
);
}
}),
]);
}
@@ -269,6 +295,13 @@ class TenantResource extends Resource
actor: Filament::auth()->user()
);
app(SuperAdminAuditLogger::class)->record(
'tenant.activated',
$record,
SuperAdminAuditLogger::fieldsMetadata(['is_active']),
source: static::class
);
return $updated;
}),
Actions\Action::make('deactivate')
@@ -287,6 +320,13 @@ class TenantResource extends Resource
actor: Filament::auth()->user()
);
app(SuperAdminAuditLogger::class)->record(
'tenant.deactivated',
$record,
SuperAdminAuditLogger::fieldsMetadata(['is_active']),
source: static::class
);
return $updated;
}),
Actions\Action::make('suspend')
@@ -305,6 +345,13 @@ class TenantResource extends Resource
actor: Filament::auth()->user()
);
app(SuperAdminAuditLogger::class)->record(
'tenant.suspended',
$record,
SuperAdminAuditLogger::fieldsMetadata(['is_suspended']),
source: static::class
);
return $updated;
}),
Actions\Action::make('unsuspend')
@@ -322,6 +369,13 @@ class TenantResource extends Resource
actor: Filament::auth()->user()
);
app(SuperAdminAuditLogger::class)->record(
'tenant.unsuspended',
$record,
SuperAdminAuditLogger::fieldsMetadata(['is_suspended']),
source: static::class
);
return $updated;
}),
Actions\Action::make('schedule_deletion')
@@ -374,6 +428,13 @@ class TenantResource extends Resource
],
Filament::auth()->user()
);
app(SuperAdminAuditLogger::class)->record(
'tenant.deletion_scheduled',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
source: static::class
);
})
->successNotificationTitle(__('admin.tenants.actions.schedule_deletion_success')),
Actions\Action::make('cancel_deletion')
@@ -398,6 +459,13 @@ class TenantResource extends Resource
],
Filament::auth()->user()
);
app(SuperAdminAuditLogger::class)->record(
'tenant.deletion_cancelled',
$record,
SuperAdminAuditLogger::fieldsMetadata(['pending_deletion_at', 'deletion_warning_sent_at']),
source: static::class
);
})
->successNotificationTitle(__('admin.tenants.actions.cancel_deletion_success')),
Actions\Action::make('anonymize_now')
@@ -415,6 +483,13 @@ class TenantResource extends Resource
'anonymize_requested',
actor: Filament::auth()->user()
);
app(SuperAdminAuditLogger::class)->record(
'tenant.anonymize_requested',
$record,
SuperAdminAuditLogger::fieldsMetadata([]),
source: static::class
);
})
->successNotificationTitle(__('admin.tenants.actions.anonymize_success')),
];
@@ -472,6 +547,13 @@ class TenantResource extends Resource
],
Filament::auth()->user()
);
app(SuperAdminAuditLogger::class)->record(
'tenant.limits_updated',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
source: static::class
);
}),
Actions\Action::make('update_subscription_expires_at')
->label(__('admin.tenants.actions.update_subscription_expires_at'))
@@ -508,6 +590,13 @@ class TenantResource extends Resource
],
Filament::auth()->user()
);
app(SuperAdminAuditLogger::class)->record(
'tenant.subscription_expires_at_updated',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
source: static::class
);
}),
Actions\Action::make('set_grace_period')
->label(__('admin.tenants.actions.set_grace_period'))
@@ -537,6 +626,13 @@ class TenantResource extends Resource
],
Filament::auth()->user()
);
app(SuperAdminAuditLogger::class)->record(
'tenant.grace_period_set',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
source: static::class
);
}),
Actions\Action::make('clear_grace_period')
->label(__('admin.tenants.actions.clear_grace_period'))
@@ -560,6 +656,13 @@ class TenantResource extends Resource
],
Filament::auth()->user()
);
app(SuperAdminAuditLogger::class)->record(
'tenant.grace_period_cleared',
$record,
SuperAdminAuditLogger::fieldsMetadata(['grace_period_ends_at']),
source: static::class
);
}),
];
}

View File

@@ -2,10 +2,10 @@
namespace App\Filament\Resources\TenantResource\Pages;
use App\Filament\Resources\Pages\AuditedEditRecord;
use App\Filament\Resources\TenantResource;
use Filament\Resources\Pages\EditRecord;
class EditTenant extends EditRecord
class EditTenant extends AuditedEditRecord
{
protected static string $resource = TenantResource::class;

View File

@@ -3,6 +3,7 @@
namespace App\Filament\Resources\TenantResource\Pages;
use App\Filament\Resources\TenantResource;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions;
use Filament\Resources\Pages\ViewRecord;
@@ -23,7 +24,13 @@ class ViewTenant extends ViewRecord
protected function getHeaderActions(): array
{
return [
Actions\EditAction::make(),
Actions\EditAction::make()
->after(fn (array $data, $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'updated',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
static::class
)),
...TenantResource::lifecycleActions(),
];
}

View File

@@ -4,6 +4,7 @@ namespace App\Filament\Resources\TenantResource\Pages;
use App\Filament\Resources\TenantResource;
use App\Filament\Resources\TenantResource\Schemas\TenantLifecycleInfolist;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions;
use Filament\Resources\Pages\ViewRecord;
use Filament\Schemas\Schema;
@@ -25,7 +26,13 @@ class ViewTenantLifecycle extends ViewRecord
protected function getHeaderActions(): array
{
return [
Actions\EditAction::make(),
Actions\EditAction::make()
->after(fn (array $data, $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'updated',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
static::class
)),
Actions\ActionGroup::make(TenantResource::lifecycleActions())
->label(__('admin.tenants.actions.lifecycle'))
->icon('heroicon-o-shield-exclamation'),

View File

@@ -2,6 +2,7 @@
namespace App\Filament\Resources\TenantResource\RelationManagers;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\ViewAction;
@@ -15,6 +16,7 @@ use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Collection;
class PackagePurchasesRelationManager extends RelationManager
{
@@ -130,7 +132,18 @@ class PackagePurchasesRelationManager extends RelationManager
])
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
DeleteBulkAction::make()
->after(function (Collection $records): void {
$logger = app(SuperAdminAuditLogger::class);
foreach ($records as $record) {
$logger->recordModelMutation(
'deleted',
$record,
source: static::class
);
}
}),
]),
]);
}

View File

@@ -2,22 +2,19 @@
namespace App\Filament\Resources\TenantResource\RelationManagers;
use Filament\Forms;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Schemas\Schema;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\ViewAction;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Eloquent\Collection;
class PurchasesRelationManager extends RelationManager
{
@@ -77,7 +74,7 @@ class PurchasesRelationManager extends RelationManager
->columns([
TextColumn::make('package_id')
->badge()
->color(fn (string $state): string => match($state) {
->color(fn (string $state): string => match ($state) {
'starter_pack' => 'info',
'pro_pack' => 'success',
'lifetime_unlimited' => 'danger',
@@ -92,7 +89,7 @@ class PurchasesRelationManager extends RelationManager
->money('EUR'),
TextColumn::make('platform')
->badge()
->color(fn (string $state): string => match($state) {
->color(fn (string $state): string => match ($state) {
'ios' => 'info',
'android' => 'success',
'web' => 'warning',
@@ -123,10 +120,19 @@ class PurchasesRelationManager extends RelationManager
])
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
DeleteBulkAction::make()
->after(function (Collection $records): void {
$logger = app(SuperAdminAuditLogger::class);
foreach ($records as $record) {
$logger->recordModelMutation(
'deleted',
$record,
source: static::class
);
}
}),
]),
]);
}
}

View File

@@ -2,6 +2,8 @@
namespace App\Filament\Resources\TenantResource\RelationManagers;
use App\Models\TenantPackage;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions\Action;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
@@ -17,6 +19,7 @@ use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Collection;
class TenantPackagesRelationManager extends RelationManager
{
@@ -92,22 +95,57 @@ class TenantPackagesRelationManager extends RelationManager
])
->headerActions([])
->actions([
EditAction::make(),
EditAction::make()
->after(fn (array $data, TenantPackage $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'updated',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
static::class
)),
Action::make('activate')
->label('Aktivieren')
->icon('heroicon-o-check-circle')
->color('success')
->action(fn ($record) => $record->update(['active' => true])),
->action(function (TenantPackage $record): void {
$record->update(['active' => true]);
app(SuperAdminAuditLogger::class)->record(
'tenant_package.activated',
$record,
SuperAdminAuditLogger::fieldsMetadata(['active']),
source: static::class
);
}),
Action::make('deactivate')
->label('Deaktivieren')
->icon('heroicon-o-x-circle')
->color('danger')
->requiresConfirmation()
->action(fn ($record) => $record->update(['active' => false])),
->action(function (TenantPackage $record): void {
$record->update(['active' => false]);
app(SuperAdminAuditLogger::class)->record(
'tenant_package.deactivated',
$record,
SuperAdminAuditLogger::fieldsMetadata(['active']),
source: static::class
);
}),
])
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
DeleteBulkAction::make()
->after(function (Collection $records): void {
$logger = app(SuperAdminAuditLogger::class);
foreach ($records as $record) {
$logger->recordModelMutation(
'deleted',
$record,
source: static::class
);
}
}),
]),
]);
}

View File

@@ -5,6 +5,7 @@ namespace App\Filament\Resources;
use App\Filament\Clusters\WeeklyOps\WeeklyOpsCluster;
use App\Filament\Resources\UserResource\Pages;
use App\Models\User;
use App\Services\Audit\SuperAdminAuditLogger;
use BackedEnum;
use Filament\Actions\ActionGroup;
use Filament\Actions\BulkActionGroup;
@@ -19,6 +20,7 @@ use Filament\Schemas\Schema;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Collection;
class UserResource extends Resource
{
@@ -98,12 +100,29 @@ class UserResource extends Resource
->actions([
ActionGroup::make([
ViewAction::make(),
EditAction::make(),
EditAction::make()
->after(fn (array $data, User $record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'updated',
$record,
SuperAdminAuditLogger::fieldsMetadata($data),
static::class
)),
]),
])
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
DeleteBulkAction::make()
->after(function (Collection $records): void {
$logger = app(SuperAdminAuditLogger::class);
foreach ($records as $record) {
$logger->recordModelMutation(
'deleted',
$record,
source: static::class
);
}
}),
]),
]);
}

View File

@@ -2,11 +2,11 @@
namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\Pages\AuditedCreateRecord;
use App\Filament\Resources\UserResource;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Support\Facades\Hash;
class CreateUser extends CreateRecord
class CreateUser extends AuditedCreateRecord
{
protected static string $resource = UserResource::class;

View File

@@ -2,19 +2,25 @@
namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\Pages\AuditedEditRecord;
use App\Filament\Resources\UserResource;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
use Illuminate\Support\Facades\Hash;
class EditUser extends EditRecord
class EditUser extends AuditedEditRecord
{
protected static string $resource = UserResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
Actions\DeleteAction::make()
->after(fn ($record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
'deleted',
$record,
source: static::class
)),
];
}

View File

@@ -4,6 +4,7 @@ namespace App\Filament\SuperAdmin\Pages;
use App\Filament\Clusters\RareAdmin\RareAdminCluster;
use App\Models\GuestPolicySetting;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Forms;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
@@ -163,6 +164,26 @@ class GuestPolicySettingsPage extends Page
$settings->guest_notification_ttl_hours = $this->guest_notification_ttl_hours;
$settings->save();
app(SuperAdminAuditLogger::class)->record(
'guest_policy.updated',
$settings,
SuperAdminAuditLogger::fieldsMetadata([
'guest_downloads_enabled',
'guest_sharing_enabled',
'guest_upload_visibility',
'per_device_upload_limit',
'join_token_failure_limit',
'join_token_failure_decay_minutes',
'join_token_access_limit',
'join_token_access_decay_minutes',
'join_token_download_limit',
'join_token_download_decay_minutes',
'share_link_ttl_hours',
'guest_notification_ttl_hours',
]),
source: static::class
);
Notification::make()
->title(__('admin.guest_policy.notifications.saved'))
->success()

View File

@@ -4,6 +4,7 @@ namespace App\Filament\SuperAdmin\Pages;
use App\Filament\Clusters\RareAdmin\RareAdminCluster;
use App\Models\WatermarkSetting;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Forms;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
@@ -128,6 +129,21 @@ class WatermarkSettingsPage extends Page
$settings->offset_y = $this->offset_y;
$settings->save();
app(SuperAdminAuditLogger::class)->record(
'watermark_settings.updated',
$settings,
SuperAdminAuditLogger::fieldsMetadata([
'asset',
'position',
'opacity',
'scale',
'padding',
'offset_x',
'offset_y',
]),
source: static::class
);
Notification::make()
->title('Wasserzeichen aktualisiert')
->success()

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Prunable;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class SuperAdminActionLog extends Model
{
/** @use HasFactory<\Database\Factories\SuperAdminActionLogFactory> */
use HasFactory;
use Prunable;
protected $guarded = [];
protected $casts = [
'metadata' => 'array',
'occurred_at' => 'datetime',
];
public function actor(): BelongsTo
{
return $this->belongsTo(User::class, 'actor_id');
}
public function subject(): MorphTo
{
return $this->morphTo();
}
public function prunable(): Builder
{
return static::query()->where('occurred_at', '<=', now()->subMonths(6));
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace App\Services\Audit;
use App\Models\SuperAdminActionLog;
use App\Models\User;
use Filament\Facades\Filament;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class SuperAdminAuditLogger
{
/**
* @param array<string, array<int, string>> $metadata
*/
public function record(
string $action,
?Model $subject = null,
array $metadata = [],
?User $actor = null,
?string $source = null
): ?SuperAdminActionLog {
$actor = $actor ?? Filament::auth()->user();
if (! $this->shouldLog($actor)) {
return null;
}
$metadata = $this->sanitizeMetadata($metadata);
return SuperAdminActionLog::create([
'actor_id' => $actor?->getKey(),
'action' => $action,
'subject_type' => $subject?->getMorphClass(),
'subject_id' => $subject?->getKey(),
'source' => $source,
'metadata' => $metadata ?: null,
'occurred_at' => now(),
]);
}
/**
* @param array<string, array<int, string>> $metadata
*/
public function recordModelMutation(
string $operation,
Model $record,
array $metadata = [],
?string $source = null,
?User $actor = null
): ?SuperAdminActionLog {
$action = $this->formatAction($record, $operation);
return $this->record(
$action,
$record,
$metadata,
$actor,
$source
);
}
/**
* @param array<int, string>|array<string, mixed> $data
* @return array<string, array<int, string>>
*/
public static function fieldsMetadata(array $data): array
{
$fields = array_is_list($data) ? $data : array_keys($data);
return ['fields' => array_values(array_unique($fields))];
}
private function shouldLog(?User $actor): bool
{
if (! $actor || $actor->role !== 'super_admin') {
return false;
}
$panel = Filament::getCurrentPanel();
if ($panel) {
return $panel->getId() === 'superadmin';
}
if (app()->runningInConsole()) {
return false;
}
return request()->is('super-admin*');
}
/**
* @param array<string, array<int, string>> $metadata
* @return array<string, array<int, string>>
*/
private function sanitizeMetadata(array $metadata): array
{
$sanitized = [];
foreach ($metadata as $key => $value) {
if (! is_array($value)) {
continue;
}
$values = array_values(array_filter($value, fn ($item) => is_string($item) && $item !== ''));
if ($values === []) {
continue;
}
$sanitized[$key] = $values;
}
return $sanitized;
}
private function formatAction(Model $record, string $operation): string
{
return Str::kebab(class_basename($record)).'.'.$operation;
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Database\Factories;
use App\Models\SuperAdminActionLog;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\SuperAdminActionLog>
*/
class SuperAdminActionLogFactory extends Factory
{
protected $model = SuperAdminActionLog::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'actor_id' => User::factory(),
'action' => $this->faker->randomElement([
'tenant.updated',
'photo.approved',
'user.created',
]),
'subject_type' => User::class,
'subject_id' => User::factory(),
'source' => $this->faker->randomElement([
'App\\Filament\\Resources\\TenantResource',
'App\\Filament\\Clusters\\DailyOps\\Resources\\Photos\\PhotoResource',
]),
'metadata' => [
'fields' => ['status', 'moderated_at'],
],
'occurred_at' => now(),
];
}
}

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('super_admin_action_logs', function (Blueprint $table) {
$table->id();
$table->foreignId('actor_id')->nullable()->constrained('users')->nullOnDelete();
$table->string('action', 120);
$table->nullableMorphs('subject');
$table->string('source', 200)->nullable();
$table->json('metadata')->nullable();
$table->timestamp('occurred_at')->useCurrent();
$table->timestamps();
$table->index(['action', 'occurred_at']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('super_admin_action_logs');
}
};

View File

@@ -0,0 +1,19 @@
<?php
namespace Database\Seeders;
use App\Models\SuperAdminActionLog;
use Illuminate\Database\Seeder;
class SuperAdminActionLogSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
SuperAdminActionLog::factory()
->count(10)
->create();
}
}

View File

@@ -1,8 +1,10 @@
<?php
use App\Models\SuperAdminActionLog;
use App\Services\Monitoring\PackageLimitMetrics;
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schedule;
Artisan::command('inspire', function () {
$this->comment(Inspiring::quote());
@@ -21,3 +23,7 @@ Artisan::command('metrics:package-limits {--reset}', function () {
$this->comment('Package limit metrics cache was reset.');
}
})->purpose('Inspect package limit monitoring counters and optionally reset them');
Schedule::command('model:prune', [
'--model' => [SuperAdminActionLog::class],
])->daily();

View File

@@ -5,6 +5,7 @@ namespace Tests\Feature;
use App\Filament\Clusters\DailyOps\Resources\Photos\Pages\ListPhotos;
use App\Models\Event;
use App\Models\Photo;
use App\Models\SuperAdminActionLog;
use App\Models\Tenant;
use App\Models\User;
use Filament\Actions\Testing\TestAction;
@@ -40,6 +41,10 @@ class PhotoModerationQueueTest extends TestCase
$this->assertSame('Looks good.', $photo->moderation_notes);
$this->assertNotNull($photo->moderated_at);
$this->assertSame($user->id, $photo->moderated_by);
$this->assertTrue(SuperAdminActionLog::query()
->where('action', 'photo.approved')
->where('subject_id', $photo->id)
->exists());
}
public function test_superadmin_can_bulk_reject_pending_photos(): void
@@ -74,6 +79,10 @@ class PhotoModerationQueueTest extends TestCase
$this->assertNotNull($photoB->moderated_at);
$this->assertSame($user->id, $photoA->moderated_by);
$this->assertSame($user->id, $photoB->moderated_by);
$this->assertTrue(SuperAdminActionLog::query()
->where('action', 'photo.rejected')
->whereIn('subject_id', [$photoA->id, $photoB->id])
->count() === 2);
}
private function bootSuperAdminPanel(User $user): void

View File

@@ -0,0 +1,141 @@
<?php
namespace Tests\Feature;
use App\Filament\Resources\GiftVoucherResource\Pages\ListGiftVouchers;
use App\Filament\Resources\MediaStorageTargetResource\Pages\EditMediaStorageTarget;
use App\Filament\Resources\TaskResource\Pages\ImportTasks;
use App\Models\Emotion;
use App\Models\EventType;
use App\Models\GiftVoucher;
use App\Models\MediaStorageTarget;
use App\Models\SuperAdminActionLog;
use App\Models\Task;
use App\Models\User;
use App\Services\GiftVouchers\GiftVoucherService;
use Filament\Actions\Testing\TestAction;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage;
use Livewire\Livewire;
use Tests\TestCase;
class SuperAdminAuditLogMutationTest extends TestCase
{
use RefreshDatabase;
public function test_save_without_changes_does_not_create_audit_log(): void
{
$user = User::factory()->create(['role' => 'super_admin']);
$target = MediaStorageTarget::query()->create([
'key' => 'hot-storage',
'name' => 'Hot Storage',
'driver' => 'local',
'config' => [],
'is_hot' => true,
'is_default' => false,
'is_active' => true,
'priority' => 0,
]);
$this->bootSuperAdminPanel($user);
Livewire::test(EditMediaStorageTarget::class, ['record' => $target->getKey()])
->call('save');
$this->assertFalse(SuperAdminActionLog::query()->exists());
}
public function test_gift_voucher_issue_action_creates_audit_log(): void
{
$user = User::factory()->create(['role' => 'super_admin']);
$voucher = GiftVoucher::factory()->create();
$this->bootSuperAdminPanel($user);
$this->mock(GiftVoucherService::class, function ($mock) use ($voucher): void {
$mock->shouldReceive('issueFromPaddle')
->once()
->andReturn($voucher);
});
Livewire::test(ListGiftVouchers::class)
->callAction(TestAction::make('issue'), [
'amount' => 25,
'currency' => 'EUR',
'purchaser_email' => 'buyer@example.com',
]);
$this->assertTrue(SuperAdminActionLog::query()
->where('action', 'gift-voucher.issued')
->where('subject_id', $voucher->id)
->exists());
}
public function test_task_import_creates_audit_logs(): void
{
Storage::fake('public');
$user = User::factory()->create(['role' => 'super_admin']);
$emotion = Emotion::factory()->create(['name' => ['de' => 'Freude', 'en' => 'Joy']]);
$eventType = EventType::factory()->create(['slug' => 'party']);
$headers = [
'emotion_name',
'emotion_name_de',
'emotion_name_en',
'event_type_slug',
'title_de',
'title_en',
'description_de',
'description_en',
'difficulty',
'example_text_de',
'example_text_en',
'sort_order',
'is_active',
];
$row = [
'',
$emotion->name['de'],
$emotion->name['en'],
$eventType->slug,
'Aufgabe',
'Task',
'Beschreibung',
'Description',
'easy',
'Beispiel',
'Example',
'1',
'1',
];
$csv = implode(',', $headers)."\n".implode(',', $row)."\n";
Storage::disk('public')->put('imports/tasks.csv', $csv);
$this->bootSuperAdminPanel($user);
$component = app(ImportTasks::class);
$method = new \ReflectionMethod($component, 'importTasksCsv');
$method->setAccessible(true);
$method->invoke($component, Storage::disk('public')->path('imports/tasks.csv'));
$this->assertSame(1, Task::query()->count());
$this->assertTrue(SuperAdminActionLog::query()
->where('action', 'task.created')
->exists());
}
private function bootSuperAdminPanel(User $user): void
{
$panel = Filament::getPanel('superadmin');
$this->assertNotNull($panel);
Filament::setCurrentPanel($panel);
Filament::bootCurrentPanel();
Filament::auth()->login($user);
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Tests\Feature;
use App\Filament\SuperAdmin\Pages\GuestPolicySettingsPage;
use App\Models\SuperAdminActionLog;
use App\Models\User;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Tests\TestCase;
class SuperAdminAuditLogSettingsTest extends TestCase
{
use RefreshDatabase;
public function test_superadmin_settings_save_creates_audit_log(): void
{
$user = User::factory()->create(['role' => 'super_admin']);
$this->bootSuperAdminPanel($user);
Livewire::test(GuestPolicySettingsPage::class)
->set('guest_downloads_enabled', false)
->call('save');
$this->assertTrue(SuperAdminActionLog::query()
->where('action', 'guest_policy.updated')
->exists());
}
private function bootSuperAdminPanel(User $user): void
{
$panel = Filament::getPanel('superadmin');
$this->assertNotNull($panel);
Filament::setCurrentPanel($panel);
Filament::bootCurrentPanel();
Filament::auth()->login($user);
}
}

View File

@@ -4,6 +4,7 @@ namespace Tests\Feature;
use App\Filament\Resources\TenantResource\Pages\ListTenants;
use App\Jobs\AnonymizeAccount;
use App\Models\SuperAdminActionLog;
use App\Models\Tenant;
use App\Models\TenantLifecycleEvent;
use App\Models\User;
@@ -79,6 +80,10 @@ class TenantLifecycleActionsTest extends TestCase
$tenant->refresh();
$this->assertFalse((bool) $tenant->is_active);
$this->assertTrue(SuperAdminActionLog::query()
->where('action', 'tenant.deactivated')
->where('subject_id', $tenant->id)
->exists());
$this->assertTrue(TenantLifecycleEvent::query()
->where('tenant_id', $tenant->id)
->where('type', 'deactivated')