Skip to content

feat(frontend): move 2FA management into dedicated tab with inline stepper#1649

Merged
riderx merged 5 commits into
mainfrom
feat/manage-2fa-tab
Feb 20, 2026
Merged

feat(frontend): move 2FA management into dedicated tab with inline stepper#1649
riderx merged 5 commits into
mainfrom
feat/manage-2fa-tab

Conversation

@WcaleNieWolny
Copy link
Copy Markdown
Contributor

@WcaleNieWolny WcaleNieWolny commented Feb 20, 2026

Summary (AI generated)

  • Created a new Manage 2FA tab under Account Settings with a dedicated page
  • Replaced the old dialog-based 2FA setup with an inline 5-step wizard: CAPTCHA → Send OTP → Verify OTP → Scan QR → Verify TOTP
  • When 2FA is enabled, shows a green checkmark status with setup date and disable option
  • When 2FA is not enabled, shows an orange X status with the step-by-step setup wizard
  • Removed all 2FA-related code from the General account settings page
  • Added redirect from ?setup2fa=true query param to the new Manage 2FA page

Motivation (AI generated)

The previous 2FA setup flow was scattered across the General account page with a confusing mix of inline email verification and popup dialogs for QR code scanning. Users had to understand a non-obvious two-phase process (verify email first, then enable 2FA). The new dedicated tab with a clear stepper makes the flow self-explanatory.

Business Impact (AI generated)

Improves 2FA adoption by making the setup process clearer and more guided. Organizations that enforce 2FA will see fewer support requests from users confused by the old flow.

Test Plan (AI generated)

  • Navigate to Account Settings and verify the new "Manage 2FA" tab appears
  • With 2FA disabled: verify the stepper shows all 5 steps and the orange X status
  • Complete CAPTCHA step and verify auto-advance to step 2
  • Send verification code and verify email is received with OTP token
  • Enter OTP code and verify advancement to QR code step
  • Scan QR code and verify TOTP code to enable 2FA
  • With 2FA enabled: verify green checkmark, setup date, and disable button
  • Disable 2FA and verify the stepper reappears
  • Verify the General account page no longer shows any 2FA sections
  • Verify ?setup2fa=true redirects to the Manage 2FA page
  • Note: Supabase Magic Link email template must include {{ .Token }} for OTP codes to appear

Generated with AI

Summary by CodeRabbit

  • New Features
    • Added a "Manage 2FA" page in account settings with a dedicated multi-step setup wizard.
    • Multi-step flow: CAPTCHA → email OTP → authenticator app (QR/TOTP) enrollment and final verification.
    • Users can enable/disable 2FA and see setup date tracking.
    • New UI labels including a "Resend" action and localized step/status messages for the 2FA flow.

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 20, 2026

Warning

Rate limit exceeded

@WcaleNieWolny has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 19 minutes and 35 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Extracts two-factor authentication management into a dedicated ManageTwoFactor page and component, adds translations and a new account tab, and removes inline MFA/OTP workflows from the account settings index component.

Changes

Cohort / File(s) Summary
Localization
messages/en.json
Added new 2FA and resend translation keys (setup/status steps, management label, resend).
Navigation & Types
src/constants/accountTabs.ts, src/route-map.d.ts
Added a new "manage-2fa" account tab (ShieldCheck icon) and route type/file mapping for ManageTwoFactor.vue/settings/account/manage-2fa.
New 2FA page
src/pages/settings/account/ManageTwoFactor.vue
New Vue component implementing a multi-step 2FA management flow (CAPTCHA, email OTP send/verify, TOTP QR enrollment, TOTP verification, enable/disable flows, cleanup of unverified factors, route/query handling, toast/error handling).
Account settings refactor
src/pages/settings/account/index.vue
Removed embedded MFA/OTP state, handlers, dialogs, and UI; replaced in-page setup with redirect to the new ManageTwoFactor page when setup2fa=true query is present.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Manage as "ManageTwoFactor<br/>Component"
    participant Captcha as "Turnstile<br/>CAPTCHA"
    participant EmailAPI as "Email OTP<br/>Endpoint"
    participant MFAEnroll as "MFA Enroll<br/>Endpoint"
    participant MFAVerify as "MFA Verify<br/>Endpoint"

    User->>Manage: Open /settings/account/manage-2fa
    Manage->>Manage: Load existing MFA factors, cleanup unverified
    Manage->>Captcha: Render CAPTCHA (step 1)
    User->>Captcha: Complete CAPTCHA
    Captcha->>Manage: Return token
    User->>Manage: Request OTP (step 2)
    Manage->>EmailAPI: Send OTP (with CAPTCHA token)
    EmailAPI->>User: Deliver OTP via email
    User->>Manage: Submit OTP
    Manage->>EmailAPI: Verify OTP
    EmailAPI->>Manage: OTP valid
    Manage->>MFAEnroll: Enroll TOTP factor (generate QR/secret)
    MFAEnroll->>Manage: Return QR + secret
    User->>Manage: Scan QR + enter TOTP code
    Manage->>MFAVerify: Verify TOTP
    MFAVerify->>Manage: TOTP verified
    Manage->>Manage: Enable MFA, persist factor
    Manage->>User: Show success notification
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • riderx

