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

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

View File

@@ -59,7 +59,7 @@
{"id":"fotospiel-app-gsv","title":"Localized SEO: validate hreflang via Search Console/Lighthouse","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-01T16:02:36.4821072+01:00","created_by":"soeren","updated_at":"2026-01-01T16:02:36.4821072+01:00"} {"id":"fotospiel-app-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-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-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-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-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)"} {"id":"fotospiel-app-jqy","title":"Tenant admin onboarding: Playwright skeleton for welcome flow","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:08:11.226297707+01:00","created_by":"soeren","updated_at":"2026-01-01T16:08:16.827679424+01:00","closed_at":"2026-01-01T16:08:16.827679424+01:00","close_reason":"Completed in codebase (verified)"}

View File

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

View File

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

View File

@@ -3,9 +3,9 @@
namespace App\Filament\Blog\Resources\CategoryResource\Pages; namespace App\Filament\Blog\Resources\CategoryResource\Pages;
use App\Filament\Blog\Resources\CategoryResource; 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; protected static string $resource = CategoryResource::class;

View File

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

View File

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

View File

@@ -3,9 +3,9 @@
namespace App\Filament\Blog\Resources\PostResource\Pages; namespace App\Filament\Blog\Resources\PostResource\Pages;
use App\Filament\Blog\Resources\PostResource; 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; protected static string $resource = PostResource::class;
} }

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ namespace App\Filament\Clusters\DailyOps\Resources\Photos\Tables;
use App\Models\Event; use App\Models\Event;
use App\Models\Photo; use App\Models\Photo;
use App\Models\Tenant; use App\Models\Tenant;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Actions\BulkAction; use Filament\Actions\BulkAction;
use Filament\Actions\BulkActionGroup; use Filament\Actions\BulkActionGroup;
@@ -208,6 +209,18 @@ class PhotosTable
'moderated_at' => now(), 'moderated_at' => now(),
'moderated_by' => Filament::auth()->id(), '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 private static function applyModerationToRecords(Collection $records, string $status, ?string $notes): int
@@ -215,7 +228,7 @@ class PhotosTable
$moderatedAt = now(); $moderatedAt = now();
$moderatedBy = Filament::auth()->id(); $moderatedBy = Filament::auth()->id();
return Photo::query() $updated = Photo::query()
->whereIn('id', $records->pluck('id')) ->whereIn('id', $records->pluck('id'))
->where('status', 'pending') ->where('status', 'pending')
->update([ ->update([
@@ -224,6 +237,24 @@ class PhotosTable
'moderated_at' => $moderatedAt, 'moderated_at' => $moderatedAt,
'moderated_by' => $moderatedBy, '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 private static function statusLabels(): array

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@
namespace App\Filament\Resources\EmotionResource\Pages; namespace App\Filament\Resources\EmotionResource\Pages;
use App\Filament\Resources\EmotionResource; use App\Filament\Resources\EmotionResource;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions; use Filament\Actions;
use Filament\Resources\Pages\ManageRecords; use Filament\Resources\Pages\ManageRecords;
@@ -13,7 +14,13 @@ class ManageEmotions extends ManageRecords
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ 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') Actions\Action::make('import')
->label(__('admin.common.import_csv')) ->label(__('admin.common.import_csv'))
->icon('heroicon-o-arrow-up-tray') ->icon('heroicon-o-arrow-up-tray')

View File

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

View File

@@ -3,9 +3,9 @@
namespace App\Filament\Resources\EventPurchaseResource\Pages; namespace App\Filament\Resources\EventPurchaseResource\Pages;
use App\Filament\Resources\EventPurchaseResource; 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; protected static string $resource = EventPurchaseResource::class;
} }

View File

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

View File

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

View File

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

View File

@@ -3,9 +3,9 @@
namespace App\Filament\Resources\EventResource\Pages; namespace App\Filament\Resources\EventResource\Pages;
use App\Filament\Resources\EventResource; 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; protected static string $resource = EventResource::class;
} }

View File

@@ -3,9 +3,9 @@
namespace App\Filament\Resources\EventResource\Pages; namespace App\Filament\Resources\EventResource\Pages;
use App\Filament\Resources\EventResource; 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; protected static string $resource = EventResource::class;
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,11 +3,12 @@
namespace App\Filament\Resources\GiftVoucherResource\Pages; namespace App\Filament\Resources\GiftVoucherResource\Pages;
use App\Filament\Resources\GiftVoucherResource; use App\Filament\Resources\GiftVoucherResource;
use App\Services\Audit\SuperAdminAuditLogger;
use App\Services\GiftVouchers\GiftVoucherService; use App\Services\GiftVouchers\GiftVoucherService;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Resources\Pages\ListRecords;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Placeholder; use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Pages\ListRecords;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class ListGiftVouchers extends ListRecords 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'), ->modalHeading('Geschenkgutschein ausstellen'),
]; ];

