Files
crewli/api/app/Services/ImpersonationService.php
bert.hausmans ddf26dad33 feat: platform admin backend — controllers, services, routes, tests
Add cross-organisation admin API endpoints behind role:super_admin middleware:
- AdminOrganisationController: CRUD with search, filter, billing_status management
- AdminUserController: user management with role assignment across orgs
- AdminStatsController: platform-wide aggregate statistics
- AdminActivityLogController: filterable activity log viewer
- AdminImpersonationController + ImpersonationService: user impersonation with
  token-based session management and activity logging
- BillingStatus enum, form requests, API resources, 23 feature tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 23:33:16 +02:00

93 lines
2.6 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
final class ImpersonationService
{
/**
* Start impersonating a target user.
*
* @return array{token: string, user: User, admin_id: string}
*/
public function start(User $admin, User $targetUser): array
{
if (! $admin->hasRole('super_admin')) {
abort(403, 'Only super admins can impersonate users.');
}
if ($targetUser->hasRole('super_admin')) {
abort(403, 'Cannot impersonate another super admin.');
}
$tokenName = 'impersonation-by-' . $admin->id;
$newToken = $targetUser->createToken($tokenName);
Cache::put(
"impersonation:{$newToken->accessToken->id}",
$admin->id,
now()->addHours(4),
);
activity('admin')
->causedBy($admin)
->performedOn($targetUser)
->event('admin.impersonation.started')
->withProperties([
'admin_id' => $admin->id,
'target_user_id' => $targetUser->id,
])
->log('Started impersonating user ' . $targetUser->full_name);
return [
'token' => $newToken->plainTextToken,
'user' => $targetUser,
'admin_id' => $admin->id,
];
}
/**
* Stop impersonation and return the original admin.
*/
public function stop(User $currentUser): User
{
$currentToken = $currentUser->currentAccessToken();
if (! $currentToken || ! str_starts_with($currentToken->name, 'impersonation-by-')) {
abort(400, 'No active impersonation session.');
}
$adminId = Cache::get("impersonation:{$currentToken->id}");
$admin = $adminId ? User::find($adminId) : null;
if (! $admin) {
// Fallback: extract admin ID from token name
$admin = User::find(str_replace('impersonation-by-', '', $currentToken->name));
}
activity('admin')
->causedBy($admin ?? $currentUser)
->performedOn($currentUser)
->event('admin.impersonation.stopped')
->withProperties([
'admin_id' => $admin?->id,
'impersonated_user_id' => $currentUser->id,
])
->log('Stopped impersonating user ' . $currentUser->full_name);
Cache::forget("impersonation:{$currentToken->id}");
$currentToken->delete();
if (! $admin) {
abort(400, 'Could not resolve original admin session.');
}
return $admin;
}
}