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>
This commit is contained in:
2026-04-14 23:33:16 +02:00
parent ec31646a93
commit ddf26dad33
18 changed files with 1299 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\V1\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\AdminUpdateUserRequest;
use App\Http\Resources\Admin\AdminUserResource;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
final class AdminUserController extends Controller
{
public function index(): AnonymousResourceCollection
{
$query = User::with('organisations');
if ($search = request('search')) {
$query->where(function ($q) use ($search) {
$q->where('first_name', 'like', "%{$search}%")
->orWhere('last_name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%");
});
}
if ($organisationId = request('organisation_id')) {
$query->whereHas('organisations', fn ($q) => $q->where('organisations.id', $organisationId));
}
if ($role = request('role')) {
$query->role($role);
}
$query->orderBy('first_name')->orderBy('last_name');
return AdminUserResource::collection($query->paginate());
}
public function show(User $user): JsonResponse
{
$user->load('organisations');
return $this->success(new AdminUserResource($user));
}
public function update(AdminUpdateUserRequest $request, User $user): JsonResponse
{
$validated = $request->validated();
$roles = $validated['roles'] ?? null;
unset($validated['roles']);
if (! empty($validated)) {
$user->update($validated);
}
if ($roles !== null) {
// Sync only platform-level roles, preserving org/event roles
$platformRoles = ['super_admin', 'support_agent'];
$currentRoles = $user->getRoleNames()->filter(fn ($r) => ! in_array($r, $platformRoles))->all();
$user->syncRoles(array_merge($currentRoles, $roles));
}
activity('admin')
->causedBy(auth()->user())
->performedOn($user)
->event('admin.user.updated')
->withProperties($request->validated())
->log('Updated user ' . $user->full_name);
$user->load('organisations');
return $this->success(new AdminUserResource($user));
}
public function destroy(User $user): JsonResponse
{
activity('admin')
->causedBy(auth()->user())
->performedOn($user)
->event('admin.user.deleted')
->log('Deleted user ' . $user->full_name);
$user->tokens()->delete();
$user->delete();
return response()->json(null, 204);
}
}