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>
93 lines
2.6 KiB
PHP
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;
|
|
}
|
|
}
|