feat: set preferred MFA method from account settings

Adds the ability for users to change their preferred/primary MFA method
when both TOTP and email are available.

Backend:
- Add PUT /auth/mfa/preferred-method endpoint with validation
  (method must be totp/email, MFA must be enabled, TOTP must be
  configured if selecting totp)
- Add totp_configured and email_configured fields to MFA status
  endpoint (totp = has secret + enabled, email = always when enabled)
- Fix setupEmail() to preserve mfa_secret so TOTP config survives
  when email is set up as a second method

Frontend (organizer + portal):
- Add useSetPreferredMethod() composable to useMfa.ts
- Add totp_configured/email_configured to MfaStatus type
- SecurityTab method cards now show "Primaire methode" chip on the
  preferred method and "Als primair instellen" button on the other
- Portal security section shows per-method rows with status chips
  and primary switching

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-15 22:47:34 +02:00
parent a77986334c
commit d5fb15e5fe
9 changed files with 225 additions and 43 deletions

View File

@@ -11,6 +11,7 @@ use App\Enums\MfaMethod;
use App\Services\MfaService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
final class MfaSetupController extends Controller
{
@@ -140,9 +141,40 @@ final class MfaSetupController extends Controller
'confirmed_at' => $user->mfa_confirmed_at?->toIso8601String(),
'backup_codes_remaining' => $this->mfaService->getRemainingBackupCodeCount($user),
'is_required' => $this->mfaService->isMfaRequired($user),
'totp_configured' => $user->mfa_enabled && $user->mfa_secret !== null,
'email_configured' => $user->mfa_enabled,
]);
}
public function setPreferredMethod(Request $request): JsonResponse
{
$request->validate([
'method' => ['required', Rule::in(['totp', 'email'])],
]);
$user = $request->user();
if (! $user->mfa_enabled) {
return $this->error('MFA is niet ingeschakeld.', 422);
}
$method = $request->input('method');
if ($method === 'totp' && $user->mfa_secret === null) {
return $this->error('Authenticator app is niet geconfigureerd.', 422);
}
$user->update(['mfa_method' => $method]);
activity('mfa')
->causedBy($user)
->performedOn($user)
->withProperties(['method' => $method])
->log('mfa.preferred_method.changed');
return $this->success(['method' => $method], 'Primaire methode gewijzigd.');
}
private function verifyBackupCodeForDisable(\App\Models\User $user, string $code): void
{
$normalizedCode = strtoupper(str_replace([' ', '-'], '', $code));