Poem

🐰 Hop hop, a setup bright,
CAPTCHA cleared in morning light,
OTP wings fly to your mail,
QR secrets tell their tale,
Now two factors keep you safe at night.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately describes the main change: moving 2FA management into a dedicated tab with an inline stepper, matching the core objective and primary file changes.
Description check ✅ Passed The PR description includes a summary and test plan, addressing the template requirements. However, it lacks screenshots/videos and incomplete checklist items.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/manage-2fa-tab

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@WcaleNieWolny WcaleNieWolny marked this pull request as draft February 20, 2026 10:42
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/pages/settings/account/ManageTwoFactor.vue (2)

244-247: as any cast on MFA factor — fragile property access.

created_at and updated_at are accessed via as any because they're not in the Supabase MFA factor type definition. If the API response changes, this silently returns null (which is handled), but it also hides potential type mismatches.

Consider adding a brief comment explaining why the cast is necessary, or define a local interface extending the factor type with these optional fields.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/settings/account/ManageTwoFactor.vue` around lines 244 - 247, The
as any casts on verifiedFactor when setting mfaSetupDate are fragile; create a
local interface (e.g., ExtendedMfaFactor) that extends the Supabase factor type
with optional created_at and updated_at fields, then narrow/cast verifiedFactor
to that interface when reading those properties, or add a concise TODO comment
explaining why the cast is required and that these fields are optional and come
from the API; update the code that sets mfaFactorId and mfaSetupDate to use the
typed ExtendedMfaFactor instead of as any so property access is explicit and
documented.

257-261: onBeforeUnmount with async — unenroll may not complete.

Vue does not await async lifecycle hooks. The supabase.auth.mfa.unenroll call may be dropped if the component tears down before it completes. The risk is mitigated by the cleanupUnverifiedFactors call in onMounted, which catches orphaned factors on next visit.

No change strictly required since the safety net exists, but worth noting the intent vs. guarantee gap.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/settings/account/ManageTwoFactor.vue` around lines 257 - 261, The
onBeforeUnmount hook is declared async so the unenroll promise may be dropped;
change it to a non-async handler that explicitly handles the promise (call
supabase.auth.mfa.unenroll({ factorId: enrolledFactorId.value
}).then(...).catch(...)) so errors are surfaced and the promise isn’t silently
ignored, and if you need a guarantee the call completes before navigation use a
route guard (e.g., beforeRouteLeave) to await supabase.auth.mfa.unenroll;
reference the existing onBeforeUnmount and supabase.auth.mfa.unenroll calls (and
keep cleanupUnverifiedFactors as the safety net).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/pages/settings/account/ManageTwoFactor.vue`:
- Around line 68-91: sendOtpVerification is reusing the single-use Cloudflare
Turnstile token (captchaToken.value) which causes Resend to fail; after a
successful OTP send (no error) clear the captcha token (set captchaToken.value
to '' or undefined) so subsequent calls don't resend an already-consumed token,
then continue to set otpVerificationCode and advance currentStep; keep using the
existing signInWithOtp call and the captchaToken.value || undefined guard so new
tokens can be supplied later.

---

Nitpick comments:
In `@src/pages/settings/account/ManageTwoFactor.vue`:
- Around line 244-247: The as any casts on verifiedFactor when setting
mfaSetupDate are fragile; create a local interface (e.g., ExtendedMfaFactor)
that extends the Supabase factor type with optional created_at and updated_at
fields, then narrow/cast verifiedFactor to that interface when reading those
properties, or add a concise TODO comment explaining why the cast is required
and that these fields are optional and come from the API; update the code that
sets mfaFactorId and mfaSetupDate to use the typed ExtendedMfaFactor instead of
as any so property access is explicit and documented.
- Around line 257-261: The onBeforeUnmount hook is declared async so the
unenroll promise may be dropped; change it to a non-async handler that
explicitly handles the promise (call supabase.auth.mfa.unenroll({ factorId:
enrolledFactorId.value }).then(...).catch(...)) so errors are surfaced and the
promise isn’t silently ignored, and if you need a guarantee the call completes
before navigation use a route guard (e.g., beforeRouteLeave) to await
supabase.auth.mfa.unenroll; reference the existing onBeforeUnmount and
supabase.auth.mfa.unenroll calls (and keep cleanupUnverifiedFactors as the
safety net).

Comment thread src/pages/settings/account/ManageTwoFactor.vue
…epper

Replace the confusing inline 2FA sections on the General account page
with a dedicated Manage 2FA tab that guides users through a clear
5-step wizard (CAPTCHA, send OTP, verify OTP, scan QR, verify TOTP)
and shows a clean status view when 2FA is already enabled.
@WcaleNieWolny WcaleNieWolny marked this pull request as ready for review February 20, 2026 14:50
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2bc78d7ffc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

})
otpSending.value = false

savedCaptchaToken.value = ''
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep CAPTCHA state after OTP send failure

When signInWithOtp fails in step 2, the code clears savedCaptchaToken before checking error, and this step has no way to re-run CAPTCHA. In CAPTCHA-enforced environments, a transient send error (or an expired token) leaves the user stuck because every retry sends undefined as captchaToken until they reload the page.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/pages/settings/account/index.vue (1)

636-642: ⚠️ Potential issue | 🟡 Minor

Hardcoded English strings — use t() for i18n.

Lines 638–641 contain raw English text that bypasses the translation system. These user-facing strings should use t() keys for consistency with the rest of the component and to support localization.

This action cannot be undone. Your account and all associated data will be permanently deleted.
Your account will be deleted after 30 days
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/settings/account/index.vue` around lines 636 - 642, Replace the
hardcoded English strings in the account deletion text with i18n calls using
t(); specifically update the two <p> elements that currently contain "This
action cannot be undone. Your account and all associated data will be
permanently deleted." and "Your account will be deleted after 30 days" to use
t('settings.account.deleteWarning') and t('settings.account.deleteAfter30Days')
(or similar keys) in the template, and add corresponding keys to the locale
files so translations are available; ensure you use the same i18n pattern used
elsewhere in this component (t()) rather than raw strings.
🧹 Nitpick comments (2)
src/pages/settings/account/ManageTwoFactor.vue (1)

