feat: add user impersonation service
This commit is contained in:
@@ -9,7 +9,9 @@ use App\Filament\Resources\UserResource\Pages\ListUsers;
|
||||
use App\Filament\Resources\UserResource\RelationManagers\LogsRelationManager;
|
||||
use App\Filament\Resources\UserResource\RelationManagers\UsageLogsRelationManager;
|
||||
use App\Models\User;
|
||||
use App\Services\ImpersonationService;
|
||||
use BackedEnum;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\BulkAction;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
@@ -49,6 +51,12 @@ class UserResource extends Resource
|
||||
->email()
|
||||
->required()
|
||||
->maxLength(255),
|
||||
TextInput::make('password')
|
||||
->password()
|
||||
->required()
|
||||
->minLength(6)
|
||||
->maxLength(255)
|
||||
->visibleOn('create'),
|
||||
TextInput::make('email_verified_at')
|
||||
->label('Email Verification Status')
|
||||
->disabled()
|
||||
@@ -149,8 +157,65 @@ class UserResource extends Resource
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
Action::make('impersonate')
|
||||
->label('Login as User')
|
||||
->icon('heroicon-o-arrow-right-on-rectangle')
|
||||
->color('warning')
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Start User Impersonation')
|
||||
->modalDescription('This will open a new tab where you will be logged in as this user. All actions will be logged for security. Your admin panel will remain open in this tab.')
|
||||
->modalSubmitActionLabel('Start Impersonation')
|
||||
->modalCancelActionLabel('Cancel')
|
||||
->visible(fn (User $record): bool => auth()->user()?->isSuperAdmin() &&
|
||||
$record->isNormalUser() &&
|
||||
! app(ImpersonationService::class)->isImpersonating()
|
||||
)
|
||||
->url(function (User $record): string {
|
||||
$admin = auth()->user();
|
||||
$impersonationService = app(ImpersonationService::class);
|
||||
|
||||
if (! $impersonationService->canImpersonate($admin, $record)) {
|
||||
Notification::make()
|
||||
->title('Impersonation Failed')
|
||||
->body('Unable to start impersonation. Please check permissions and try again.')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return '#';
|
||||
}
|
||||
|
||||
// Return impersonation URL
|
||||
return route('impersonation.start', $record);
|
||||
})
|
||||
->openUrlInNewTab(),
|
||||
])
|
||||
->toolbarActions([
|
||||
Action::make('stopImpersonation')
|
||||
->label('Stop Impersonating')
|
||||
->icon('heroicon-o-arrow-left-on-rectangle')
|
||||
->color('danger')
|
||||
->visible(fn (): bool => app(ImpersonationService::class)->isImpersonating())
|
||||
->action(function () {
|
||||
$impersonationService = app(ImpersonationService::class);
|
||||
|
||||
if (! $impersonationService->stopImpersonation(request())) {
|
||||
Notification::make()
|
||||
->title('Failed to Stop Impersonation')
|
||||
->body('Unable to stop impersonation session.')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title('Impersonation Ended')
|
||||
->body('You have been returned to your admin account.')
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return redirect()->to(\Filament\Pages\Dashboard::getUrl());
|
||||
}),
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
BulkAction::make('updateLevel')
|
||||
|
||||
@@ -6,7 +6,9 @@ use App\Filament\Resources\UserResource;
|
||||
use App\Models\User;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Illuminate\Support\Facades\Response;
|
||||
|
||||
class EditUser extends EditRecord
|
||||
@@ -17,6 +19,25 @@ class EditUser extends EditRecord
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
Action::make('mark_email_verified')
|
||||
->label('Mark Email as Verified')
|
||||
->icon(Heroicon::OutlinedEnvelope)
|
||||
->action(function (User $user) {
|
||||
if (! $user->hasVerifiedEmail()) {
|
||||
$user->markEmailAsVerified();
|
||||
Notification::make('email_verified_successfully')
|
||||
->title('Email Verified Successfully')
|
||||
->icon(Heroicon::OutlinedEnvelope)
|
||||
->success()
|
||||
->send();
|
||||
} else {
|
||||
Notification::make('email_already_verified')
|
||||
->title('Email Already Verified')
|
||||
->icon(Heroicon::OutlinedEnvelope)
|
||||
->warning()
|
||||
->send();
|
||||
}
|
||||
}),
|
||||
Action::make('download_report')
|
||||
->label('Download User Report')
|
||||
->icon('heroicon-o-user')
|
||||
|
||||
115
app/Http/Controllers/ImpersonationController.php
Normal file
115
app/Http/Controllers/ImpersonationController.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\ImpersonationService;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
final class ImpersonationController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ImpersonationService $impersonationService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Start impersonating a user.
|
||||
*/
|
||||
public function start(Request $request, User $user)
|
||||
{
|
||||
$admin = Auth::user();
|
||||
|
||||
if (! $admin || ! $admin->isSuperAdmin()) {
|
||||
Notification::make()
|
||||
->title('Access Denied')
|
||||
->body('Only SuperAdmin users can impersonate other users.')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
if (! $this->impersonationService->startImpersonation($admin, $user, $request)) {
|
||||
Notification::make()
|
||||
->title('Impersonation Failed')
|
||||
->body('Unable to start impersonation. Please check permissions and try again.')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title('Impersonation Started')
|
||||
->body("You are now logged in as {$user->name}.")
|
||||
->success()
|
||||
->send();
|
||||
|
||||
// Redirect to user dashboard (assuming you have a dashboard route)
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop impersonating the current user.
|
||||
*/
|
||||
public function stop(Request $request)
|
||||
{
|
||||
if (! $this->impersonationService->isImpersonating()) {
|
||||
return redirect()->to(\Filament\Pages\Dashboard::getUrl());
|
||||
}
|
||||
|
||||
$impersonator = $this->impersonationService->getCurrentImpersonator();
|
||||
|
||||
if (! $this->impersonationService->stopImpersonation($request)) {
|
||||
Notification::make()
|
||||
->title('Failed to Stop Impersonation')
|
||||
->body('Unable to stop impersonation session.')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title('Impersonation Ended')
|
||||
->body('You have been returned to your admin account.')
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return redirect()->to(\Filament\Pages\Dashboard::getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Show impersonation status (AJAX endpoint).
|
||||
*/
|
||||
public function status(Request $request)
|
||||
{
|
||||
if (! $this->impersonationService->isImpersonating()) {
|
||||
return response()->json([
|
||||
'is_impersonating' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
$impersonator = $this->impersonationService->getCurrentImpersonator();
|
||||
$target = Auth::user();
|
||||
$remainingMinutes = $this->impersonationService->getRemainingMinutes();
|
||||
|
||||
return response()->json([
|
||||
'is_impersonating' => true,
|
||||
'impersonator' => [
|
||||
'name' => $impersonator?->name,
|
||||
'email' => $impersonator?->email,
|
||||
],
|
||||
'target' => [
|
||||
'name' => $target?->name,
|
||||
'email' => $target?->email,
|
||||
],
|
||||
'remaining_minutes' => $remainingMinutes,
|
||||
'expires_soon' => $remainingMinutes <= 5,
|
||||
]);
|
||||
}
|
||||
}
|
||||
52
app/Http/Middleware/ImpersonationMiddleware.php
Normal file
52
app/Http/Middleware/ImpersonationMiddleware.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Services\ImpersonationService;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class ImpersonationMiddleware
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ImpersonationService $impersonationService
|
||||
) {}
|
||||
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
// Check if impersonation has expired
|
||||
if ($this->impersonationService->isImpersonating() &&
|
||||
$this->impersonationService->isImpersonationExpired()) {
|
||||
|
||||
$this->impersonationService->stopImpersonation($request);
|
||||
|
||||
// Redirect to admin dashboard with expired message
|
||||
return redirect()->to(\Filament\Pages\Dashboard::getUrl())->with('impersonation_expired', 'Your impersonation session has expired.');
|
||||
}
|
||||
|
||||
// Share impersonation data with all views
|
||||
if ($this->impersonationService->isImpersonating()) {
|
||||
$impersonator = $this->impersonationService->getCurrentImpersonator();
|
||||
$remainingMinutes = $this->impersonationService->getRemainingMinutes();
|
||||
$startTime = $this->impersonationService->getImpersonationStartTime();
|
||||
|
||||
view()->share('isImpersonating', true);
|
||||
view()->share('impersonator', $impersonator);
|
||||
view()->share('impersonationTarget', Auth::user());
|
||||
view()->share('impersonationRemainingMinutes', $remainingMinutes);
|
||||
view()->share('impersonationStartTime', $startTime);
|
||||
} else {
|
||||
view()->share('isImpersonating', false);
|
||||
view()->share('impersonator', null);
|
||||
view()->share('impersonationTarget', null);
|
||||
view()->share('impersonationRemainingMinutes', 0);
|
||||
view()->share('impersonationStartTime', null);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
132
app/Models/ImpersonationLog.php
Normal file
132
app/Models/ImpersonationLog.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
final class ImpersonationLog extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'admin_id',
|
||||
'target_user_id',
|
||||
'start_time',
|
||||
'end_time',
|
||||
'ip_address',
|
||||
'user_agent',
|
||||
'status',
|
||||
'pages_visited',
|
||||
'actions_taken',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'start_time' => 'datetime',
|
||||
'end_time' => 'datetime',
|
||||
'pages_visited' => 'array',
|
||||
'actions_taken' => 'array',
|
||||
];
|
||||
|
||||
public function admin(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'admin_id');
|
||||
}
|
||||
|
||||
public function targetUser(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'target_user_id');
|
||||
}
|
||||
|
||||
public function getDurationInSecondsAttribute(): ?int
|
||||
{
|
||||
if (! $this->end_time) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->start_time->diffInSeconds($this->end_time);
|
||||
}
|
||||
|
||||
public function getDurationInMinutesAttribute(): ?int
|
||||
{
|
||||
$seconds = $this->duration_in_seconds;
|
||||
|
||||
return $seconds ? (int) ceil($seconds / 60) : null;
|
||||
}
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->status === 'active' && is_null($this->end_time);
|
||||
}
|
||||
|
||||
public function isCompleted(): bool
|
||||
{
|
||||
return $this->status === 'completed' && ! is_null($this->end_time);
|
||||
}
|
||||
|
||||
public function isForceTerminated(): bool
|
||||
{
|
||||
return $this->status === 'force_terminated';
|
||||
}
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('status', 'active')->whereNull('end_time');
|
||||
}
|
||||
|
||||
public function scopeCompleted($query)
|
||||
{
|
||||
return $query->where('status', 'completed');
|
||||
}
|
||||
|
||||
public function scopeForAdmin($query, User $admin)
|
||||
{
|
||||
return $query->where('admin_id', $admin->id);
|
||||
}
|
||||
|
||||
public function scopeForTarget($query, User $target)
|
||||
{
|
||||
return $query->where('target_user_id', $target->id);
|
||||
}
|
||||
|
||||
public function scopeToday($query)
|
||||
{
|
||||
return $query->whereDate('start_time', today());
|
||||
}
|
||||
|
||||
public function scopeThisWeek($query)
|
||||
{
|
||||
return $query->whereBetween('start_time', [now()->startOfWeek(), now()->endOfWeek()]);
|
||||
}
|
||||
|
||||
public function scopeThisMonth($query)
|
||||
{
|
||||
return $query->whereBetween('start_time', [now()->startOfMonth(), now()->endOfMonth()]);
|
||||
}
|
||||
|
||||
public function addPageVisited(string $page): void
|
||||
{
|
||||
$pages = $this->pages_visited ?? [];
|
||||
$pages[] = [
|
||||
'page' => $page,
|
||||
'timestamp' => now()->toISOString(),
|
||||
];
|
||||
|
||||
$this->update(['pages_visited' => $pages]);
|
||||
}
|
||||
|
||||
public function addActionTaken(string $action, array $data = []): void
|
||||
{
|
||||
$actions = $this->actions_taken ?? [];
|
||||
$actions[] = [
|
||||
'action' => $action,
|
||||
'data' => $data,
|
||||
'timestamp' => now()->toISOString(),
|
||||
];
|
||||
|
||||
$this->update(['actions_taken' => $actions]);
|
||||
}
|
||||
}
|
||||
@@ -139,4 +139,14 @@ class User extends Authenticatable implements FilamentUser, MustVerifyEmail
|
||||
{
|
||||
return $this->hasMany(UsageLog::class);
|
||||
}
|
||||
|
||||
public function impersonationLogs()
|
||||
{
|
||||
return $this->hasMany(ImpersonationLog::class, 'admin_id');
|
||||
}
|
||||
|
||||
public function impersonationTargets()
|
||||
{
|
||||
return $this->hasMany(ImpersonationLog::class, 'target_user_id');
|
||||
}
|
||||
}
|
||||
|
||||
97
app/Policies/ImpersonationPolicy.php
Normal file
97
app/Policies/ImpersonationPolicy.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\enum\UserLevel;
|
||||
use App\Models\User;
|
||||
use App\Services\ImpersonationService;
|
||||
use Illuminate\Auth\Access\Response;
|
||||
|
||||
final class ImpersonationPolicy
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ImpersonationService $impersonationService
|
||||
) {}
|
||||
|
||||
public function start(User $admin, User $target): Response
|
||||
{
|
||||
// Only SUPERADMIN can impersonate
|
||||
if ($admin->user_level !== UserLevel::SUPERADMIN) {
|
||||
return Response::deny('Only SUPERADMIN users can impersonate other users.');
|
||||
}
|
||||
|
||||
// Cannot impersonate other SUPERADMIN users
|
||||
if ($target->user_level === UserLevel::SUPERADMIN) {
|
||||
return Response::deny('Cannot impersonate SUPERADMIN users.');
|
||||
}
|
||||
|
||||
// Cannot impersonate self
|
||||
if ($admin->id === $target->id) {
|
||||
return Response::deny('Cannot impersonate yourself.');
|
||||
}
|
||||
|
||||
// Can only impersonate NORMALUSER
|
||||
if ($target->user_level !== UserLevel::NORMALUSER) {
|
||||
return Response::deny('Can only impersonate NORMALUSER accounts.');
|
||||
}
|
||||
|
||||
// Check if admin is already impersonating someone
|
||||
if ($this->impersonationService->isImpersonating()) {
|
||||
return Response::deny('You are already impersonating another user. Please stop the current impersonation first.');
|
||||
}
|
||||
|
||||
// Rate limiting: Check if admin has too many recent impersonation attempts
|
||||
$recentAttempts = $admin->impersonationLogs()
|
||||
->where('start_time', '>', now()->subHours(1))
|
||||
->count();
|
||||
|
||||
if ($recentAttempts >= 10) {
|
||||
return Response::deny('Too many impersonation attempts. Please try again later.');
|
||||
}
|
||||
|
||||
return Response::allow();
|
||||
}
|
||||
|
||||
public function stop(User $user): Response
|
||||
{
|
||||
// Only users who are currently impersonating can stop impersonation
|
||||
if (! $this->impersonationService->isImpersonating()) {
|
||||
return Response::deny('No active impersonation session found.');
|
||||
}
|
||||
|
||||
// The original admin user must be the one stopping the impersonation
|
||||
$impersonator = $this->impersonationService->getCurrentImpersonator();
|
||||
if (! $impersonator || $impersonator->id !== $user->id) {
|
||||
return Response::deny('You are not authorized to stop this impersonation session.');
|
||||
}
|
||||
|
||||
return Response::allow();
|
||||
}
|
||||
|
||||
public function viewAny(User $user): Response
|
||||
{
|
||||
// Only SUPERADMIN can view impersonation logs
|
||||
if ($user->user_level !== UserLevel::SUPERADMIN) {
|
||||
return Response::deny('Only SUPERADMIN users can view impersonation logs.');
|
||||
}
|
||||
|
||||
return Response::allow();
|
||||
}
|
||||
|
||||
public function view(User $user, $impersonationLog): Response
|
||||
{
|
||||
// Only SUPERADMIN can view specific impersonation logs
|
||||
if ($user->user_level !== UserLevel::SUPERADMIN) {
|
||||
return Response::deny('Only SUPERADMIN users can view impersonation logs.');
|
||||
}
|
||||
|
||||
// Users can only view logs where they are the admin
|
||||
if ($impersonationLog->admin_id !== $user->id) {
|
||||
return Response::deny('You can only view your own impersonation logs.');
|
||||
}
|
||||
|
||||
return Response::allow();
|
||||
}
|
||||
}
|
||||
197
app/Services/ImpersonationService.php
Normal file
197
app/Services/ImpersonationService.php
Normal file
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\enum\UserLevel;
|
||||
use App\Models\ImpersonationLog;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
|
||||
final class ImpersonationService
|
||||
{
|
||||
private const IMPERSONATION_SESSION_KEY = 'impersonation';
|
||||
|
||||
private const IMPERSONATION_START_TIME_KEY = 'impersonation_start_time';
|
||||
|
||||
private const ORIGINAL_USER_KEY = 'original_admin_user_id';
|
||||
|
||||
private const IMPERSONATION_TIMEOUT = 30; // minutes
|
||||
|
||||
public function canImpersonate(User $admin, User $target): bool
|
||||
{
|
||||
// Only SUPERADMIN can impersonate
|
||||
if ($admin->level !== UserLevel::SUPERADMIN) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot impersonate other SUPERADMIN users
|
||||
if ($target->level === UserLevel::SUPERADMIN) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot impersonate self
|
||||
if ($admin->id === $target->id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Can only impersonate NORMALUSER
|
||||
if ($target->level !== UserLevel::NORMALUSER) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if admin is already impersonating someone
|
||||
if ($this->isImpersonating()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isImpersonating(): bool
|
||||
{
|
||||
return Session::has(self::IMPERSONATION_SESSION_KEY) &&
|
||||
Session::has(self::ORIGINAL_USER_KEY);
|
||||
}
|
||||
|
||||
public function getCurrentImpersonator(): ?User
|
||||
{
|
||||
if (! $this->isImpersonating()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$originalUserId = Session::get(self::ORIGINAL_USER_KEY);
|
||||
|
||||
return User::find($originalUserId);
|
||||
}
|
||||
|
||||
public function getImpersonationStartTime(): ?Carbon
|
||||
{
|
||||
if (! $this->isImpersonating()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Session::get(self::IMPERSONATION_START_TIME_KEY);
|
||||
}
|
||||
|
||||
public function isImpersonationExpired(): bool
|
||||
{
|
||||
if (! $this->isImpersonating()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$startTime = $this->getImpersonationStartTime();
|
||||
|
||||
if (! $startTime) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $startTime->diffInMinutes(now()) > self::IMPERSONATION_TIMEOUT;
|
||||
}
|
||||
|
||||
public function startImpersonation(User $admin, User $target, Request $request): bool
|
||||
{
|
||||
if (! $this->canImpersonate($admin, $target)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Store original admin user info
|
||||
Session::put(self::ORIGINAL_USER_KEY, $admin->id);
|
||||
Session::put(self::IMPERSONATION_START_TIME_KEY, now());
|
||||
|
||||
// Log the impersonation start
|
||||
$impersonationLog = ImpersonationLog::create([
|
||||
'admin_id' => $admin->id,
|
||||
'target_user_id' => $target->id,
|
||||
'start_time' => now(),
|
||||
'ip_address' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
'status' => 'active',
|
||||
]);
|
||||
|
||||
Session::put(self::IMPERSONATION_SESSION_KEY, $impersonationLog->id);
|
||||
|
||||
// Log out the admin and log in as target user
|
||||
Auth::logout();
|
||||
Auth::login($target);
|
||||
|
||||
// Regenerate session for security
|
||||
Session::regenerate(true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function stopImpersonation(Request $request): bool
|
||||
{
|
||||
if (! $this->isImpersonating()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$admin = $this->getCurrentImpersonator();
|
||||
$target = Auth::user();
|
||||
$impersonationLogId = Session::get(self::IMPERSONATION_SESSION_KEY);
|
||||
|
||||
// Update the impersonation log
|
||||
if ($impersonationLogId) {
|
||||
$log = ImpersonationLog::find($impersonationLogId);
|
||||
if ($log) {
|
||||
$log->update([
|
||||
'end_time' => now(),
|
||||
'status' => 'completed',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear impersonation session data
|
||||
Session::forget([
|
||||
self::IMPERSONATION_SESSION_KEY,
|
||||
self::IMPERSONATION_START_TIME_KEY,
|
||||
self::ORIGINAL_USER_KEY,
|
||||
]);
|
||||
|
||||
// Log out target user and log back in as admin
|
||||
Auth::logout();
|
||||
|
||||
if ($admin) {
|
||||
Auth::login($admin);
|
||||
}
|
||||
|
||||
// Regenerate session for security
|
||||
Session::regenerate(true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function forceStopAllImpersonations(): void
|
||||
{
|
||||
// Force end any active impersonation sessions in the database
|
||||
ImpersonationLog::where('status', 'active')
|
||||
->whereNull('end_time')
|
||||
->update([
|
||||
'end_time' => now(),
|
||||
'status' => 'force_terminated',
|
||||
]);
|
||||
}
|
||||
|
||||
public function getRemainingMinutes(): int
|
||||
{
|
||||
if (! $this->isImpersonating()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$startTime = $this->getImpersonationStartTime();
|
||||
|
||||
if (! $startTime) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$elapsed = $startTime->diffInMinutes(now());
|
||||
$remaining = self::IMPERSONATION_TIMEOUT - $elapsed;
|
||||
|
||||
return (int) max(0, $remaining);
|
||||
}
|
||||
}
|
||||
26
app/View/Components/ImpersonationBanner.php
Normal file
26
app/View/Components/ImpersonationBanner.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class ImpersonationBanner extends Component
|
||||
{
|
||||
/**
|
||||
* Create a new component instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the view / contents that represent the component.
|
||||
*/
|
||||
public function render(): View|Closure|string
|
||||
{
|
||||
return view('components.impersonation-banner');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user