0.29.1原版
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { Navigate, Outlet, useLocation, useSearchParams } from "react-router-dom";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
import { AUTH_REDIRECT_PARAM, buildAuthRoute, getSafeRedirectPath } from "@/utils/auth-redirect";
|
||||
import { ROUTES } from "./routes";
|
||||
|
||||
/**
|
||||
* Index-route gate mounted at `/`. Authenticated visitors fall through to the
|
||||
* nested Home page; unauthenticated visitors are redirected to `/explore`,
|
||||
* preserving the original query string and hash so bookmarks like `/?filter=foo`
|
||||
* keep working.
|
||||
*/
|
||||
export const LandingRoute = () => {
|
||||
const currentUser = useCurrentUser();
|
||||
const location = useLocation();
|
||||
|
||||
if (!currentUser) {
|
||||
return (
|
||||
<Navigate
|
||||
to={{
|
||||
pathname: ROUTES.EXPLORE,
|
||||
search: location.search,
|
||||
hash: location.hash,
|
||||
}}
|
||||
replace
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <Outlet />;
|
||||
};
|
||||
|
||||
/**
|
||||
* Guard for routes that require an authenticated user. Unauthenticated visitors
|
||||
* are redirected to `/auth` with the original location preserved as the `redirect`
|
||||
* query parameter, so they return to the intended page after signing in.
|
||||
*/
|
||||
export const RequireAuthRoute = () => {
|
||||
const currentUser = useCurrentUser();
|
||||
const location = useLocation();
|
||||
|
||||
if (!currentUser) {
|
||||
const redirect = `${location.pathname}${location.search}${location.hash}`;
|
||||
return <Navigate to={buildAuthRoute({ redirect })} replace />;
|
||||
}
|
||||
|
||||
return <Outlet />;
|
||||
};
|
||||
|
||||
/**
|
||||
* Guard for guest-only routes (sign-in and sign-up). Already-authenticated users
|
||||
* are redirected to the requested `redirect` target (when safe) or to `/`.
|
||||
*
|
||||
* The OAuth callback route (`/auth/callback`) intentionally opts out of this guard:
|
||||
* an authenticated session in another tab must not prevent the callback from
|
||||
* consuming its one-time OAuth state and completing the in-flight sign-in.
|
||||
*/
|
||||
export const RequireGuestRoute = () => {
|
||||
const currentUser = useCurrentUser();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
if (currentUser) {
|
||||
const redirectTarget = getSafeRedirectPath(searchParams.get(AUTH_REDIRECT_PARAM));
|
||||
return <Navigate to={redirectTarget || ROUTES.HOME} replace />;
|
||||
}
|
||||
|
||||
return <Outlet />;
|
||||
};
|
||||
@@ -0,0 +1,121 @@
|
||||
import { lazy } from "react";
|
||||
import { createBrowserRouter, Navigate, type RouteObject } from "react-router-dom";
|
||||
import App from "@/App";
|
||||
import { ChunkLoadErrorFallback } from "@/components/ErrorBoundary";
|
||||
import MainLayout from "@/layouts/MainLayout";
|
||||
import RootLayout from "@/layouts/RootLayout";
|
||||
import { LandingRoute, RequireAuthRoute, RequireGuestRoute } from "./guards";
|
||||
import { ROUTES } from "./routes";
|
||||
|
||||
// Wrap lazy imports to auto-reload on chunk load failure (e.g., after redeployment).
|
||||
function lazyWithReload<T extends React.ComponentType>(factory: () => Promise<{ default: T }>) {
|
||||
return lazy(() =>
|
||||
factory().catch((error) => {
|
||||
const isChunkError =
|
||||
error?.message?.includes("Failed to fetch dynamically imported module") ||
|
||||
error?.message?.includes("Importing a module script failed");
|
||||
const reloadKey = "chunk-reload";
|
||||
if (isChunkError && !sessionStorage.getItem(reloadKey)) {
|
||||
sessionStorage.setItem(reloadKey, "1");
|
||||
window.location.reload();
|
||||
}
|
||||
throw error;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const AdminSignIn = lazyWithReload(() => import("@/pages/AdminSignIn"));
|
||||
const About = lazyWithReload(() => import("@/pages/About"));
|
||||
const Archived = lazyWithReload(() => import("@/pages/Archived"));
|
||||
const AuthCallback = lazyWithReload(() => import("@/pages/AuthCallback"));
|
||||
const Explore = lazyWithReload(() => import("@/pages/Explore"));
|
||||
const Home = lazyWithReload(() => import("@/pages/Home"));
|
||||
const Inboxes = lazyWithReload(() => import("@/pages/Inboxes"));
|
||||
const MemoDetail = lazyWithReload(() => import("@/pages/MemoDetail"));
|
||||
const NotFound = lazyWithReload(() => import("@/pages/NotFound"));
|
||||
const PermissionDenied = lazyWithReload(() => import("@/pages/PermissionDenied"));
|
||||
const Attachments = lazyWithReload(() => import("@/pages/Attachments"));
|
||||
const Setting = lazyWithReload(() => import("@/pages/Setting"));
|
||||
const Shortcuts = lazyWithReload(() => import("@/pages/Shortcuts"));
|
||||
const SignIn = lazyWithReload(() => import("@/pages/SignIn"));
|
||||
const SignUp = lazyWithReload(() => import("@/pages/SignUp"));
|
||||
const UserProfile = lazyWithReload(() => import("@/pages/UserProfile"));
|
||||
|
||||
// Backward compatibility alias.
|
||||
export const Routes = ROUTES;
|
||||
export { ROUTES };
|
||||
|
||||
/**
|
||||
* Static route configuration. Exported so tests can assert on the tree shape
|
||||
* (e.g. that `/auth/callback` stays outside the guest-only guard subtree) and
|
||||
* so integration tests can drive a `createMemoryRouter` over the same tree.
|
||||
*/
|
||||
export const routeConfig: RouteObject[] = [
|
||||
{
|
||||
path: "/",
|
||||
element: <App />,
|
||||
errorElement: <ChunkLoadErrorFallback />,
|
||||
children: [
|
||||
{
|
||||
path: Routes.AUTH,
|
||||
children: [
|
||||
// The OAuth callback must run regardless of the current session — an
|
||||
// authenticated tab elsewhere must not block it from consuming its
|
||||
// one-time OAuth state. Keep it outside the guest-only subtree.
|
||||
{ path: "callback", element: <AuthCallback /> },
|
||||
{
|
||||
element: <RequireGuestRoute />,
|
||||
children: [
|
||||
{ path: "", element: <SignIn /> },
|
||||
{ path: "admin", element: <AdminSignIn /> },
|
||||
{ path: "signup", element: <SignUp /> },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
// Backward compatibility: the old `/home` URL now lives at `/`.
|
||||
{ path: "home", element: <Navigate to={Routes.HOME} replace /> },
|
||||
{
|
||||
element: <RootLayout />,
|
||||
children: [
|
||||
{
|
||||
element: <MainLayout />,
|
||||
children: [
|
||||
{
|
||||
element: <LandingRoute />,
|
||||
children: [{ index: true, element: <Home /> }],
|
||||
},
|
||||
{ path: Routes.ABOUT, element: <About /> },
|
||||
{ path: Routes.EXPLORE, element: <Explore /> },
|
||||
{ path: "u/:username", element: <UserProfile /> },
|
||||
{
|
||||
element: <RequireAuthRoute />,
|
||||
children: [
|
||||
{ path: Routes.ARCHIVED, element: <Archived /> },
|
||||
{ path: Routes.SHORTCUTS, element: <Shortcuts /> },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ path: "memos/:uid", element: <MemoDetail /> },
|
||||
{ path: "memos/shares/:token", element: <MemoDetail /> },
|
||||
{
|
||||
element: <RequireAuthRoute />,
|
||||
children: [
|
||||
{ path: Routes.ATTACHMENTS, element: <Attachments /> },
|
||||
{ path: Routes.INBOX, element: <Inboxes /> },
|
||||
{ path: Routes.SETTING, element: <Setting /> },
|
||||
],
|
||||
},
|
||||
{ path: "403", element: <PermissionDenied /> },
|
||||
{ path: "404", element: <NotFound /> },
|
||||
{ path: "*", element: <NotFound /> },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const router = createBrowserRouter(routeConfig);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,15 @@
|
||||
export const ROUTES = {
|
||||
HOME: "/",
|
||||
ABOUT: "/about",
|
||||
ATTACHMENTS: "/attachments",
|
||||
INBOX: "/inbox",
|
||||
ARCHIVED: "/archived",
|
||||
SHORTCUTS: "/shortcuts",
|
||||
SETTING: "/setting",
|
||||
EXPLORE: "/explore",
|
||||
AUTH: "/auth",
|
||||
SHARED_MEMO: "/memos/shares",
|
||||
} as const;
|
||||
|
||||
export type RouteKey = keyof typeof ROUTES;
|
||||
export type RoutePath = (typeof ROUTES)[RouteKey];
|
||||
Reference in New Issue
Block a user