fix(auth): replace the sign-in page in the history after signing in

Back after signing in, registering or taking over a profile showed the
form that had just been submitted, because each pushed the next page
on top of it. Password, passkey and second-factor sign-in, registration
and the password take-over now replace the page instead.
This commit is contained in:
Marc-André Bombeck committed 2026-11-11 21:15:05 +01:00
1 parent 6c5a6eacbb
commit 8fdc238981
6 files changed
+47 -10

No files matched your search

@@ -3,12 +3,19 @@
*
* Single sign-on leaves the page for the identity provider and the session
* starts on the way back, so the previous account's offline copies have to
* be gone before the page is left.
* be gone before the page is left. A sign-in that stays in the app replaces
* the sign-in page in the history, so Back does not show the form again.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { stubFetch } from "../../../../../tests/browser/fetch-stub";
import { page } from "vitest/browser";
import {
stubFetch,
type FetchStub,
} from "../../../../../tests/browser/fetch-stub";
import { renderWithApp } from "../../../../../tests/browser/render";
import { recordHistory } from "../../../../../tests/browser/url";
const clearOffline = vi.hoisted(() => vi.fn(async () => {}));
const loadDocument = vi.hoisted(() => vi.fn());
@@ -20,8 +27,10 @@ vi.mock("@/lib/navigation/load-document", () => ({ loadDocument }));
import LoginPage from "../page";
let api: FetchStub;
beforeEach(() => {
stubFetch()
api = stubFetch()
.on("GET", "/api/auth/registration-status", {
data: { registrationEnabled: false, sessionCookieSecure: false },
})
@@ -50,3 +59,23 @@ describe("sign-in page, single sign-on", () => {
);
});
});
describe("sign-in page, password", () => {
it("replaces itself with the page asked for", async () => {
api.on("POST", "/api/auth/login", { data: { id: "signed-in" } });
window.history.replaceState(null, "", "/auth/login?next=%2Fmood%3Fadd%3D1");
const history = recordHistory();
const { screen } = await renderWithApp(<LoginPage />, { user: null });
await screen.getByTestId("login-use-password").click();
await screen.getByTestId("login-email").fill("someone");
await page
.elementLocator(document.querySelector("#password")!)
.fill("a long enough passphrase");
await screen.getByTestId("login-password-submit").click();
await expect.poll(() => window.location.pathname).toBe("/mood");
expect(window.location.search).toBe("?add=1");
expect(history.pushes()).toEqual([]);
});
});
+1 -1
View File
@@ -236,7 +236,7 @@ export default function LoginPage() {
if (target === "/" && isDashboardSnapshotEnabled()) {
prefetchDashboardSnapshot(queryClient);
}
router.push(target);
router.replace(target);
}
async function handlePasskeyLogin() {
@@ -3,7 +3,8 @@
*
* A new account starts from an empty cache, like every other sign-in: the
* client cache outlives the navigation, and nothing read before the account
* existed belongs to it.
* existed belongs to it. The app replaces the page in the history, so Back
* does not show the form again.
*/
import { describe, expect, it } from "vitest";
import { page } from "vitest/browser";
@@ -12,6 +13,7 @@ import { queryKeys } from "@/lib/query-keys";
import { stubFetch } from "../../../../../tests/browser/fetch-stub";
import { renderWithApp } from "../../../../../tests/browser/render";
import { recordHistory } from "../../../../../tests/browser/url";
import RegisterPage from "../page";
async function fill(selector: string, value: string): Promise<void> {
@@ -24,6 +26,7 @@ describe("registration page", () => {
.on("GET", "/api/auth/me", { status: 401, error: "Unauthorized" })
.on("POST", "/api/auth/register", { data: { id: "new-account" } });
window.history.replaceState(null, "", "/auth/register");
const history = recordHistory();
const { screen, queryClient } = await renderWithApp(<RegisterPage />, {
user: null,
});
@@ -37,6 +40,7 @@ describe("registration page", () => {
await expect.poll(() => window.location.pathname).toBe("/");
expect(api.calls("POST", "/api/auth/register")).toHaveLength(1);
expect(history.pushes()).toEqual([]);
expect(queryClient.getQueryData(queryKeys.dashboardSnapshot())).toBe(
undefined,
);
+1 -1
View File
@@ -74,7 +74,7 @@ export default function RegisterPage() {
});
queryClient.clear(); // nothing cached before belongs to the new account
router.push("/");
router.replace("/");
} catch (err) {
setError(
err instanceof ApiError && err.message
@@ -2,9 +2,10 @@
* Taking over a managed profile, driven by the claim page's buttons.
*
* Both ways in start a session for a different account than whatever this
* browser held before: the password claim stays in the single-page app and
* starts from an empty cache, the single-sign-on claim leaves for the
* identity provider and clears the offline copies before it goes.
* browser held before: the password claim stays in the single-page app,
* starts from an empty cache and replaces the claim page in the history; the
* single-sign-on claim leaves for the identity provider and clears the
* offline copies before it goes.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { page } from "vitest/browser";
@@ -16,6 +17,7 @@ import {
type FetchStub,
} from "../../../../tests/browser/fetch-stub";
import { renderWithApp } from "../../../../tests/browser/render";
import { recordHistory } from "../../../../tests/browser/url";
const clearOffline = vi.hoisted(() => vi.fn(async () => {}));
const loadDocument = vi.hoisted(() => vi.fn());
@@ -70,6 +72,7 @@ async function click(slot: string): Promise<void> {
describe("ClaimProfile", () => {
it("starts the claimed account from an empty cache", async () => {
const history = recordHistory();
const { queryClient } = await renderWithApp(
<ClaimProfile token={TOKEN} />,
{
@@ -87,6 +90,7 @@ describe("ClaimProfile", () => {
.poll(() => window.location.pathname)
.toBe("/onboarding/handover");
expect(api.calls("POST", "/api/auth/claim")).toHaveLength(1);
expect(history.pushes()).toEqual([]);
expect(queryClient.getQueryData(queryKeys.dashboardSnapshot())).toBe(
undefined,
);
+1 -1
View File
@@ -122,7 +122,7 @@ export function ClaimProfile({
onSuccess: () => {
// As after any sign-in: nothing cached before belongs to this account.
queryClient.clear();
router.push("/onboarding/handover");
router.replace("/onboarding/handover");
},
},
);