Three verification methods (TOTP authenticator, email code, backup codes), trusted device management with 30-day expiry, role-based enforcement for super_admin and org_admin, admin reset capability, and full test coverage (46 tests). Modifies login flow to support MFA challenge/response with temporary session tokens stored in cache. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
104 lines
3.1 KiB
PHP
104 lines
3.1 KiB
PHP
<?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 App\Services\MfaService;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
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);
|
|
}
|
|
|
|
public function resetMfa(Request $request, User $user, MfaService $mfaService): JsonResponse
|
|
{
|
|
if (! $user->mfa_enabled) {
|
|
return $this->error('MFA is niet ingeschakeld voor deze gebruiker.', 422);
|
|
}
|
|
|
|
$mfaService->adminReset($request->user(), $user);
|
|
|
|
return $this->success(null, 'MFA reset for user');
|
|
}
|
|
}
|