- Add highly optimized Dockerfile with Nginx and PHP-FPM 8.4 - Add docker-compose.yml configured with Redis and MariaDB 10.11 - Implement entrypoint.sh and supervisord.conf for background workers - Refactor legacy IMAP scripts into scheduled Artisan Commands - Secure app by removing old routes with hardcoded basic auth credentials - Configure email attachments to use Laravel Storage instead of insecure public/tmp
383 lines
15 KiB
PHP
383 lines
15 KiB
PHP
<?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 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.'"',
|
|
]
|
|
);
|
|
}
|
|
}
|