feat: add impersonation log viewer in filament dashboard
This commit is contained in:
383
app/Filament/Pages/ImpersonationLogViewer.php
Normal file
383
app/Filament/Pages/ImpersonationLogViewer.php
Normal file
@@ -0,0 +1,383 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use App\Enum\UserLevel;
|
||||
use App\Models\ImpersonationLog;
|
||||
use App\Models\User;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\BulkAction;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\Concerns\InteractsWithActions;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Concerns\InteractsWithTable;
|
||||
use Filament\Tables\Contracts\HasTable;
|
||||
use Filament\Tables\Filters\Filter;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Str;
|
||||
use UnitEnum;
|
||||
|
||||
class ImpersonationLogViewer extends Page implements HasForms, HasTable
|
||||
{
|
||||
use InteractsWithActions;
|
||||
use InteractsWithForms;
|
||||
use InteractsWithTable;
|
||||
|
||||
protected static string|null|\BackedEnum $navigationIcon = 'heroicon-o-eye';
|
||||
|
||||
protected static string|UnitEnum|null $navigationGroup = 'Settings';
|
||||
|
||||
protected static ?int $navigationSort = 10;
|
||||
|
||||
protected string $view = 'filament.pages.impersonation-log-viewer';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorizeAccess();
|
||||
}
|
||||
|
||||
protected function authorizeAccess(): void
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if (! $user || ! $user->isSuperAdmin()) {
|
||||
abort(403, 'Access denied. Only Super Admin can view impersonation logs.');
|
||||
}
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'Impersonation Logs';
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('export_csv')
|
||||
->label('Export CSV')
|
||||
->icon('heroicon-o-arrow-down-tray')
|
||||
->color('success')
|
||||
->action(fn () => $this->exportCsv()),
|
||||
|
||||
Action::make('refresh')
|
||||
->label('Refresh')
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->color('gray')
|
||||
->action(fn () => $this->resetTable()),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderWidgets(): array
|
||||
{
|
||||
return [
|
||||
\App\Filament\Widgets\ImpersonationStatsOverview::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->query(
|
||||
ImpersonationLog::query()
|
||||
->with(['admin', 'targetUser'])
|
||||
->latest('start_time')
|
||||
)
|
||||
->columns([
|
||||
TextColumn::make('id')
|
||||
->label('ID')
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
|
||||
TextColumn::make('admin.name')
|
||||
->label('Admin')
|
||||
->description(fn (ImpersonationLog $record): string => $record->admin?->email ?? 'Unknown')
|
||||
->sortable()
|
||||
->searchable()
|
||||
->weight('medium'),
|
||||
|
||||
TextColumn::make('targetUser.name')
|
||||
->label('Target User')
|
||||
->description(fn (ImpersonationLog $record): string => $record->targetUser?->email ?? 'Unknown')
|
||||
->sortable()
|
||||
->searchable()
|
||||
->weight('medium'),
|
||||
|
||||
TextColumn::make('start_time')
|
||||
->label('Started')
|
||||
->dateTime('M j, Y g:i A')
|
||||
->sortable()
|
||||
->description(fn (ImpersonationLog $record): string => $record->start_time->diffForHumans()),
|
||||
|
||||
TextColumn::make('end_time')
|
||||
->label('Ended')
|
||||
->dateTime('M j, Y g:i A')
|
||||
->sortable()
|
||||
->placeholder('Active session')
|
||||
->description(fn (ImpersonationLog $record): string => $record->end_time ? $record->end_time->diffForHumans() : 'Still active'
|
||||
),
|
||||
|
||||
TextColumn::make('duration_in_minutes')
|
||||
->label('Duration')
|
||||
->formatStateUsing(function ($record) {
|
||||
return match(true) {
|
||||
!$record->duration_in_minutes => 'Active',
|
||||
$record->duration_in_minutes < 60 => "{$record->duration_in_minutes}m",
|
||||
default => round($record->duration_in_minutes / 60, 1) . 'h',
|
||||
};
|
||||
})
|
||||
->sortable()
|
||||
->alignCenter(),
|
||||
|
||||
TextColumn::make('status')
|
||||
->label('Status')
|
||||
->formatStateUsing(function ($record) {
|
||||
return match ($record->status) {
|
||||
'active' => 'Active',
|
||||
'completed' => 'Completed',
|
||||
'force_terminated' => 'Terminated',
|
||||
'expired' => 'Expired',
|
||||
default => 'Unknown',
|
||||
};
|
||||
})
|
||||
->badge()
|
||||
->color(fn ($record) => match ($record->status) {
|
||||
'active' => 'success',
|
||||
'completed' => 'primary',
|
||||
'force_terminated' => 'danger',
|
||||
'expired' => 'warning',
|
||||
default => 'gray',
|
||||
}),
|
||||
|
||||
TextColumn::make('ip_address')
|
||||
->label('IP Address')
|
||||
->searchable()
|
||||
->toggleable(isToggledHiddenByDefault: true)
|
||||
->copyable()
|
||||
->fontFamily('mono'),
|
||||
|
||||
TextColumn::make('pages_visited_count')
|
||||
->label('Pages')
|
||||
->formatStateUsing(fn (ImpersonationLog $record): int => is_array($record->pages_visited) ? count($record->pages_visited) : 0
|
||||
)
|
||||
->alignCenter()
|
||||
->toggleable(),
|
||||
|
||||
TextColumn::make('actions_taken_count')
|
||||
->label('Actions')
|
||||
->formatStateUsing(fn (ImpersonationLog $record): int => is_array($record->actions_taken) ? count($record->actions_taken) : 0
|
||||
)
|
||||
->alignCenter()
|
||||
->toggleable(),
|
||||
])
|
||||
->defaultSort('start_time', 'desc')
|
||||
->filters([
|
||||
SelectFilter::make('status')
|
||||
->label('Status')
|
||||
->options([
|
||||
'active' => 'Active',
|
||||
'completed' => 'Completed',
|
||||
'force_terminated' => 'Force Terminated',
|
||||
'expired' => 'Expired',
|
||||
]),
|
||||
|
||||
SelectFilter::make('admin_id')
|
||||
->label('Admin')
|
||||
->searchable()
|
||||
->getSearchResultsUsing(function (string $search): array {
|
||||
return User::where('level', UserLevel::SUPERADMIN->value)
|
||||
->where('name', 'like', "%{$search}%")
|
||||
->orWhere('email', 'like', "%{$search}%")
|
||||
->limit(50)
|
||||
->pluck('name', 'id')
|
||||
->toArray();
|
||||
})
|
||||
->getOptionLabelUsing(function ($value): string {
|
||||
return User::find($value)?->name ?? 'Unknown';
|
||||
}),
|
||||
|
||||
SelectFilter::make('target_user_id')
|
||||
->label('Target User')
|
||||
->searchable()
|
||||
->getSearchResultsUsing(function (string $search): array {
|
||||
return User::where('name', 'like', "%{$search}%")
|
||||
->orWhere('email', 'like', "%{$search}%")
|
||||
->limit(50)
|
||||
->pluck('name', 'id')
|
||||
->toArray();
|
||||
})
|
||||
->getOptionLabelUsing(function ($value): string {
|
||||
return User::find($value)?->name ?? 'Unknown';
|
||||
}),
|
||||
|
||||
Filter::make('date_range')
|
||||
->schema([
|
||||
DatePicker::make('start_date')
|
||||
->label('Start Date')
|
||||
->required(),
|
||||
DatePicker::make('end_date')
|
||||
->label('End Date')
|
||||
->required(),
|
||||
])
|
||||
->query(function (Builder $query, array $data): Builder {
|
||||
return $query
|
||||
->when(
|
||||
$data['start_date'],
|
||||
fn (Builder $query, $date): Builder => $query->whereDate('start_time', '>=', $date)
|
||||
)
|
||||
->when(
|
||||
$data['end_date'],
|
||||
fn (Builder $query, $date): Builder => $query->whereDate('start_time', '<=', $date)
|
||||
);
|
||||
}),
|
||||
|
||||
Filter::make('ip_address')
|
||||
->schema([
|
||||
TextInput::make('ip')
|
||||
->label('IP Address')
|
||||
->placeholder('192.168.1.1')
|
||||
->required(),
|
||||
])
|
||||
->query(function (Builder $query, array $data): Builder {
|
||||
return $query->when(
|
||||
$data['ip'],
|
||||
fn (Builder $query, $ip): Builder => $query->where('ip_address', 'like', "%{$ip}%")
|
||||
);
|
||||
}),
|
||||
])
|
||||
->recordActions([
|
||||
Action::make('view_details')
|
||||
->label('View Details')
|
||||
->icon('heroicon-o-eye')
|
||||
->color('primary')
|
||||
->slideOver()
|
||||
->modalContent(fn (ImpersonationLog $record): View => view(
|
||||
'filament.modals.impersonation-log-details-slideover',
|
||||
['record' => $record]
|
||||
))
|
||||
->modalHeading('Impersonation Session Details')
|
||||
->action(fn () => null),
|
||||
|
||||
Action::make('terminate')
|
||||
->label('Terminate')
|
||||
->icon('heroicon-o-x-circle')
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Terminate Impersonation Session')
|
||||
->modalDescription('This will immediately terminate the active impersonation session.')
|
||||
->modalSubmitActionLabel('Terminate Session')
|
||||
->visible(fn (ImpersonationLog $record): bool => $record->isActive())
|
||||
->action(function (ImpersonationLog $record) {
|
||||
$record->update([
|
||||
'end_time' => now(),
|
||||
'status' => 'force_terminated',
|
||||
]);
|
||||
}),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
BulkAction::make('terminate_active')
|
||||
->label('Terminate Active Sessions')
|
||||
->icon('heroicon-o-x-circle')
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->deselectRecordsAfterCompletion()
|
||||
->action(function (Collection $records) {
|
||||
$records->filter(fn (ImpersonationLog $record) => $record->isActive())
|
||||
->each(function (ImpersonationLog $record) {
|
||||
$record->update([
|
||||
'end_time' => now(),
|
||||
'status' => 'force_terminated',
|
||||
]);
|
||||
});
|
||||
}),
|
||||
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
])
|
||||
->emptyStateHeading('No impersonation logs found')
|
||||
->emptyStateDescription('No impersonation sessions have been recorded yet.')
|
||||
->emptyStateActions([
|
||||
Action::make('refresh')
|
||||
->label('Refresh')
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->action(fn () => $this->resetTable()),
|
||||
]);
|
||||
}
|
||||
|
||||
public function exportCsv()
|
||||
{
|
||||
$logs = ImpersonationLog::query()
|
||||
->with(['admin', 'targetUser'])
|
||||
->latest('start_time')
|
||||
->get();
|
||||
|
||||
$filename = 'impersonation_logs_' . now()->format('Y_m_d_H_i_s') . '.csv';
|
||||
|
||||
// Create a temporary file
|
||||
$handle = fopen('php://temp', 'r+');
|
||||
|
||||
// Add BOM for Excel UTF-8 support
|
||||
fwrite($handle, "\xEF\xBB\xBF");
|
||||
|
||||
// Write headers
|
||||
$headers = [
|
||||
'ID', 'Admin Name', 'Admin Email', 'Target Name', 'Target Email',
|
||||
'Start Time', 'End Time', 'Duration (minutes)', 'Status', 'IP Address',
|
||||
'User Agent', 'Pages Visited', 'Actions Taken',
|
||||
];
|
||||
fputcsv($handle, $headers);
|
||||
|
||||
// Write data rows
|
||||
foreach ($logs as $log) {
|
||||
fputcsv($handle, [
|
||||
$log->id,
|
||||
$log->admin?->name ?? 'Unknown',
|
||||
$log->admin?->email ?? 'Unknown',
|
||||
$log->targetUser?->name ?? 'Unknown',
|
||||
$log->targetUser?->email ?? 'Unknown',
|
||||
$log->start_time->toDateTimeString(),
|
||||
$log->end_time?->toDateTimeString() ?? 'Active',
|
||||
$log->duration_in_minutes ?? 'Active',
|
||||
$log->status,
|
||||
$log->ip_address,
|
||||
$log->user_agent,
|
||||
is_array($log->pages_visited) ? json_encode($log->pages_visited) : '[]',
|
||||
is_array($log->actions_taken) ? json_encode($log->actions_taken) : '[]',
|
||||
]);
|
||||
}
|
||||
|
||||
// Rewind the file pointer
|
||||
rewind($handle);
|
||||
|
||||
// Get the CSV content
|
||||
$csvContent = stream_get_contents($handle);
|
||||
|
||||
// Close the file handle
|
||||
fclose($handle);
|
||||
|
||||
// Return a download response
|
||||
return response()->streamDownload(
|
||||
function () use ($csvContent) {
|
||||
echo $csvContent;
|
||||
},
|
||||
$filename,
|
||||
[
|
||||
'Content-Type' => 'text/csv',
|
||||
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user