fix(settings): move password reset to the end of the security section

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.
This commit is contained in:
Marc-André Bombeck committed 2026-08-08 20:57:40 +02:00
1 parent e344ebb781
commit ee561eaa23
2 files changed
+26 -9

No files matched your search

@@ -72,14 +72,30 @@ describe("change-password card placement", () => {
expect(render(<PasswordCard />)).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(<SecuritySection />);
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("<PasskeyListSection");
const passwordIdx = source.indexOf("<PasswordCard");
expect(passkeyIdx).toBeGreaterThan(-1);
expect(passwordIdx).toBeGreaterThan(passkeyIdx);
});
it("has exactly one home in the tree", () => {
const owners = walk(join(ROOT, "src"))
.filter((file) => readFileSync(file, "utf8").includes("PasswordCard"))
@@ -50,13 +50,6 @@ export function SecuritySection() {
<div className="space-y-6">
{showNudge && <PasskeyUpgradeNudge />}
{/* 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. */}
<PasswordCard />
{isLoading || !status ? (
<>
<Skeleton className="h-40 w-full rounded-xl" />
@@ -75,6 +68,14 @@ export function SecuritySection() {
<PasskeyListSection isAuthenticated={isAuthenticated} />
</>
)}
{/* 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. */}
<PasswordCard />
</div>
);
}