feat: add user impersonation service

This commit is contained in:
idevakk
2025-11-17 10:44:19 -08:00
parent f60c986e07
commit a7029b5f57
21 changed files with 1343 additions and 6 deletions

View 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);
}
}