0.29.1原版

This commit is contained in:
anian
2026-07-02 19:14:14 +08:00
commit d94008f0fb
947 changed files with 174905 additions and 0 deletions
+67
View File
@@ -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 />;
};
+121
View File
@@ -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;
+15
View File
@@ -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];