From ee561eaa2351581fd2f0b6c3ce417cfc64bd2a93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 8 Aug 2026 20:57:40 +0200 Subject: [PATCH] fix(settings): move password reset to the end of the security section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security now reads authenticator, then security keys, then passkeys, then the password — the password card was mounted first. Keep it outside the factor-status conditional so a stalled MFA read never takes it down; it just renders last instead of first. A source-order guard pins the sequence, since this class of ordering regression is easy to miss. --- .../password-card-placement.test.tsx | 20 +++++++++++++++++-- .../settings/security-section/index.tsx | 15 +++++++------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/components/settings/__tests__/password-card-placement.test.tsx b/src/components/settings/__tests__/password-card-placement.test.tsx index ec428f6f3..de120af1e 100644 --- a/src/components/settings/__tests__/password-card-placement.test.tsx +++ b/src/components/settings/__tests__/password-card-placement.test.tsx @@ -72,14 +72,30 @@ describe("change-password card placement", () => { expect(render()).toContain(SLOT); }); - it("is mounted by Settings → Security before the factor status lands", () => { + it("is mounted by Settings → Security even before the factor status lands", () => { // The mocked query never resolves, which is the case that matters: the // MFA read is still in flight (or failing) and the password card is - // reachable regardless. + // reachable regardless — it sits outside the status conditional. const html = render(); expect(html).toContain(SLOT); }); + it("orders the password card last, after the passkey list", () => { + // The section reads authenticator → security keys → passkeys → password. + // The order lives in one component and the resolved cards only render once + // the status query settles (the render mock keeps it in flight), so the + // guard reads the source: the password card must come after the passkey + // list. Re-reported once as an order regression; this pins it. + const source = readFileSync( + join(ROOT, "src/components/settings/security-section/index.tsx"), + "utf8", + ); + const passkeyIdx = source.indexOf(" { const owners = walk(join(ROOT, "src")) .filter((file) => readFileSync(file, "utf8").includes("PasswordCard")) diff --git a/src/components/settings/security-section/index.tsx b/src/components/settings/security-section/index.tsx index 4b439c67b..757b67717 100644 --- a/src/components/settings/security-section/index.tsx +++ b/src/components/settings/security-section/index.tsx @@ -50,13 +50,6 @@ export function SecuritySection() {
{showNudge && } - {/* The password leads, and it renders before the factor status has - landed — it depends on nothing this page fetches. It is also the - only way to change a password now that the Account card is gone, so - a `/api/auth/me/mfa` that never answers must not take it down with - the cards that do need it. */} - - {isLoading || !status ? ( <> @@ -75,6 +68,14 @@ export function SecuritySection() { )} + + {/* The password comes last: authenticator, then security keys, then + passkeys, then the password. It sits OUTSIDE the status conditional + on purpose — it depends on nothing this page fetches and is the only + way to change a password now that the Account card is gone, so a + `/api/auth/me/mfa` that never answers must not take it down with the + cards that do need the status. */} +
); }