Files
gitmost/apps/client/src/App.tsx
T
agent_coder 199a9a1750 feat(client): страница управления API-ключами в настройках аккаунта (#506)
Фаза 2 поверх серверных эндпоинтов #501: страница «Настройки → Аккаунт →
API-ключи» (lazy-роут, отдельный чанк) — список, создание, показ токена
один раз и отзыв.

- Список: явная дата истечения (не «через N дней»), подсветка ключей с
  истечением < 30 дней, «использован» с семантикой «в течение последнего
  часа» (last_used_at троттлится на 1 ч серверно).
- Создание: имя + срок (30д/90д/1 год[дефолт]/бессрочный → null). После
  submit — модалка показа токена один раз с копированием и явной датой.
- Токен-материал живёт ТОЛЬКО в state открытой модалки: mutateAsync +
  reset() чистит копию из query-cache, ничего не пишется в localStorage.
  Закрытие модалки — токен исчезает навсегда.
- Отзыв: подтверждение → revoke → строка уходит из списка.
- Admin (CASL Manage на API = owner/admin) видит ключи всего воркспейса с
  колонкой «автор»; обычный член — только свои.

Тесты (vitest): показ-один-раз + отсутствие токена в localStorage/кэше,
явная дата + подсветка < 30 дней, отзыв убирает строку, admin/member вид,
дефолт срока = 1 год и «бессрочный» → null. Стаб ResizeObserver добавлен в
общий vitest.setup для рендера Mantine ScrollArea/Table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 19:20:17 +03:00

130 lines
5.6 KiB
TypeScript

import { lazy, Suspense } from "react";
import { Navigate, Route, Routes } from "react-router-dom";
import { Center, Loader } from "@mantine/core";
import { Error404 } from "@/components/ui/error-404.tsx";
import Layout from "@/components/layouts/global/layout.tsx";
import { useTrackOrigin } from "@/hooks/use-track-origin";
// ShareLayout is route-split: its ShareShell chrome pulls in the table of
// contents (and thus TipTap), so keeping it out of the eager graph removes the
// editor engine from startup for authenticated users too.
const ShareLayout = lazy(
() => import("@/features/share/components/share-layout.tsx"),
);
// Auth / entry pages stay eager: they are the first paint for an unauthenticated
// visitor (e.g. /login) and are already small, so code-splitting them would only
// add a cold-chunk round trip to the most common cold-start path.
import SetupWorkspace from "@/pages/auth/setup-workspace.tsx";
import LoginPage from "@/pages/auth/login";
import InviteSignup from "@/pages/auth/invite-signup.tsx";
import ForgotPassword from "@/pages/auth/forgot-password.tsx";
import PasswordReset from "./pages/auth/password-reset";
import PageRedirect from "@/pages/page/page-redirect.tsx";
import ShareRedirect from "@/pages/share/share-redirect.tsx";
// Heavy / leaf pages are route-split with React.lazy so their code (most
// importantly the whole TipTap editor + KaTeX + lowlight grammars + drawio that
// the page editor and the readonly share editor pull in) is fetched only when
// the matching route is actually visited. The <Suspense> boundaries live inside
// each Layout (around its <Outlet/>), so the app shell stays mounted while a
// route chunk loads.
const Home = lazy(() => import("@/pages/dashboard/home"));
const Page = lazy(() => import("@/pages/page/page"));
const SpaceHome = lazy(() => import("@/pages/space/space-home.tsx"));
const SpaceTrash = lazy(() => import("@/pages/space/space-trash.tsx"));
const SpacesPage = lazy(() => import("@/pages/spaces/spaces.tsx"));
const FavoritesPage = lazy(() => import("@/pages/favorites/favorites-page"));
const LabelPage = lazy(() => import("@/pages/label/label-page"));
const SharedPage = lazy(() => import("@/pages/share/shared-page.tsx"));
const AccountSettings = lazy(
() => import("@/pages/settings/account/account-settings"),
);
const AccountPreferences = lazy(
() => import("@/pages/settings/account/account-preferences.tsx"),
);
// #506 — lazy leaf (own chunk): the API-keys management page is route-split so
// its code (Mantine table/modals + the create/revoke flow) stays out of the
// entry bundle (post-#342 bundle discipline).
const AccountApiKeys = lazy(
() => import("@/pages/settings/account/account-api-keys.tsx"),
);
const WorkspaceSettings = lazy(
() => import("@/pages/settings/workspace/workspace-settings"),
);
const AiSettings = lazy(() => import("@/pages/settings/workspace/ai-settings"));
const WorkspaceMembers = lazy(
() => import("@/pages/settings/workspace/workspace-members"),
);
const Groups = lazy(() => import("@/pages/settings/group/groups"));
const GroupInfo = lazy(() => import("./pages/settings/group/group-info"));
const Spaces = lazy(() => import("@/pages/settings/space/spaces.tsx"));
const Shares = lazy(() => import("@/pages/settings/shares/shares.tsx"));
export default function App() {
useTrackOrigin();
return (
<Suspense
fallback={
<Center h="100vh">
<Loader size="sm" />
</Center>
}
>
<Routes>
<Route index element={<Navigate to="/home" />} />
<Route path={"/login"} element={<LoginPage />} />
<Route path={"/invites/:invitationId"} element={<InviteSignup />} />
<Route path={"/forgot-password"} element={<ForgotPassword />} />
<Route path={"/password-reset"} element={<PasswordReset />} />
<Route path={"/setup/register"} element={<SetupWorkspace />} />
<Route element={<ShareLayout />}>
<Route
path={"/share/:shareId/p/:pageSlug"}
element={<SharedPage />}
/>
<Route path={"/share/p/:pageSlug"} element={<SharedPage />} />
</Route>
<Route path={"/share/:shareId"} element={<ShareRedirect />} />
<Route path={"/p/:pageSlug"} element={<PageRedirect />} />
<Route element={<Layout />}>
<Route path={"/home"} element={<Home />} />
<Route path={"/spaces"} element={<SpacesPage />} />
<Route path={"/favorites"} element={<FavoritesPage />} />
<Route path={"/labels/:labelName"} element={<LabelPage />} />
<Route path={"/s/:spaceSlug"} element={<SpaceHome />} />
<Route path={"/s/:spaceSlug/trash"} element={<SpaceTrash />} />
<Route
path={"/s/:spaceSlug/p/:pageSlug"}
element={<Page />}
/>
<Route path={"/settings"}>
<Route path={"account/profile"} element={<AccountSettings />} />
<Route
path={"account/preferences"}
element={<AccountPreferences />}
/>
<Route path={"account/api-keys"} element={<AccountApiKeys />} />
<Route path={"workspace"} element={<WorkspaceSettings />} />
<Route path={"ai"} element={<AiSettings />} />
<Route path={"members"} element={<WorkspaceMembers />} />
<Route path={"groups"} element={<Groups />} />
<Route path={"groups/:groupId"} element={<GroupInfo />} />
<Route path={"spaces"} element={<Spaces />} />
<Route path={"sharing"} element={<Shares />} />
</Route>
</Route>
<Route path="*" element={<Error404 />} />
</Routes>
</Suspense>
);
}