From 67ce1e9d9dc002eadbbbef1d3396203f49001bf7 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Thu, 16 Apr 2026 02:51:50 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20impersonation=20UX=20=E2=80=94=20banner?= =?UTF-8?q?=20contrast,=20route=20blocking,=20nav=20filtering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Banner: white elevated button for contrast, fixed 48px height, layout top padding offset so content isn't obscured - Middleware: allow GET me/profile (viewing), block mutations only; add auth/refresh to blocked routes - Navigation: hide Platform section during impersonation; hide org-dependent items when impersonated user has no organisation - Test: add read-only routes allowed test, auth/refresh blocked test Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Http/Middleware/HandleImpersonation.php | 16 ++++--- .../Api/V1/Admin/AdminImpersonationTest.php | 18 +++++++ .../platform/ImpersonationBanner.vue | 47 +++++++------------ .../DefaultLayoutWithVerticalNav.vue | 20 +++++++- 4 files changed, 64 insertions(+), 37 deletions(-) diff --git a/api/app/Http/Middleware/HandleImpersonation.php b/api/app/Http/Middleware/HandleImpersonation.php index fe4154d1..e44644c9 100644 --- a/api/app/Http/Middleware/HandleImpersonation.php +++ b/api/app/Http/Middleware/HandleImpersonation.php @@ -14,15 +14,14 @@ use Symfony\Component\HttpFoundation\Response; class HandleImpersonation { /** - * Routes that are blocked during impersonation. - * These are prefix-matched against the request path (without api/v1 prefix). + * Routes that are blocked during impersonation (all methods). + * Prefix-matched against the request path (without api/v1 prefix). */ - private const SENSITIVE_ROUTE_PREFIXES = [ - 'auth/password', + private const BLOCKED_ROUTE_PREFIXES = [ 'auth/logout', + 'auth/refresh', 'auth/mfa', 'auth/trusted-devices', - 'me/profile', 'me/change-password', 'me/change-email', 'admin/impersonate', @@ -101,7 +100,12 @@ class HandleImpersonation $path = $request->path(); $path = preg_replace('#^api/v1/#', '', $path); - foreach (self::SENSITIVE_ROUTE_PREFIXES as $prefix) { + // Block profile mutations but allow GET (viewing) + if (str_starts_with($path, 'me/profile') && $request->method() !== 'GET') { + return true; + } + + foreach (self::BLOCKED_ROUTE_PREFIXES as $prefix) { if (str_starts_with($path, $prefix)) { return true; } diff --git a/api/tests/Feature/Api/V1/Admin/AdminImpersonationTest.php b/api/tests/Feature/Api/V1/Admin/AdminImpersonationTest.php index ce1822f2..51f15312 100644 --- a/api/tests/Feature/Api/V1/Admin/AdminImpersonationTest.php +++ b/api/tests/Feature/Api/V1/Admin/AdminImpersonationTest.php @@ -391,6 +391,7 @@ class AdminImpersonationTest extends TestCase ['POST', '/api/v1/auth/mfa/setup/totp'], ['GET', '/api/v1/auth/mfa/status'], ['GET', '/api/v1/auth/trusted-devices'], + ['POST', '/api/v1/auth/refresh'], ]; foreach ($sensitiveRoutes as [$method, $url]) { @@ -406,6 +407,23 @@ class AdminImpersonationTest extends TestCase } } + public function test_read_only_routes_allowed_during_impersonation(): void + { + Sanctum::actingAs($this->superAdmin); + + $this->postJson( + "/api/v1/admin/impersonate/{$this->targetUser->id}", + $this->startPayload(), + )->assertOk(); + + // GET /auth/me should be allowed (profile viewing) + $response = $this->withHeader('X-Impersonate-User', $this->targetUser->id) + ->getJson('/api/v1/auth/me'); + + $response->assertOk(); + $response->assertJsonPath('data.id', $this->targetUser->id); + } + public function test_ip_change_terminates_session(): void { Sanctum::actingAs($this->superAdmin); diff --git a/apps/app/src/components/platform/ImpersonationBanner.vue b/apps/app/src/components/platform/ImpersonationBanner.vue index dd0bf3f4..1fecb19e 100644 --- a/apps/app/src/components/platform/ImpersonationBanner.vue +++ b/apps/app/src/components/platform/ImpersonationBanner.vue @@ -65,54 +65,43 @@ onUnmounted(() => { - + Je bekijkt het platform als {{ impersonationStore.impersonatedUser?.full_name }} - ({{ impersonationStore.impersonatedUser?.email }}) - - {{ remainingFormatted }} - + + Terug naar admin - - diff --git a/apps/app/src/layouts/components/DefaultLayoutWithVerticalNav.vue b/apps/app/src/layouts/components/DefaultLayoutWithVerticalNav.vue index 2d7ab896..48fb1c65 100644 --- a/apps/app/src/layouts/components/DefaultLayoutWithVerticalNav.vue +++ b/apps/app/src/layouts/components/DefaultLayoutWithVerticalNav.vue @@ -1,22 +1,36 @@