View File

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

View File

@@ -3,9 +3,9 @@
namespace App\Filament\Resources\LegalPageResource\Pages; namespace App\Filament\Resources\LegalPageResource\Pages;
use App\Filament\Resources\LegalPageResource; 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; protected static string $resource = LegalPageResource::class;
} }

View File

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

View File

@@ -3,10 +3,9 @@
namespace App\Filament\Resources\MediaStorageTargetResource\Pages; namespace App\Filament\Resources\MediaStorageTargetResource\Pages;
use App\Filament\Resources\MediaStorageTargetResource; 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; protected static string $resource = MediaStorageTargetResource::class;
} }

View File

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

View File

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

View File

@@ -3,9 +3,9 @@
namespace App\Filament\Resources\PackageAddonResource\Pages; namespace App\Filament\Resources\PackageAddonResource\Pages;
use App\Filament\Resources\PackageAddonResource; 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; protected static string $resource = PackageAddonResource::class;
} }

View File

@@ -3,9 +3,9 @@
namespace App\Filament\Resources\PackageAddonResource\Pages; namespace App\Filament\Resources\PackageAddonResource\Pages;
use App\Filament\Resources\PackageAddonResource; 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; protected static string $resource = PackageAddonResource::class;

View File

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

View File

@@ -3,9 +3,9 @@
namespace App\Filament\Resources\PackageResource\Pages; namespace App\Filament\Resources\PackageResource\Pages;
use App\Filament\Resources\PackageResource; 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; protected static string $resource = PackageResource::class;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,10 +2,10 @@
namespace App\Filament\Resources\PhotoboothSettings\Pages; namespace App\Filament\Resources\PhotoboothSettings\Pages;
use App\Filament\Resources\Pages\AuditedEditRecord;
use App\Filament\Resources\PhotoboothSettings\PhotoboothSettingResource; 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; protected static string $resource = PhotoboothSettingResource::class;

View File

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

View File

