From 412ecbe6914e6d77a5dcc7e3f47f0c2238b4de1f Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Fri, 2 Jan 2026 11:57:49 +0100 Subject: [PATCH] Implement superadmin audit log for mutations --- .beads/issues.jsonl | 2 +- .beads/last-touched | 2 +- .../Blog/Resources/CategoryResource.php | 36 +++-- .../CategoryResource/Pages/CreateCategory.php | 6 +- .../CategoryResource/Pages/EditCategory.php | 19 ++- app/Filament/Blog/Resources/PostResource.php | 22 ++- .../PostResource/Pages/CreatePost.php | 6 +- .../Resources/PostResource/Pages/EditPost.php | 12 +- .../Resources/Photos/Pages/EditPhoto.php | 12 +- .../Resources/Photos/Tables/PhotosTable.php | 33 +++- .../Pages/ManageSuperAdminActionLogs.php | 16 ++ .../SuperAdminActionLogResource.php | 123 +++++++++++++++ .../Resources/Coupons/Pages/CreateCoupon.php | 6 +- .../Resources/Coupons/Pages/EditCoupon.php | 31 +++- .../Resources/Coupons/Pages/ViewCoupon.php | 9 +- .../Resources/Coupons/Tables/CouponsTable.php | 49 +++++- app/Filament/Resources/EmotionResource.php | 23 ++- .../EmotionResource/Pages/ManageEmotions.php | 9 +- .../Resources/EventPurchaseResource.php | 22 ++- .../Pages/CreateEventPurchase.php | 6 +- .../Pages/EditEventPurchase.php | 14 +- .../Pages/ViewEventPurchase.php | 11 +- app/Filament/Resources/EventResource.php | 34 ++++- .../EventResource/Pages/CreateEvent.php | 4 +- .../EventResource/Pages/EditEvent.php | 4 +- .../EventPackagesRelationManager.php | 40 ++++- app/Filament/Resources/EventTypeResource.php | 23 ++- .../Pages/ManageEventTypes.php | 9 +- .../Resources/GiftVoucherResource.php | 22 +++ .../Pages/ListGiftVouchers.php | 20 ++- app/Filament/Resources/LegalPageResource.php | 23 ++- .../LegalPageResource/Pages/EditLegalPage.php | 4 +- .../Resources/MediaStorageTargetResource.php | 23 ++- .../Pages/CreateMediaStorageTarget.php | 5 +- .../Pages/EditMediaStorageTarget.php | 13 +- .../Resources/PackageAddonResource.php | 23 ++- .../Pages/CreatePackageAddon.php | 4 +- .../Pages/EditPackageAddon.php | 4 +- app/Filament/Resources/PackageResource.php | 71 ++++++++- .../PackageResource/Pages/CreatePackage.php | 4 +- .../PackageResource/Pages/EditPackage.php | 12 +- .../Resources/Pages/AuditedCreateRecord.php | 19 +++ .../Resources/Pages/AuditedEditRecord.php | 25 ++++ app/Filament/Resources/PhotoResource.php | 82 +++++++++- .../PhotoResource/Pages/EditPhoto.php | 4 +- .../Pages/EditPhotoboothSetting.php | 4 +- .../Tables/PhotoboothSettingsTable.php | 9 +- app/Filament/Resources/PurchaseResource.php | 30 +++- .../PurchaseResource/Pages/CreatePurchase.php | 6 +- .../PurchaseResource/Pages/EditPurchase.php | 14 +- .../PurchaseResource/Pages/ViewPurchase.php | 23 ++- app/Filament/Resources/TaskResource.php | 30 +++- .../TaskResource/Pages/ImportTasks.php | 26 +++- .../TaskResource/Pages/ManageTasks.php | 9 +- .../Pages/ViewTenantFeedback.php | 8 +- .../Tables/TenantFeedbackTable.php | 33 +++- .../Resources/TenantPackageResource.php | 30 +++- .../Pages/CreateTenantPackage.php | 4 +- .../Pages/EditTenantPackage.php | 12 +- .../Pages/ViewTenantPackage.php | 16 +- app/Filament/Resources/TenantResource.php | 107 ++++++++++++- .../TenantResource/Pages/EditTenant.php | 4 +- .../TenantResource/Pages/ViewTenant.php | 9 +- .../Pages/ViewTenantLifecycle.php | 9 +- .../PackagePurchasesRelationManager.php | 15 +- .../PurchasesRelationManager.php | 36 +++-- .../TenantPackagesRelationManager.php | 46 +++++- app/Filament/Resources/UserResource.php | 23 ++- .../UserResource/Pages/CreateUser.php | 4 +- .../Resources/UserResource/Pages/EditUser.php | 12 +- .../Pages/GuestPolicySettingsPage.php | 21 +++ .../Pages/WatermarkSettingsPage.php | 16 ++ app/Models/SuperAdminActionLog.php | 40 +++++ app/Services/Audit/SuperAdminAuditLogger.php | 122 +++++++++++++++ .../factories/SuperAdminActionLogFactory.php | 42 ++++++ ...5_create_super_admin_action_logs_table.php | 35 +++++ .../seeders/SuperAdminActionLogSeeder.php | 19 +++ routes/console.php | 6 + tests/Feature/PhotoModerationQueueTest.php | 9 ++ .../SuperAdminAuditLogMutationTest.php | 141 ++++++++++++++++++ .../SuperAdminAuditLogSettingsTest.php | 42 ++++++ tests/Feature/TenantLifecycleActionsTest.php | 5 + 82 files changed, 1766 insertions(+), 192 deletions(-) create mode 100644 app/Filament/Clusters/RareAdmin/Resources/SuperAdminActionLogs/Pages/ManageSuperAdminActionLogs.php create mode 100644 app/Filament/Clusters/RareAdmin/Resources/SuperAdminActionLogs/SuperAdminActionLogResource.php create mode 100644 app/Filament/Resources/Pages/AuditedCreateRecord.php create mode 100644 app/Filament/Resources/Pages/AuditedEditRecord.php create mode 100644 app/Models/SuperAdminActionLog.php create mode 100644 app/Services/Audit/SuperAdminAuditLogger.php create mode 100644 database/factories/SuperAdminActionLogFactory.php create mode 100644 database/migrations/2026_01_02_100805_create_super_admin_action_logs_table.php create mode 100644 database/seeders/SuperAdminActionLogSeeder.php create mode 100644 tests/Feature/SuperAdminAuditLogMutationTest.php create mode 100644 tests/Feature/SuperAdminAuditLogSettingsTest.php diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 31c3033..931ba18 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -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)"} diff --git a/.beads/last-touched b/.beads/last-touched index 6827291..b4222b2 100644 --- a/.beads/last-touched +++ b/.beads/last-touched @@ -1 +1 @@ -fotospiel-app-z2k +fotospiel-app-iyc diff --git a/app/Filament/Blog/Resources/CategoryResource.php b/app/Filament/Blog/Resources/CategoryResource.php index 32ccc9a..1c4f436 100644 --- a/app/Filament/Blog/Resources/CategoryResource.php +++ b/app/Filament/Blog/Resources/CategoryResource.php @@ -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 + ); + } + }), ]), ]); } diff --git a/app/Filament/Blog/Resources/CategoryResource/Pages/CreateCategory.php b/app/Filament/Blog/Resources/CategoryResource/Pages/CreateCategory.php index a668dc1..58e27bb 100644 --- a/app/Filament/Blog/Resources/CategoryResource/Pages/CreateCategory.php +++ b/app/Filament/Blog/Resources/CategoryResource/Pages/CreateCategory.php @@ -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; @@ -30,4 +30,4 @@ class CreateCategory extends CreateRecord $this->record = static::getResource()::getModel()::create($data); } -} \ No newline at end of file +} diff --git a/app/Filament/Blog/Resources/CategoryResource/Pages/EditCategory.php b/app/Filament/Blog/Resources/CategoryResource/Pages/EditCategory.php index 7028b51..e7022f8 100644 --- a/app/Filament/Blog/Resources/CategoryResource/Pages/EditCategory.php +++ b/app/Filament/Blog/Resources/CategoryResource/Pages/EditCategory.php @@ -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(); } -} \ No newline at end of file +} diff --git a/app/Filament/Blog/Resources/PostResource.php b/app/Filament/Blog/Resources/PostResource.php index 02c0004..857d795 100644 --- a/app/Filament/Blog/Resources/PostResource.php +++ b/app/Filament/Blog/Resources/PostResource.php @@ -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 + ); + } + }), ]), ]); } diff --git a/app/Filament/Blog/Resources/PostResource/Pages/CreatePost.php b/app/Filament/Blog/Resources/PostResource/Pages/CreatePost.php index 273e44c..a4c5738 100644 --- a/app/Filament/Blog/Resources/PostResource/Pages/CreatePost.php +++ b/app/Filament/Blog/Resources/PostResource/Pages/CreatePost.php @@ -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; -} \ No newline at end of file +} diff --git a/app/Filament/Blog/Resources/PostResource/Pages/EditPost.php b/app/Filament/Blog/Resources/PostResource/Pages/EditPost.php index 5091c01..66474e6 100644 --- a/app/Filament/Blog/Resources/PostResource/Pages/EditPost.php +++ b/app/Filament/Blog/Resources/PostResource/Pages/EditPost.php @@ -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 + )), ]; } diff --git a/app/Filament/Clusters/DailyOps/Resources/Photos/Pages/EditPhoto.php b/app/Filament/Clusters/DailyOps/Resources/Photos/Pages/EditPhoto.php index 4bdcbc8..1ab9f90 100644 --- a/app/Filament/Clusters/DailyOps/Resources/Photos/Pages/EditPhoto.php +++ b/app/Filament/Clusters/DailyOps/Resources/Photos/Pages/EditPhoto.php @@ -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 + )), ]; } } diff --git a/app/Filament/Clusters/DailyOps/Resources/Photos/Tables/PhotosTable.php b/app/Filament/Clusters/DailyOps/Resources/Photos/Tables/PhotosTable.php index dd1b1d4..4a943f8 100644 --- a/app/Filament/Clusters/DailyOps/Resources/Photos/Tables/PhotosTable.php +++ b/app/Filament/Clusters/DailyOps/Resources/Photos/Tables/PhotosTable.php @@ -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 diff --git a/app/Filament/Clusters/RareAdmin/Resources/SuperAdminActionLogs/Pages/ManageSuperAdminActionLogs.php b/app/Filament/Clusters/RareAdmin/Resources/SuperAdminActionLogs/Pages/ManageSuperAdminActionLogs.php new file mode 100644 index 0000000..55913dc --- /dev/null +++ b/app/Filament/Clusters/RareAdmin/Resources/SuperAdminActionLogs/Pages/ManageSuperAdminActionLogs.php @@ -0,0 +1,16 @@ +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('/'), + ]; + } +} diff --git a/app/Filament/Resources/Coupons/Pages/CreateCoupon.php b/app/Filament/Resources/Coupons/Pages/CreateCoupon.php index f90a157..60c7256 100644 --- a/app/Filament/Resources/Coupons/Pages/CreateCoupon.php +++ b/app/Filament/Resources/Coupons/Pages/CreateCoupon.php @@ -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); } } diff --git a/app/Filament/Resources/Coupons/Pages/EditCoupon.php b/app/Filament/Resources/Coupons/Pages/EditCoupon.php index f1cf47c..42d1af9 100644 --- a/app/Filament/Resources/Coupons/Pages/EditCoupon.php +++ b/app/Filament/Resources/Coupons/Pages/EditCoupon.php @@ -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); } } diff --git a/app/Filament/Resources/Coupons/Pages/ViewCoupon.php b/app/Filament/Resources/Coupons/Pages/ViewCoupon.php index 824bbd5..d09ecef 100644 --- a/app/Filament/Resources/Coupons/Pages/ViewCoupon.php +++ b/app/Filament/Resources/Coupons/Pages/ViewCoupon.php @@ -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 + )), ]; } } diff --git a/app/Filament/Resources/Coupons/Tables/CouponsTable.php b/app/Filament/Resources/Coupons/Tables/CouponsTable.php index 88bb1d3..4f2aa21 100644 --- a/app/Filament/Resources/Coupons/Tables/CouponsTable.php +++ b/app/Filament/Resources/Coupons/Tables/CouponsTable.php @@ -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 + ); + } + }), ]), ]); } diff --git a/app/Filament/Resources/EmotionResource.php b/app/Filament/Resources/EmotionResource.php index 45861d1..6802717 100644 --- a/app/Filament/Resources/EmotionResource.php +++ b/app/Filament/Resources/EmotionResource.php @@ -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 + ); + } + }), ]); } diff --git a/app/Filament/Resources/EmotionResource/Pages/ManageEmotions.php b/app/Filament/Resources/EmotionResource/Pages/ManageEmotions.php index 60f8703..c529c1e 100644 --- a/app/Filament/Resources/EmotionResource/Pages/ManageEmotions.php +++ b/app/Filament/Resources/EmotionResource/Pages/ManageEmotions.php @@ -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') diff --git a/app/Filament/Resources/EventPurchaseResource.php b/app/Filament/Resources/EventPurchaseResource.php index a63b013..93bad03 100644 --- a/app/Filament/Resources/EventPurchaseResource.php +++ b/app/Filament/Resources/EventPurchaseResource.php @@ -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), diff --git a/app/Filament/Resources/EventPurchaseResource/Pages/CreateEventPurchase.php b/app/Filament/Resources/EventPurchaseResource/Pages/CreateEventPurchase.php index 69dd6a5..808f154 100644 --- a/app/Filament/Resources/EventPurchaseResource/Pages/CreateEventPurchase.php +++ b/app/Filament/Resources/EventPurchaseResource/Pages/CreateEventPurchase.php @@ -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; -} \ No newline at end of file +} diff --git a/app/Filament/Resources/EventPurchaseResource/Pages/EditEventPurchase.php b/app/Filament/Resources/EventPurchaseResource/Pages/EditEventPurchase.php index b6fbd9e..921239e 100644 --- a/app/Filament/Resources/EventPurchaseResource/Pages/EditEventPurchase.php +++ b/app/Filament/Resources/EventPurchaseResource/Pages/EditEventPurchase.php @@ -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 + )), ]; } -} \ No newline at end of file +} diff --git a/app/Filament/Resources/EventPurchaseResource/Pages/ViewEventPurchase.php b/app/Filament/Resources/EventPurchaseResource/Pages/ViewEventPurchase.php index 5776d21..ae883b3 100644 --- a/app/Filament/Resources/EventPurchaseResource/Pages/ViewEventPurchase.php +++ b/app/Filament/Resources/EventPurchaseResource/Pages/ViewEventPurchase.php @@ -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 + )), ]; } -} \ No newline at end of file +} diff --git a/app/Filament/Resources/EventResource.php b/app/Filament/Resources/EventResource.php index c1ce6a0..452fbb7 100644 --- a/app/Filament/Resources/EventResource.php +++ b/app/Filament/Resources/EventResource.php @@ -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 + ); + } + }), ]); } diff --git a/app/Filament/Resources/EventResource/Pages/CreateEvent.php b/app/Filament/Resources/EventResource/Pages/CreateEvent.php index c1031e9..cdc509a 100644 --- a/app/Filament/Resources/EventResource/Pages/CreateEvent.php +++ b/app/Filament/Resources/EventResource/Pages/CreateEvent.php @@ -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; } diff --git a/app/Filament/Resources/EventResource/Pages/EditEvent.php b/app/Filament/Resources/EventResource/Pages/EditEvent.php index a1ef6d8..ab31fa9 100644 --- a/app/Filament/Resources/EventResource/Pages/EditEvent.php +++ b/app/Filament/Resources/EventResource/Pages/EditEvent.php @@ -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; } diff --git a/app/Filament/Resources/EventResource/RelationManagers/EventPackagesRelationManager.php b/app/Filament/Resources/EventResource/RelationManagers/EventPackagesRelationManager.php index 4e49a4d..b5bfcea 100644 --- a/app/Filament/Resources/EventResource/RelationManagers/EventPackagesRelationManager.php +++ b/app/Filament/Resources/EventResource/RelationManagers/EventPackagesRelationManager.php @@ -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 + ); + } + }), ]), ]); } diff --git a/app/Filament/Resources/EventTypeResource.php b/app/Filament/Resources/EventTypeResource.php index ffa9968..28f8115 100644 --- a/app/Filament/Resources/EventTypeResource.php +++ b/app/Filament/Resources/EventTypeResource.php @@ -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 + ); + } + }), ]); } diff --git a/app/Filament/Resources/EventTypeResource/Pages/ManageEventTypes.php b/app/Filament/Resources/EventTypeResource/Pages/ManageEventTypes.php index 571b378..9cdf07f 100644 --- a/app/Filament/Resources/EventTypeResource/Pages/ManageEventTypes.php +++ b/app/Filament/Resources/EventTypeResource/Pages/ManageEventTypes.php @@ -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 + )), ]; } } diff --git a/app/Filament/Resources/GiftVoucherResource.php b/app/Filament/Resources/GiftVoucherResource.php index cd973ee..12c5067 100644 --- a/app/Filament/Resources/GiftVoucherResource.php +++ b/app/Filament/Resources/GiftVoucherResource.php @@ -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'), ]); diff --git a/app/Filament/Resources/GiftVoucherResource/Pages/ListGiftVouchers.php b/app/Filament/Resources/GiftVoucherResource/Pages/ListGiftVouchers.php index 4bed8fc..95467e5 100644 --- a/app/Filament/Resources/GiftVoucherResource/Pages/ListGiftVouchers.php +++ b/app/Filament/Resources/GiftVoucherResource/Pages/ListGiftVouchers.php @@ -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'), ]; diff --git a/app/Filament/Resources/LegalPageResource.php b/app/Filament/Resources/LegalPageResource.php index aa184f4..6ae42c4 100644 --- a/app/Filament/Resources/LegalPageResource.php +++ b/app/Filament/Resources/LegalPageResource.php @@ -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 + ); + } + }), ]); } diff --git a/app/Filament/Resources/LegalPageResource/Pages/EditLegalPage.php b/app/Filament/Resources/LegalPageResource/Pages/EditLegalPage.php index 08e716c..2811036 100644 --- a/app/Filament/Resources/LegalPageResource/Pages/EditLegalPage.php +++ b/app/Filament/Resources/LegalPageResource/Pages/EditLegalPage.php @@ -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; } diff --git a/app/Filament/Resources/MediaStorageTargetResource.php b/app/Filament/Resources/MediaStorageTargetResource.php index 50b52bb..8100d32 100644 --- a/app/Filament/Resources/MediaStorageTargetResource.php +++ b/app/Filament/Resources/MediaStorageTargetResource.php @@ -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 + ); + } + }), ]); } diff --git a/app/Filament/Resources/MediaStorageTargetResource/Pages/CreateMediaStorageTarget.php b/app/Filament/Resources/MediaStorageTargetResource/Pages/CreateMediaStorageTarget.php index 96c1fc9..bab02e6 100644 --- a/app/Filament/Resources/MediaStorageTargetResource/Pages/CreateMediaStorageTarget.php +++ b/app/Filament/Resources/MediaStorageTargetResource/Pages/CreateMediaStorageTarget.php @@ -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; } - diff --git a/app/Filament/Resources/MediaStorageTargetResource/Pages/EditMediaStorageTarget.php b/app/Filament/Resources/MediaStorageTargetResource/Pages/EditMediaStorageTarget.php index e3dad17..ea8b07c 100644 --- a/app/Filament/Resources/MediaStorageTargetResource/Pages/EditMediaStorageTarget.php +++ b/app/Filament/Resources/MediaStorageTargetResource/Pages/EditMediaStorageTarget.php @@ -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 + )), ]; } } - diff --git a/app/Filament/Resources/PackageAddonResource.php b/app/Filament/Resources/PackageAddonResource.php index e82bcb1..c4b5953 100644 --- a/app/Filament/Resources/PackageAddonResource.php +++ b/app/Filament/Resources/PackageAddonResource.php @@ -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 + ); + } + }), ]), ]); } diff --git a/app/Filament/Resources/PackageAddonResource/Pages/CreatePackageAddon.php b/app/Filament/Resources/PackageAddonResource/Pages/CreatePackageAddon.php index 58c8f79..a5d3658 100644 --- a/app/Filament/Resources/PackageAddonResource/Pages/CreatePackageAddon.php +++ b/app/Filament/Resources/PackageAddonResource/Pages/CreatePackageAddon.php @@ -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; } diff --git a/app/Filament/Resources/PackageAddonResource/Pages/EditPackageAddon.php b/app/Filament/Resources/PackageAddonResource/Pages/EditPackageAddon.php index 706e646..c3caa1f 100644 --- a/app/Filament/Resources/PackageAddonResource/Pages/EditPackageAddon.php +++ b/app/Filament/Resources/PackageAddonResource/Pages/EditPackageAddon.php @@ -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; diff --git a/app/Filament/Resources/PackageResource.php b/app/Filament/Resources/PackageResource.php index 1e0fe6d..f161137 100644 --- a/app/Filament/Resources/PackageResource.php +++ b/app/Filament/Resources/PackageResource.php @@ -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 + ); + } + }), ]), ]); } diff --git a/app/Filament/Resources/PackageResource/Pages/CreatePackage.php b/app/Filament/Resources/PackageResource/Pages/CreatePackage.php index 2111007..943b8a9 100644 --- a/app/Filament/Resources/PackageResource/Pages/CreatePackage.php +++ b/app/Filament/Resources/PackageResource/Pages/CreatePackage.php @@ -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; } diff --git a/app/Filament/Resources/PackageResource/Pages/EditPackage.php b/app/Filament/Resources/PackageResource/Pages/EditPackage.php index b899994..be1acf4 100644 --- a/app/Filament/Resources/PackageResource/Pages/EditPackage.php +++ b/app/Filament/Resources/PackageResource/Pages/EditPackage.php @@ -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 + )), ]; } } diff --git a/app/Filament/Resources/Pages/AuditedCreateRecord.php b/app/Filament/Resources/Pages/AuditedCreateRecord.php new file mode 100644 index 0000000..7a9c21c --- /dev/null +++ b/app/Filament/Resources/Pages/AuditedCreateRecord.php @@ -0,0 +1,19 @@ +recordModelMutation( + 'created', + $this->record, + SuperAdminAuditLogger::fieldsMetadata($this->form->getState()), + static::class + ); + } +} diff --git a/app/Filament/Resources/Pages/AuditedEditRecord.php b/app/Filament/Resources/Pages/AuditedEditRecord.php new file mode 100644 index 0000000..653ea9f --- /dev/null +++ b/app/Filament/Resources/Pages/AuditedEditRecord.php @@ -0,0 +1,25 @@ +record->getChanges()); + + if ($changed === []) { + return; + } + + app(SuperAdminAuditLogger::class)->recordModelMutation( + 'updated', + $this->record, + SuperAdminAuditLogger::fieldsMetadata($changed ?: $this->form->getState()), + static::class + ); + } +} diff --git a/app/Filament/Resources/PhotoResource.php b/app/Filament/Resources/PhotoResource.php index acd5c1f..8f21dd2 100644 --- a/app/Filament/Resources/PhotoResource.php +++ b/app/Filament/Resources/PhotoResource.php @@ -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 + ); + } + }), ]); } diff --git a/app/Filament/Resources/PhotoResource/Pages/EditPhoto.php b/app/Filament/Resources/PhotoResource/Pages/EditPhoto.php index 878082c..c480f7f 100644 --- a/app/Filament/Resources/PhotoResource/Pages/EditPhoto.php +++ b/app/Filament/Resources/PhotoResource/Pages/EditPhoto.php @@ -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; } diff --git a/app/Filament/Resources/PhotoboothSettings/Pages/EditPhotoboothSetting.php b/app/Filament/Resources/PhotoboothSettings/Pages/EditPhotoboothSetting.php index 0e186eb..9b504c2 100644 --- a/app/Filament/Resources/PhotoboothSettings/Pages/EditPhotoboothSetting.php +++ b/app/Filament/Resources/PhotoboothSettings/Pages/EditPhotoboothSetting.php @@ -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; diff --git a/app/Filament/Resources/PhotoboothSettings/Tables/PhotoboothSettingsTable.php b/app/Filament/Resources/PhotoboothSettings/Tables/PhotoboothSettingsTable.php index f41d476..26ca964 100644 --- a/app/Filament/Resources/PhotoboothSettings/Tables/PhotoboothSettingsTable.php +++ b/app/Filament/Resources/PhotoboothSettings/Tables/PhotoboothSettingsTable.php @@ -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([]); diff --git a/app/Filament/Resources/PurchaseResource.php b/app/Filament/Resources/PurchaseResource.php index 6a74e3b..4422410 100644 --- a/app/Filament/Resources/PurchaseResource.php +++ b/app/Filament/Resources/PurchaseResource.php @@ -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') diff --git a/app/Filament/Resources/PurchaseResource/Pages/CreatePurchase.php b/app/Filament/Resources/PurchaseResource/Pages/CreatePurchase.php index 2d8b4b4..d721aa4 100644 --- a/app/Filament/Resources/PurchaseResource/Pages/CreatePurchase.php +++ b/app/Filament/Resources/PurchaseResource/Pages/CreatePurchase.php @@ -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; -} \ No newline at end of file +} diff --git a/app/Filament/Resources/PurchaseResource/Pages/EditPurchase.php b/app/Filament/Resources/PurchaseResource/Pages/EditPurchase.php index 1377436..8cd5ad2 100644 --- a/app/Filament/Resources/PurchaseResource/Pages/EditPurchase.php +++ b/app/Filament/Resources/PurchaseResource/Pages/EditPurchase.php @@ -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 + )), ]; } -} \ No newline at end of file +} diff --git a/app/Filament/Resources/PurchaseResource/Pages/ViewPurchase.php b/app/Filament/Resources/PurchaseResource/Pages/ViewPurchase.php index b58ec37..c6b2aa8 100644 --- a/app/Filament/Resources/PurchaseResource/Pages/ViewPurchase.php +++ b/app/Filament/Resources/PurchaseResource/Pages/ViewPurchase.php @@ -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 + ); }), ]; } diff --git a/app/Filament/Resources/TaskResource.php b/app/Filament/Resources/TaskResource.php index fb40005..22bd971 100644 --- a/app/Filament/Resources/TaskResource.php +++ b/app/Filament/Resources/TaskResource.php @@ -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 + ); + } + }), ]); } diff --git a/app/Filament/Resources/TaskResource/Pages/ImportTasks.php b/app/Filament/Resources/TaskResource/Pages/ImportTasks.php index 967755e..ef6059d 100644 --- a/app/Filament/Resources/TaskResource/Pages/ImportTasks.php +++ b/app/Filament/Resources/TaskResource/Pages/ImportTasks.php @@ -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]; } } diff --git a/app/Filament/Resources/TaskResource/Pages/ManageTasks.php b/app/Filament/Resources/TaskResource/Pages/ManageTasks.php index f24884a..5b43de6 100644 --- a/app/Filament/Resources/TaskResource/Pages/ManageTasks.php +++ b/app/Filament/Resources/TaskResource/Pages/ManageTasks.php @@ -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') diff --git a/app/Filament/Resources/TenantFeedbackResource/Pages/ViewTenantFeedback.php b/app/Filament/Resources/TenantFeedbackResource/Pages/ViewTenantFeedback.php index 196469e..9dc1b74 100644 --- a/app/Filament/Resources/TenantFeedbackResource/Pages/ViewTenantFeedback.php +++ b/app/Filament/Resources/TenantFeedbackResource/Pages/ViewTenantFeedback.php @@ -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 + )), ]; } } diff --git a/app/Filament/Resources/TenantFeedbackResource/Tables/TenantFeedbackTable.php b/app/Filament/Resources/TenantFeedbackResource/Tables/TenantFeedbackTable.php index 17592e5..d3a6661 100644 --- a/app/Filament/Resources/TenantFeedbackResource/Tables/TenantFeedbackTable.php +++ b/app/Filament/Resources/TenantFeedbackResource/Tables/TenantFeedbackTable.php @@ -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 diff --git a/app/Filament/Resources/TenantPackageResource.php b/app/Filament/Resources/TenantPackageResource.php index e90a166..f5b4978 100644 --- a/app/Filament/Resources/TenantPackageResource.php +++ b/app/Filament/Resources/TenantPackageResource.php @@ -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 + ); + } + }), ]), ]); } diff --git a/app/Filament/Resources/TenantPackageResource/Pages/CreateTenantPackage.php b/app/Filament/Resources/TenantPackageResource/Pages/CreateTenantPackage.php index 73811b2..828f6f8 100644 --- a/app/Filament/Resources/TenantPackageResource/Pages/CreateTenantPackage.php +++ b/app/Filament/Resources/TenantPackageResource/Pages/CreateTenantPackage.php @@ -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; } diff --git a/app/Filament/Resources/TenantPackageResource/Pages/EditTenantPackage.php b/app/Filament/Resources/TenantPackageResource/Pages/EditTenantPackage.php index c6bb61d..3a3fb8e 100644 --- a/app/Filament/Resources/TenantPackageResource/Pages/EditTenantPackage.php +++ b/app/Filament/Resources/TenantPackageResource/Pages/EditTenantPackage.php @@ -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 + )), ]; } } diff --git a/app/Filament/Resources/TenantPackageResource/Pages/ViewTenantPackage.php b/app/Filament/Resources/TenantPackageResource/Pages/ViewTenantPackage.php index 09811ef..3e8e429 100644 --- a/app/Filament/Resources/TenantPackageResource/Pages/ViewTenantPackage.php +++ b/app/Filament/Resources/TenantPackageResource/Pages/ViewTenantPackage.php @@ -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 + )), ]; } } diff --git a/app/Filament/Resources/TenantResource.php b/app/Filament/Resources/TenantResource.php index 7d3ebda..8028ee9 100644 --- a/app/Filament/Resources/TenantResource.php +++ b/app/Filament/Resources/TenantResource.php @@ -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 + ); }), ]; } diff --git a/app/Filament/Resources/TenantResource/Pages/EditTenant.php b/app/Filament/Resources/TenantResource/Pages/EditTenant.php index 89244be..964036e 100644 --- a/app/Filament/Resources/TenantResource/Pages/EditTenant.php +++ b/app/Filament/Resources/TenantResource/Pages/EditTenant.php @@ -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; diff --git a/app/Filament/Resources/TenantResource/Pages/ViewTenant.php b/app/Filament/Resources/TenantResource/Pages/ViewTenant.php index 76944fb..6847c1e 100644 --- a/app/Filament/Resources/TenantResource/Pages/ViewTenant.php +++ b/app/Filament/Resources/TenantResource/Pages/ViewTenant.php @@ -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(), ]; } diff --git a/app/Filament/Resources/TenantResource/Pages/ViewTenantLifecycle.php b/app/Filament/Resources/TenantResource/Pages/ViewTenantLifecycle.php index 1856cc9..def795f 100644 --- a/app/Filament/Resources/TenantResource/Pages/ViewTenantLifecycle.php +++ b/app/Filament/Resources/TenantResource/Pages/ViewTenantLifecycle.php @@ -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'), diff --git a/app/Filament/Resources/TenantResource/RelationManagers/PackagePurchasesRelationManager.php b/app/Filament/Resources/TenantResource/RelationManagers/PackagePurchasesRelationManager.php index 58273bd..9ca7e70 100644 --- a/app/Filament/Resources/TenantResource/RelationManagers/PackagePurchasesRelationManager.php +++ b/app/Filament/Resources/TenantResource/RelationManagers/PackagePurchasesRelationManager.php @@ -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 + ); + } + }), ]), ]); } diff --git a/app/Filament/Resources/TenantResource/RelationManagers/PurchasesRelationManager.php b/app/Filament/Resources/TenantResource/RelationManagers/PurchasesRelationManager.php index ffd2a43..b85023a 100644 --- a/app/Filament/Resources/TenantResource/RelationManagers/PurchasesRelationManager.php +++ b/app/Filament/Resources/TenantResource/RelationManagers/PurchasesRelationManager.php @@ -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 + ); + } + }), ]), ]); } } - - diff --git a/app/Filament/Resources/TenantResource/RelationManagers/TenantPackagesRelationManager.php b/app/Filament/Resources/TenantResource/RelationManagers/TenantPackagesRelationManager.php index f16bd7e..c02508d 100644 --- a/app/Filament/Resources/TenantResource/RelationManagers/TenantPackagesRelationManager.php +++ b/app/Filament/Resources/TenantResource/RelationManagers/TenantPackagesRelationManager.php @@ -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 + ); + } + }), ]), ]); } diff --git a/app/Filament/Resources/UserResource.php b/app/Filament/Resources/UserResource.php index b613af0..80d53f0 100644 --- a/app/Filament/Resources/UserResource.php +++ b/app/Filament/Resources/UserResource.php @@ -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 + ); + } + }), ]), ]); } diff --git a/app/Filament/Resources/UserResource/Pages/CreateUser.php b/app/Filament/Resources/UserResource/Pages/CreateUser.php index 575a620..4db2971 100644 --- a/app/Filament/Resources/UserResource/Pages/CreateUser.php +++ b/app/Filament/Resources/UserResource/Pages/CreateUser.php @@ -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; diff --git a/app/Filament/Resources/UserResource/Pages/EditUser.php b/app/Filament/Resources/UserResource/Pages/EditUser.php index df2601f..2ee0ba9 100644 --- a/app/Filament/Resources/UserResource/Pages/EditUser.php +++ b/app/Filament/Resources/UserResource/Pages/EditUser.php @@ -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 + )), ]; } diff --git a/app/Filament/SuperAdmin/Pages/GuestPolicySettingsPage.php b/app/Filament/SuperAdmin/Pages/GuestPolicySettingsPage.php index a1eddbf..0b20ace 100644 --- a/app/Filament/SuperAdmin/Pages/GuestPolicySettingsPage.php +++ b/app/Filament/SuperAdmin/Pages/GuestPolicySettingsPage.php @@ -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() diff --git a/app/Filament/SuperAdmin/Pages/WatermarkSettingsPage.php b/app/Filament/SuperAdmin/Pages/WatermarkSettingsPage.php index 92e8ae0..4fee93f 100644 --- a/app/Filament/SuperAdmin/Pages/WatermarkSettingsPage.php +++ b/app/Filament/SuperAdmin/Pages/WatermarkSettingsPage.php @@ -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() diff --git a/app/Models/SuperAdminActionLog.php b/app/Models/SuperAdminActionLog.php new file mode 100644 index 0000000..2fb3208 --- /dev/null +++ b/app/Models/SuperAdminActionLog.php @@ -0,0 +1,40 @@ + */ + 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)); + } +} diff --git a/app/Services/Audit/SuperAdminAuditLogger.php b/app/Services/Audit/SuperAdminAuditLogger.php new file mode 100644 index 0000000..2a70a57 --- /dev/null +++ b/app/Services/Audit/SuperAdminAuditLogger.php @@ -0,0 +1,122 @@ +> $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> $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|array $data + * @return array> + */ + 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> $metadata + * @return array> + */ + 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; + } +} diff --git a/database/factories/SuperAdminActionLogFactory.php b/database/factories/SuperAdminActionLogFactory.php new file mode 100644 index 0000000..a380e15 --- /dev/null +++ b/database/factories/SuperAdminActionLogFactory.php @@ -0,0 +1,42 @@ + + */ +class SuperAdminActionLogFactory extends Factory +{ + protected $model = SuperAdminActionLog::class; + + /** + * Define the model's default state. + * + * @return array + */ + 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(), + ]; + } +} diff --git a/database/migrations/2026_01_02_100805_create_super_admin_action_logs_table.php b/database/migrations/2026_01_02_100805_create_super_admin_action_logs_table.php new file mode 100644 index 0000000..d41edb0 --- /dev/null +++ b/database/migrations/2026_01_02_100805_create_super_admin_action_logs_table.php @@ -0,0 +1,35 @@ +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'); + } +}; diff --git a/database/seeders/SuperAdminActionLogSeeder.php b/database/seeders/SuperAdminActionLogSeeder.php new file mode 100644 index 0000000..0b62cdc --- /dev/null +++ b/database/seeders/SuperAdminActionLogSeeder.php @@ -0,0 +1,19 @@ +count(10) + ->create(); + } +} diff --git a/routes/console.php b/routes/console.php index a886cd0..2220532 100644 --- a/routes/console.php +++ b/routes/console.php @@ -1,8 +1,10 @@ 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(); diff --git a/tests/Feature/PhotoModerationQueueTest.php b/tests/Feature/PhotoModerationQueueTest.php index 0116a66..2eef139 100644 --- a/tests/Feature/PhotoModerationQueueTest.php +++ b/tests/Feature/PhotoModerationQueueTest.php @@ -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 diff --git a/tests/Feature/SuperAdminAuditLogMutationTest.php b/tests/Feature/SuperAdminAuditLogMutationTest.php new file mode 100644 index 0000000..57209f6 --- /dev/null +++ b/tests/Feature/SuperAdminAuditLogMutationTest.php @@ -0,0 +1,141 @@ +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); + } +} diff --git a/tests/Feature/SuperAdminAuditLogSettingsTest.php b/tests/Feature/SuperAdminAuditLogSettingsTest.php new file mode 100644 index 0000000..e3c8be7 --- /dev/null +++ b/tests/Feature/SuperAdminAuditLogSettingsTest.php @@ -0,0 +1,42 @@ +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); + } +} diff --git a/tests/Feature/TenantLifecycleActionsTest.php b/tests/Feature/TenantLifecycleActionsTest.php index ba1808c..d0cd10f 100644 --- a/tests/Feature/TenantLifecycleActionsTest.php +++ b/tests/Feature/TenantLifecycleActionsTest.php @@ -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')