From c62f3776682d099797205537b525c198bb1cbc20 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Wed, 15 Apr 2026 22:30:58 +0200 Subject: [PATCH] fix: MFA setup completion not updating UI state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: the MFA status endpoint returned `mfa_enabled` as the JSON key but the TypeScript MfaStatus interface expected `enabled`. At runtime, `mfaStatus.value?.enabled` was always `undefined`, so `isEnabled` was always false — the banner never hid and the method cards never showed "Geconfigureerd". Additionally, the auth store had no way to re-fetch /auth/me after initialization, so `mfaSetupRequired` was never properly refreshed from the backend after MFA setup. Fixes: - Rename `mfa_enabled` → `enabled` in the MFA status endpoint response to match the TypeScript type (and the /auth/me MeResource which already used `enabled`) - Add `refreshUser()` to the auth store for post-initialization re-fetching of /auth/me - Call `refreshUser()` in onSetupCompleted so the store reflects the backend state without a full page reload - Update backend tests to match the renamed response key Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Api/V1/Auth/MfaSetupController.php | 2 +- api/tests/Feature/Auth/MfaSetupControllerTest.php | 4 ++-- .../components/account-settings/SecurityTab.vue | 7 +++++-- apps/app/src/stores/useAuthStore.ts | 15 +++++++++++++++ 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/api/app/Http/Controllers/Api/V1/Auth/MfaSetupController.php b/api/app/Http/Controllers/Api/V1/Auth/MfaSetupController.php index 6fe0b3f1..6e700161 100644 --- a/api/app/Http/Controllers/Api/V1/Auth/MfaSetupController.php +++ b/api/app/Http/Controllers/Api/V1/Auth/MfaSetupController.php @@ -135,7 +135,7 @@ final class MfaSetupController extends Controller $user = $request->user(); return $this->success([ - 'mfa_enabled' => $user->mfa_enabled, + 'enabled' => $user->mfa_enabled, 'method' => $user->mfa_method, 'confirmed_at' => $user->mfa_confirmed_at?->toIso8601String(), 'backup_codes_remaining' => $this->mfaService->getRemainingBackupCodeCount($user), diff --git a/api/tests/Feature/Auth/MfaSetupControllerTest.php b/api/tests/Feature/Auth/MfaSetupControllerTest.php index d9aafeb2..37a2f237 100644 --- a/api/tests/Feature/Auth/MfaSetupControllerTest.php +++ b/api/tests/Feature/Auth/MfaSetupControllerTest.php @@ -147,7 +147,7 @@ class MfaSetupControllerTest extends TestCase $response->assertOk() ->assertJson([ 'data' => [ - 'mfa_enabled' => false, + 'enabled' => false, 'method' => null, 'confirmed_at' => null, 'backup_codes_remaining' => 0, @@ -164,7 +164,7 @@ class MfaSetupControllerTest extends TestCase $response->assertOk() ->assertJson([ 'data' => [ - 'mfa_enabled' => true, + 'enabled' => true, 'method' => 'totp', 'backup_codes_remaining' => 10, ], diff --git a/apps/app/src/components/account-settings/SecurityTab.vue b/apps/app/src/components/account-settings/SecurityTab.vue index d14abccf..0e6041e1 100644 --- a/apps/app/src/components/account-settings/SecurityTab.vue +++ b/apps/app/src/components/account-settings/SecurityTab.vue @@ -80,10 +80,13 @@ const backupCodesColor = computed(() => { }) function onSetupCompleted() { + // Refetch MFA status so the method cards update (enabled, method, backup codes) refetchMfaStatus() - // Immediately clear the MFA enforcement state so the banner disappears - // and the router guard no longer redirects + // Immediately clear the enforcement flag so the router guard unblocks navigation authStore.mfaSetupRequired = false + // Refresh the full /auth/me response into the store so the flag stays + // correct on subsequent navigations (without needing a full page reload) + authStore.refreshUser() } function onDisabled() { diff --git a/apps/app/src/stores/useAuthStore.ts b/apps/app/src/stores/useAuthStore.ts index e838b3d7..1db98c15 100644 --- a/apps/app/src/stores/useAuthStore.ts +++ b/apps/app/src/stores/useAuthStore.ts @@ -88,6 +88,20 @@ export const useAuthStore = defineStore('auth', () => { } } + /** + * Re-fetch /auth/me and update the store. Safe to call at any time + * after initialization (e.g. after MFA setup or profile update). + */ + async function refreshUser(): Promise { + try { + const { data } = await apiClient.get<{ success: boolean; data: MeResponse }>('/auth/me') + setUser(data.data) + } + catch { + // Silently ignore — the existing state stays + } + } + /** * Called once on app startup. Validates the httpOnly cookie by calling * GET /auth/me. On 401, clears everything. @@ -132,5 +146,6 @@ export const useAuthStore = defineStore('auth', () => { logout, handleUnauthorized, initialize, + refreshUser, } })