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:
@@ -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));
|
||||
|
||||
@@ -90,14 +90,16 @@ final class MfaService
|
||||
/**
|
||||
* Setup email as MFA method (simpler than TOTP — no QR code).
|
||||
* Sends a verification code to confirm.
|
||||
* Preserves the TOTP secret so both methods can coexist.
|
||||
*/
|
||||
public function setupEmail(User $user): void
|
||||
{
|
||||
$user->update([
|
||||
'mfa_method' => MfaMethod::EMAIL->value,
|
||||
'mfa_secret' => null,
|
||||
'mfa_confirmed_at' => null,
|
||||
]);
|
||||
if (! $user->mfa_enabled) {
|
||||
$user->update([
|
||||
'mfa_method' => MfaMethod::EMAIL->value,
|
||||
'mfa_confirmed_at' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->sendEmailCode($user);
|
||||
}
|
||||
|
||||
@@ -137,6 +137,7 @@ Route::middleware('auth:sanctum')->group(function () {
|
||||
Route::post('auth/mfa/disable', [MfaSetupController::class, 'disable']);
|
||||
Route::post('auth/mfa/backup-codes', [MfaSetupController::class, 'regenerateBackupCodes']);
|
||||
Route::get('auth/mfa/status', [MfaSetupController::class, 'status']);
|
||||
Route::put('auth/mfa/preferred-method', [MfaSetupController::class, 'setPreferredMethod']);
|
||||
|
||||
// Trusted devices
|
||||
Route::get('auth/trusted-devices', [TrustedDeviceController::class, 'index']);
|
||||
|
||||
Reference in New Issue
Block a user