498-504: "Resend" restarts from CAPTCHA (step 1) — intentional but potentially frustrating UX.

Since Turnstile tokens are single-use, going back to CAPTCHA is technically necessary. Consider labeling the button more descriptively (e.g., t('restart-verification')) so users understand why they're re-solving the CAPTCHA rather than just resending the code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/settings/account/ManageTwoFactor.vue` around lines 498 - 504, The
current "Resend" button calls restartFromCaptcha which intentionally restarts
the flow at the Turnstile CAPTCHA (step 1) but is misleading; change the button
label to a clearer translation key (e.g., use t('restart-verification') instead
of t('resend')) and add/update the corresponding i18n entry so the UI
communicates that verification must be re-solved; keep the onClick handler
(restartFromCaptcha) unchanged so the single-use Turnstile token flow still
restarts correctly.
src/pages/settings/account/index.vue (1)

450-455: Redirect does not await and lacks early return.

router.replace() returns a Promise. Although no code follows the if block today, adding await + return future-proofs the hook and prevents any brief flash of the account page content before the navigation completes.

Suggested fix
 onMounted(async () => {
   // Auto-redirect to Manage 2FA page if setup2fa query param is present
   if (route.query.setup2fa === 'true') {
-    router.replace('/settings/account/manage-2fa?setup2fa=true')
+    await router.replace('/settings/account/manage-2fa?setup2fa=true')
+    return
   }
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/settings/account/index.vue` around lines 450 - 455, The onMounted
hook should await the navigation and stop further execution to avoid a flash;
inside onMounted (the async function using route.query.setup2fa) change the
redirect call to await
router.replace('/settings/account/manage-2fa?setup2fa=true') and immediately
return after that await so the hook exits early when setup2fa === 'true'.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/pages/settings/account/ManageTwoFactor.vue`:
- Around line 239-244: The cleanupUnverifiedFactors function currently awaits
Promise.all of supabase.auth.mfa.unenroll calls which will reject if any call
fails and can abort onMounted; change it to a best-effort non-throwing cleanup
by using Promise.allSettled (or wrap each unenroll in try/catch) and swallow or
log individual errors instead of letting one rejection propagate, and ensure
callers (e.g., the onMounted that awaits cleanupUnverifiedFactors) do not rely
on it throwing so the page can finish initialization even if some unenrolls
fail.
- Around line 279-282: The code casts verifiedFactor to any to access
timestamps; remove the unnecessary "as any" casts and access created_at and
updated_at directly from verifiedFactor (the variables involved are
verifiedFactor, mfaFactorId, and mfaSetupDate). Update the block where
mfaFactorId.value is set and replace (verifiedFactor as any).created_at and
(verifiedFactor as any).updated_at with verifiedFactor.created_at and
verifiedFactor.updated_at so TypeScript uses the Supabase Factor type's fields
without casting.

---

Outside diff comments:
In `@src/pages/settings/account/index.vue`:
- Around line 636-642: Replace the hardcoded English strings in the account
deletion text with i18n calls using t(); specifically update the two <p>
elements that currently contain "This action cannot be undone. Your account and
all associated data will be permanently deleted." and "Your account will be
deleted after 30 days" to use t('settings.account.deleteWarning') and
t('settings.account.deleteAfter30Days') (or similar keys) in the template, and
add corresponding keys to the locale files so translations are available; ensure
you use the same i18n pattern used elsewhere in this component (t()) rather than
raw strings.

---

Duplicate comments:
In `@src/pages/settings/account/ManageTwoFactor.vue`:
- Around line 72-97: The review notes that the single-use CAPTCHA token handling
has been fixed: ensure sendOtpVerification clears savedCaptchaToken
(savedCaptchaToken.value = '') after each attempt and that restartFromCaptcha
(triggered by the "Resend" button) resets the flow back to step 1 (e.g., by
updating currentStep) so a fresh Turnstile token is obtained before the next
call to supabase.auth.signInWithOtp; keep these behaviors in sendOtpVerification
and restartFromCaptcha as implemented.

---

Nitpick comments:
In `@src/pages/settings/account/index.vue`:
- Around line 450-455: The onMounted hook should await the navigation and stop
further execution to avoid a flash; inside onMounted (the async function using
route.query.setup2fa) change the redirect call to await
router.replace('/settings/account/manage-2fa?setup2fa=true') and immediately
return after that await so the hook exits early when setup2fa === 'true'.

In `@src/pages/settings/account/ManageTwoFactor.vue`:
- Around line 498-504: The current "Resend" button calls restartFromCaptcha
which intentionally restarts the flow at the Turnstile CAPTCHA (step 1) but is
misleading; change the button label to a clearer translation key (e.g., use
t('restart-verification') instead of t('resend')) and add/update the
corresponding i18n entry so the UI communicates that verification must be
re-solved; keep the onClick handler (restartFromCaptcha) unchanged so the
single-use Turnstile token flow still restarts correctly.

Comment on lines +239 to +244
async function cleanupUnverifiedFactors(factors: { id: string, status: string }[]) {
const unverified = factors.filter(f => f.status === 'unverified')
if (unverified.length > 0) {
await Promise.all(unverified.map(f => supabase.auth.mfa.unenroll({ factorId: f.id })))
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

cleanupUnverifiedFactors can throw and block page initialization.

Promise.all on line 242 will reject if any single unenroll call fails, and the await on line 274 has no try/catch. This would abort onMounted, leaving the page in a permanent loading state. Cleanup of stale factors is best-effort and should not prevent the page from rendering.

Suggested fix
 async function cleanupUnverifiedFactors(factors: { id: string, status: string }[]) {
   const unverified = factors.filter(f => f.status === 'unverified')
   if (unverified.length > 0) {
-    await Promise.all(unverified.map(f => supabase.auth.mfa.unenroll({ factorId: f.id })))
+    await Promise.allSettled(unverified.map(f => supabase.auth.mfa.unenroll({ factorId: f.id })))
   }
 }

Also applies to: 262-295

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/settings/account/ManageTwoFactor.vue` around lines 239 - 244, The
cleanupUnverifiedFactors function currently awaits Promise.all of
supabase.auth.mfa.unenroll calls which will reject if any call fails and can
abort onMounted; change it to a best-effort non-throwing cleanup by using
Promise.allSettled (or wrap each unenroll in try/catch) and swallow or log
individual errors instead of letting one rejection propagate, and ensure callers
(e.g., the onMounted that awaits cleanupUnverifiedFactors) do not rely on it
throwing so the page can finish initialization even if some unenrolls fail.

Comment thread src/pages/settings/account/ManageTwoFactor.vue
The cli-s3 test was failing with a duplicate key constraint violation
when Vitest retried the test. This adds cleanup before inserting
the version to handle test retries properly.
@sonarqubecloud
Copy link
Copy Markdown

@riderx riderx merged commit a34cab6 into main Feb 20, 2026
14 checks passed
@riderx riderx deleted the feat/manage-2fa-tab branch February 20, 2026 17:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants