Files
crewli/api/app/Http/Controllers/Api/V1/Admin/AdminUserController.php
bert.hausmans 948687f27e feat: enterprise MFA with TOTP, email codes, backup codes, and trusted devices
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>
2026-04-15 20:45:55 +02:00

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