v4: signing in comes back to the page that asked for it — a guest on a signed-in page, the dump editor, /notifications, /community, /settings or /manage, went to /login and, once signed in, to /, so a digest's link opened signed out lost its way; and since the redirect pushed, Back led to it again. RestrictedLoggedIn now replaces the page with /login?next=<the page>, as /authorize already did. A guest on a feed's "follow" tab, which only a link leads to, is sent there the same way, rather than shown the default tab.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||||
import { useLocation } from "react-router";
|
import { Navigate, useLocation } from "react-router";
|
||||||
|
|
||||||
import { AppHeader } from "../components/layout/AppHeader.tsx";
|
import { AppHeader } from "../components/layout/AppHeader.tsx";
|
||||||
import { PresenceRow } from "../components/feed/PresenceRow.tsx";
|
import { PresenceRow } from "../components/feed/PresenceRow.tsx";
|
||||||
@@ -36,6 +36,7 @@ import { DumpFeed, type MainFeedProps } from "../components/feed/DumpFeed.tsx";
|
|||||||
import { JournalFeed } from "../components/feed/JournalFeed.tsx";
|
import { JournalFeed } from "../components/feed/JournalFeed.tsx";
|
||||||
import { FollowedFeed } from "../components/feed/FollowedFeed.tsx";
|
import { FollowedFeed } from "../components/feed/FollowedFeed.tsx";
|
||||||
import { NotFound } from "./NotFound.tsx";
|
import { NotFound } from "./NotFound.tsx";
|
||||||
|
import { loginPath } from "../utils/auth/next.ts";
|
||||||
|
|
||||||
/** Passed to {@link Index} by the router, from the path. */
|
/** Passed to {@link Index} by the router, from the path. */
|
||||||
export interface IndexProps {
|
export interface IndexProps {
|
||||||
@@ -76,11 +77,7 @@ export function Index(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const defaultTab = useViewerDefaultFeedTab();
|
const defaultTab = useViewerDefaultFeedTab();
|
||||||
// A guest on "follow", from a shared link or after signing out, gets the
|
const tab: FeedTab = feedTab ?? defaultTab;
|
||||||
// default tab rather than an empty page.
|
|
||||||
const tab: FeedTab = feedTab && (feedTab !== "follow" || !!user)
|
|
||||||
? feedTab
|
|
||||||
: defaultTab;
|
|
||||||
|
|
||||||
const feedUrl = categorySlug
|
const feedUrl = categorySlug
|
||||||
? `${API_URL}/api/dumps?category=${encodeURIComponent(categorySlug)}`
|
? `${API_URL}/api/dumps?category=${encodeURIComponent(categorySlug)}`
|
||||||
@@ -181,6 +178,13 @@ export function Index(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (categoryNotFound) return <NotFound />;
|
if (categoryNotFound) return <NotFound />;
|
||||||
|
// The default tab is never "follow" for a guest, so only a link leads one
|
||||||
|
// here: a digest's, or the page they signed out on.
|
||||||
|
if (tab === "follow" && !user) {
|
||||||
|
return (
|
||||||
|
<Navigate to={loginPath(location.pathname + location.search)} replace />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="index-page">
|
<div className="index-page">
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
/**
|
/**
|
||||||
* Guards `App`'s routes that need a session, the dump editor and
|
* Guards `App`'s routes that need a session: the dump editor,
|
||||||
* `/notifications`.
|
* `/notifications`, `/community`, `/settings` and `/manage`. `Index` sends a
|
||||||
|
* guest on the "follow" tab to sign in the same way.
|
||||||
*
|
*
|
||||||
* @module
|
* @module
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Navigate } from "react-router";
|
import { Navigate, useLocation } from "react-router";
|
||||||
|
|
||||||
import { useAuth } from "../hooks/auth/useAuth.ts";
|
import { useAuth } from "../hooks/auth/useAuth.ts";
|
||||||
|
import { loginPath } from "../utils/auth/next.ts";
|
||||||
|
|
||||||
/** Passed to {@link RestrictedLoggedIn} by `App`, around a route's page. */
|
/** Passed to {@link RestrictedLoggedIn} by `App`, around a route's page. */
|
||||||
export interface RestrictedLoggedInProps {
|
export interface RestrictedLoggedInProps {
|
||||||
@@ -15,14 +17,17 @@ export interface RestrictedLoggedInProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders `children` when someone is signed in, and redirects to `/login`
|
* Renders `children` when someone is signed in, and otherwise replaces the
|
||||||
* otherwise. The page it came from is not remembered, so signing in lands on
|
* page with `/login`, which comes back to it once signed in: a digest's link
|
||||||
* `/`.
|
* to `/notifications` opened signed out lands there after all.
|
||||||
*/
|
*/
|
||||||
export function RestrictedLoggedIn(
|
export function RestrictedLoggedIn(
|
||||||
{ children }: RestrictedLoggedInProps,
|
{ children }: RestrictedLoggedInProps,
|
||||||
): React.JSX.Element {
|
): React.JSX.Element {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
return user ? <>{children}</> : <Navigate to="/login" />;
|
return user
|
||||||
|
? <>{children}</>
|
||||||
|
: <Navigate to={loginPath(location.pathname + location.search)} replace />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -249,10 +249,10 @@ describe("Index feeds", () => {
|
|||||||
.toBeInTheDocument();
|
.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("a guest on the follow tab gets the default feed, not a blank page", async () => {
|
test("a guest on the follow tab is sent to sign in, and back", () => {
|
||||||
route("GET /api/dumps", () => page([older]));
|
route("GET /api/dumps", () => page([older]));
|
||||||
setup("/follow");
|
setup("/music/follow");
|
||||||
await screen.findByText("Old but loved");
|
expect(currentPath()).toBe("/login?next=%2Fmusic%2Ffollow");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("the Android share sheet opens the new-dump form with the link", async () => {
|
test("the Android share sheet opens the new-dump form with the link", async () => {
|
||||||
|
|||||||
@@ -3,21 +3,22 @@ import { screen } from "@testing-library/react";
|
|||||||
import { Route } from "react-router";
|
import { Route } from "react-router";
|
||||||
import { RestrictedLoggedIn } from "../../pages/RestrictedLoggedIn.tsx";
|
import { RestrictedLoggedIn } from "../../pages/RestrictedLoggedIn.tsx";
|
||||||
import { makeSession } from "../fixtures.ts";
|
import { makeSession } from "../fixtures.ts";
|
||||||
import { renderWithContexts } from "../render.tsx";
|
import { currentPath, renderWithContexts } from "../render.tsx";
|
||||||
|
|
||||||
describe("RestrictedLoggedIn", () => {
|
describe("RestrictedLoggedIn", () => {
|
||||||
test("sends guests to login", () => {
|
test("sends guests to login, which comes back to the page", () => {
|
||||||
renderWithContexts(
|
renderWithContexts(
|
||||||
<RestrictedLoggedIn>
|
<RestrictedLoggedIn>
|
||||||
<p>secret</p>
|
<p>secret</p>
|
||||||
</RestrictedLoggedIn>,
|
</RestrictedLoggedIn>,
|
||||||
{
|
{
|
||||||
route: "/notifications",
|
route: "/community/monthly?x=1",
|
||||||
path: "/notifications",
|
path: "/community/monthly",
|
||||||
extraRoutes: <Route path="/login" element={<p>login page</p>} />,
|
extraRoutes: <Route path="/login" element={<p>login page</p>} />,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(screen.getByText("login page")).toBeInTheDocument();
|
expect(screen.getByText("login page")).toBeInTheDocument();
|
||||||
|
expect(currentPath()).toBe("/login?next=%2Fcommunity%2Fmonthly%3Fx%3D1");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("lets members through", () => {
|
test("lets members through", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user