diff --git a/resources/js/components/two-factor/Setup.vue b/resources/js/components/two-factor/Setup.vue
index 0526ad82559..aca5275063c 100644
--- a/resources/js/components/two-factor/Setup.vue
+++ b/resources/js/components/two-factor/Setup.vue
@@ -25,12 +25,20 @@ onMounted(() => getSetupCode());
function getSetupCode() {
loading.value = true;
- axios.post(props.enableUrl).then((response) => {
- qrCode.value = response.data.qr;
- secretKey.value = response.data.secret_key;
- confirmUrl.value = response.data.confirm_url;
- loading.value = false;
- });
+ axios
+ .post(props.enableUrl)
+ .then((response) => {
+ qrCode.value = response.data.qr;
+ secretKey.value = response.data.secret_key;
+ confirmUrl.value = response.data.confirm_url;
+ })
+ .catch((error) => {
+ setupModalOpen.value = false;
+ Statamic.$toast.error(error.response?.data?.message ?? error.message);
+ })
+ .finally(() => {
+ loading.value = false;
+ });
}
function confirm() {
diff --git a/resources/js/pages/auth/two-factor/Setup.vue b/resources/js/pages/auth/two-factor/Setup.vue
index e3deb6bc6aa..26bc01cf0e3 100644
--- a/resources/js/pages/auth/two-factor/Setup.vue
+++ b/resources/js/pages/auth/two-factor/Setup.vue
@@ -2,6 +2,7 @@
import Head from '@/pages/layout/Head.vue';
import Outside from '@/pages/layout/Outside.vue';
import TwoFactorSetup from '@/components/two-factor/Setup.vue';
+import { requireElevatedSession } from '@/components/elevated-sessions';
import { AuthCard, Button } from '@ui';
import { ref } from 'vue';
@@ -11,6 +12,12 @@ const props = defineProps(['routes', 'redirect']);
const setupModalOpen = ref(false);
+function openSetupModal() {
+ requireElevatedSession()
+ .then(() => (setupModalOpen.value = true))
+ .catch(() => Statamic.$toast.error(__('statamic::messages.elevated_session_required')));
+}
+
function setupComplete() {
window.location.href = props.redirect;
}
@@ -24,7 +31,7 @@ function setupComplete() {
:title="__('Set up Two Factor Authentication')"
:description="__('statamic::messages.two_factor_account_requirement')"
>
-
+
{
-
+
+
+
diff --git a/routes/cp.php b/routes/cp.php
index 2626fc0f7f2..1ba72b905d0 100644
--- a/routes/cp.php
+++ b/routes/cp.php
@@ -451,11 +451,13 @@
Route::get('session-timeout', SessionTimeoutController::class)->name('session.timeout');
if (config('statamic.users.elevated_sessions_enabled')) {
- Route::get('auth/confirm-password', [ElevatedSessionController::class, 'showForm'])->name('confirm-password');
- Route::get('elevated-session', [ElevatedSessionController::class, 'status'])->name('elevated-session.status');
- Route::get('elevated-session/passkey-options', [ElevatedSessionController::class, 'options'])->name('elevated-session.passkey-options')->middleware('throttle:statamic.cp.passkeys');
- Route::post('elevated-session', [ElevatedSessionController::class, 'confirm'])->name('elevated-session.confirm')->middleware('throttle:statamic.cp.auth');
- Route::get('elevated-session/resend-code', [ElevatedSessionController::class, 'resendCode'])->name('elevated-session.resend-code')->middleware('throttle:send-elevated-session-code');
+ Route::withoutMiddleware(RedirectIfTwoFactorSetupIncomplete::class)->group(function () {
+ Route::get('auth/confirm-password', [ElevatedSessionController::class, 'showForm'])->name('confirm-password');
+ Route::get('elevated-session', [ElevatedSessionController::class, 'status'])->name('elevated-session.status');
+ Route::get('elevated-session/passkey-options', [ElevatedSessionController::class, 'options'])->name('elevated-session.passkey-options')->middleware('throttle:statamic.cp.passkeys');
+ Route::post('elevated-session', [ElevatedSessionController::class, 'confirm'])->name('elevated-session.confirm')->middleware('throttle:statamic.cp.auth');
+ Route::get('elevated-session/resend-code', [ElevatedSessionController::class, 'resendCode'])->name('elevated-session.resend-code')->middleware('throttle:send-elevated-session-code');
+ });
}
Route::get('playground', PlaygroundController::class)->name('playground');
diff --git a/src/Actions/DisableTwoFactorAuthentication.php b/src/Actions/DisableTwoFactorAuthentication.php
index 0db90d3140b..ac25af4d76f 100644
--- a/src/Actions/DisableTwoFactorAuthentication.php
+++ b/src/Actions/DisableTwoFactorAuthentication.php
@@ -30,7 +30,7 @@ public function buttonText()
public function visibleTo($item)
{
- return $item instanceof User && $item->hasEnabledTwoFactorAuthentication();
+ return $item instanceof User && ! is_null($item->two_factor_secret);
}
public function authorize($user, $item)
diff --git a/tests/Actions/DisableTwoFactorTest.php b/tests/Actions/DisableTwoFactorTest.php
index b92a046dcba..4b0973332d8 100644
--- a/tests/Actions/DisableTwoFactorTest.php
+++ b/tests/Actions/DisableTwoFactorTest.php
@@ -50,6 +50,14 @@ public function its_only_visible_for_users_with_two_factor_enabled()
$this->assertTrue((new Action)->visibleTo($userWith2fa));
}
+ #[Test]
+ public function its_visible_for_users_with_incomplete_two_factor_setup()
+ {
+ $userWithIncompleteSetup = User::make()->set('two_factor_secret', 'secret');
+
+ $this->assertTrue((new Action)->visibleTo($userWithIncompleteSetup));
+ }
+
#[Test]
public function it_does_not_disable_two_factor_if_current_user_doesnt_have_permission()
{
diff --git a/tests/Auth/ElevatedSessionTest.php b/tests/Auth/ElevatedSessionTest.php
index ae83c518ccf..29bdc269d47 100644
--- a/tests/Auth/ElevatedSessionTest.php
+++ b/tests/Auth/ElevatedSessionTest.php
@@ -220,6 +220,33 @@ public function starting_elevated_session_clears_stored_verification_code()
->assertSessionMissing('statamic_elevated_session_verification_code');
}
+ #[Test]
+ public function it_can_get_elevated_session_status_when_two_factor_setup_is_incomplete()
+ {
+ config(['statamic.users.two_factor_enforced_roles' => ['*']]);
+
+ $this
+ ->actingAs($this->user)
+ ->getJson('/cp/elevated-session')
+ ->assertOk()
+ ->assertJson([
+ 'elevated' => false,
+ 'method' => 'password_confirmation',
+ ]);
+ }
+
+ #[Test]
+ public function it_can_start_an_elevated_session_when_two_factor_setup_is_incomplete()
+ {
+ config(['statamic.users.two_factor_enforced_roles' => ['*']]);
+
+ $this
+ ->actingAs($this->user)
+ ->postJson('/cp/elevated-session', ['password' => 'secret'])
+ ->assertOk()
+ ->assertSessionHas('statamic_elevated_session', now()->timestamp);
+ }
+
#[Test]
public function it_cannot_start_elevated_session_with_incorrect_password()
{