@@ -7,6 +7,7 @@ use App\Filament\Resources\PurchaseResource\Pages;
use App\Models\PackagePurchase; use App\Models\PackagePurchase;
use App\Notifications\Customer\RefundReceipt; use App\Notifications\Customer\RefundReceipt;
use App\Notifications\Ops\RefundProcessed; use App\Notifications\Ops\RefundProcessed;
use App\Services\Audit\SuperAdminAuditLogger;
use App\Services\Paddle\PaddleTransactionService; use App\Services\Paddle\PaddleTransactionService;
use BackedEnum; use BackedEnum;
use Filament\Actions\Action; use Filament\Actions\Action;
@@ -27,6 +28,7 @@ use Filament\Tables\Filters\Filter;
use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification; use Illuminate\Support\Facades\Notification;
@@ -178,7 +180,13 @@ class PurchaseResource extends Resource
]) ])
->actions([ ->actions([
ViewAction::make(), 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') Action::make('refund')
->label('Refund') ->label('Refund')
->color('danger') ->color('danger')
@@ -234,11 +242,29 @@ class PurchaseResource extends Resource
if ($opsEmail) { if ($opsEmail) {
Notification::route('mail', $opsEmail)->notify(new RefundProcessed($record, $refundSuccess, $reason, $errorMessage)); 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([ ->bulkActions([
BulkActionGroup::make([ 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') ->emptyStateHeading('No Purchases Found')

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@
namespace App\Filament\Resources\TaskResource\Pages; namespace App\Filament\Resources\TaskResource\Pages;
use App\Filament\Resources\TaskResource; use App\Filament\Resources\TaskResource;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions; use Filament\Actions;
use Filament\Resources\Pages\ManageRecords; use Filament\Resources\Pages\ManageRecords;
@@ -13,7 +14,13 @@ class ManageTasks extends ManageRecords
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ 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') Actions\Action::make('import')
->label(__('admin.common.import_csv')) ->label(__('admin.common.import_csv'))
->icon('heroicon-o-arrow-up-tray') ->icon('heroicon-o-arrow-up-tray')

View File

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

View File

@@ -3,6 +3,7 @@
namespace App\Filament\Resources\TenantFeedbackResource\Tables; namespace App\Filament\Resources\TenantFeedbackResource\Tables;
use App\Models\TenantFeedback; use App\Models\TenantFeedback;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Actions\BulkAction; use Filament\Actions\BulkAction;
use Filament\Actions\BulkActionGroup; use Filament\Actions\BulkActionGroup;
@@ -182,11 +183,23 @@ class TenantFeedbackTable
'moderated_at' => now(), 'moderated_at' => now(),
'moderated_by' => Filament::auth()->id(), '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 private static function applyModerationToRecords(Collection $records, string $status, ?string $notes): int
{ {
return TenantFeedback::query() $updated = TenantFeedback::query()
->whereIn('id', $records->pluck('id')) ->whereIn('id', $records->pluck('id'))
->update([ ->update([
'status' => $status, 'status' => $status,
@@ -194,6 +207,24 @@ class TenantFeedbackTable
'moderated_at' => now(), 'moderated_at' => now(),
'moderated_by' => Filament::auth()->id(), '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 private static function statusLabels(): array

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@
namespace App\Filament\Resources\TenantResource\Pages; namespace App\Filament\Resources\TenantResource\Pages;
use App\Filament\Resources\TenantResource; use App\Filament\Resources\TenantResource;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions; use Filament\Actions;
use Filament\Resources\Pages\ViewRecord; use Filament\Resources\Pages\ViewRecord;
@@ -23,7 +24,13 @@ class ViewTenant extends ViewRecord
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ 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(), ...TenantResource::lifecycleActions(),
]; ];
} }

View File

@@ -4,6 +4,7 @@ namespace App\Filament\Resources\TenantResource\Pages;
use App\Filament\Resources\TenantResource; use App\Filament\Resources\TenantResource;
use App\Filament\Resources\TenantResource\Schemas\TenantLifecycleInfolist; use App\Filament\Resources\TenantResource\Schemas\TenantLifecycleInfolist;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions; use Filament\Actions;
use Filament\Resources\Pages\ViewRecord; use Filament\Resources\Pages\ViewRecord;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
@@ -25,7 +26,13 @@ class ViewTenantLifecycle extends ViewRecord
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ 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()) Actions\ActionGroup::make(TenantResource::lifecycleActions())
->label(__('admin.tenants.actions.lifecycle')) ->label(__('admin.tenants.actions.lifecycle'))
->icon('heroicon-o-shield-exclamation'), ->icon('heroicon-o-shield-exclamation'),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ namespace App\Filament\SuperAdmin\Pages;
use App\Filament\Clusters\RareAdmin\RareAdminCluster; use App\Filament\Clusters\RareAdmin\RareAdminCluster;
use App\Models\GuestPolicySetting; use App\Models\GuestPolicySetting;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Forms; use Filament\Forms;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Pages\Page; use Filament\Pages\Page;
@@ -163,6 +164,26 @@ class GuestPolicySettingsPage extends Page
$settings->guest_notification_ttl_hours = $this->guest_notification_ttl_hours; $settings->guest_notification_ttl_hours = $this->guest_notification_ttl_hours;
$settings->save(); $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() Notification::make()
->title(__('admin.guest_policy.notifications.saved')) ->title(__('admin.guest_policy.notifications.saved'))
->success() ->success()

View File

@@ -4,6 +4,7 @@ namespace App\Filament\SuperAdmin\Pages;
use App\Filament\Clusters\RareAdmin\RareAdminCluster; use App\Filament\Clusters\RareAdmin\RareAdminCluster;
use App\Models\WatermarkSetting; use App\Models\WatermarkSetting;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Forms; use Filament\Forms;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Pages\Page; use Filament\Pages\Page;
@@ -128,6 +129,21 @@ class WatermarkSettingsPage extends Page
$settings->offset_y = $this->offset_y; $settings->offset_y = $this->offset_y;
$settings->save(); $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() Notification::make()
->title('Wasserzeichen aktualisiert') ->title('Wasserzeichen aktualisiert')
->success() ->success()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ namespace Tests\Feature;
use App\Filament\Clusters\DailyOps\Resources\Photos\Pages\ListPhotos; use App\Filament\Clusters\DailyOps\Resources\Photos\Pages\ListPhotos;
use App\Models\Event; use App\Models\Event;
use App\Models\Photo; use App\Models\Photo;
use App\Models\SuperAdminActionLog;
use App\Models\Tenant; use App\Models\Tenant;
use App\Models\User; use App\Models\User;
use Filament\Actions\Testing\TestAction; use Filament\Actions\Testing\TestAction;
@@ -40,6 +41,10 @@ class PhotoModerationQueueTest extends TestCase
$this->assertSame('Looks good.', $photo->moderation_notes); $this->assertSame('Looks good.', $photo->moderation_notes);
$this->assertNotNull($photo->moderated_at); $this->assertNotNull($photo->moderated_at);
$this->assertSame($user->id, $photo->moderated_by); $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 public function test_superadmin_can_bulk_reject_pending_photos(): void
@@ -74,6 +79,10 @@ class PhotoModerationQueueTest extends TestCase
$this->assertNotNull($photoB->moderated_at); $this->assertNotNull($photoB->moderated_at);
$this->assertSame($user->id, $photoA->moderated_by); $this->assertSame($user->id, $photoA->moderated_by);
$this->assertSame($user->id, $photoB->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 private function bootSuperAdminPanel(User $user): void

View File

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

View File

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

View File

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