feat: add authorization

This commit is contained in:
2026-08-13 15:03:59 +03:00
parent 1e13adabd3
commit 3448fea140
28 changed files with 4504 additions and 3707 deletions
+34 -29
View File
@@ -1,7 +1,9 @@
import { MantineProvider } from "@mantine/core";
import { cleanup, render, screen } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import { MemoryRouter } from "react-router";
import { afterEach, describe, expect, it } from "vitest";
import { AuthProvider } from "../../features/auth";
import {
AccountsPageEmpty,
AccountsPageError,
@@ -12,43 +14,46 @@ import { AppRoutes } from "./AppRoutes";
describe("AppRoutes", () => {
afterEach(cleanup);
function renderRoutes(path: string) {
return render(
<QueryClientProvider client={new QueryClient()}>
<MemoryRouter initialEntries={[path]}>
<MantineProvider theme={theme}>
<AuthProvider>
<AppRoutes />
</AuthProvider>
</MantineProvider>
</MemoryRouter>
</QueryClientProvider>,
);
}
it.each([
["/", "Счета"],
["/", "Вход"],
["/login", "Вход"],
["/register", "Регистрация"],
["/accounts", "Счета"],
["/accounts/demo", "Счет"],
["/settings", "Настройки"],
["/accounts", "Вход"],
["/accounts/demo", "Вход"],
["/settings", "Вход"],
["/unknown", "Страница не найдена"],
])("renders %s with its page title", (path, title) => {
render(
<MemoryRouter initialEntries={[path]}>
<MantineProvider theme={theme}>
<AppRoutes />
</MantineProvider>
</MemoryRouter>,
);
])("renders %s with its page title", async (path, title) => {
renderRoutes(path);
expect(
screen.getByRole("heading", { level: 1, name: title }),
).toBeInTheDocument();
await waitFor(() =>
expect(
screen.getByRole("heading", { level: 1, name: title }),
).toBeInTheDocument(),
);
});
it("renders only the empty state for accounts by default", () => {
render(
<MemoryRouter initialEntries={["/accounts"]}>
<MantineProvider theme={theme}>
<AppRoutes />
</MantineProvider>
</MemoryRouter>,
);
it("redirects anonymous users from accounts", async () => {
renderRoutes("/accounts");
expect(
screen.getByRole("heading", { level: 2, name: "Пока ничего нет" }),
).toBeInTheDocument();
expect(screen.queryByText("Загрузка")).not.toBeInTheDocument();
expect(screen.queryByText("Что-то пошло не так")).not.toBeInTheDocument();
await waitFor(() =>
expect(
screen.getByRole("heading", { level: 1, name: "Вход" }),
).toBeInTheDocument(),
);
});
it.each([
+46 -5
View File
@@ -1,5 +1,5 @@
import { Navigate, Route, Routes } from "react-router";
import { Navigate, Route, Routes, useLocation } from "react-router";
import { useAuth } from "../../features/auth";
import { AccountPage } from "../../pages/account";
import { AccountsPage } from "../../pages/accounts";
import { LoginPage } from "../../pages/login";
@@ -8,13 +8,54 @@ import { RegisterPage } from "../../pages/register";
import { SettingsPage } from "../../pages/settings";
import { AppShell } from "../../shared/ui/AppShell";
function Protected({ children }: { children: React.ReactNode }) {
const { user, ready } = useAuth();
const location = useLocation();
if (!ready) return null;
return user ? (
children
) : (
<Navigate
to="/login"
state={{ returnTo: `${location.pathname}${location.search}` }}
replace
/>
);
}
function Public({ children }: { children: React.ReactNode }) {
const { user, ready } = useAuth();
if (!ready) return null;
return user ? <Navigate to="/accounts" replace /> : children;
}
export function AppRoutes() {
return (
<Routes>
<Route path="/" element={<Navigate to="/accounts" replace />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route element={<AppShell />}>
<Route
path="/login"
element={
<Public>
<LoginPage />
</Public>
}
/>
<Route
path="/register"
element={
<Public>
<RegisterPage />
</Public>
}
/>
<Route
element={
<Protected>
<AppShell />
</Protected>
}
>
<Route path="/accounts" element={<AccountsPage />} />
<Route path="/accounts/:accountId" element={<AccountPage />} />
<Route path="/settings" element={<SettingsPage />} />
+15
View File
@@ -32,6 +32,21 @@ a {
.bottom-nav {
display: none;
}
.auth-page {
align-items: center;
display: flex;
justify-content: center;
min-height: 100dvh;
padding: 1.5rem;
}
.auth-card {
box-shadow: 0 12px 32px rgba(30, 41, 59, 0.1);
width: min(100%, 26rem);
}
.auth-card fieldset {
border: 0;
padding: 0;
}
@media (max-width: 47.99375em) {
.bottom-nav {
background: var(--ft-color-bg-surface);
+4 -2
View File
@@ -1,7 +1,7 @@
import { MantineProvider } from "@mantine/core";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter } from "react-router";
import { AuthProvider } from "../../features/auth";
import { AppRoutes } from "../routes";
import { theme } from "../theme";
@@ -14,7 +14,9 @@ export function App() {
<MantineProvider theme={theme} forceColorScheme="light">
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<AppRoutes />
<AuthProvider>
<AppRoutes />
</AuthProvider>
</BrowserRouter>
</QueryClientProvider>
</MantineProvider>