Files
PnvPanel/docs/frontend.md
T
Leonid Pershin 24d9ea1099
CI / Backend (build + test) (push) Successful in 1m14s
CI / Frontend (lint + typecheck + build) (push) Successful in 30s
Implement role and user management enhancements
- Added MaxIpLimit to roles, allowing for the configuration of simultaneous IP limits for users.
- Updated role creation and update commands to include MaxIpLimit, ensuring proper handling in the application logic.
- Enhanced user management by introducing a DELETE endpoint for user accounts, with appropriate checks to prevent self-deletion.
- Updated documentation to reflect changes in role and user management, clarifying the new IP limit functionality and user deletion process.
- Adjusted related tests to cover new functionality and ensure robust validation of role and user management features.
2026-07-13 07:18:13 +03:00

157 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Frontend
SPA на **React 19 + Vite + TypeScript**. Общается с бэком по REST (JWT Bearer) и получает
живые обновления по SignalR.
> **Раздача из единого контейнера.** В проде собранный фронт (`dist/`) кладётся в `wwwroot`
> ASP.NET Core и раздаётся тем же приложением (SPA-fallback на `index.html`). Фронт и бек — один
> origin, база API — относительный `/api`, SignalR — `/hubs/panel`. В dev Vite-сервер проксирует
> `/api` и `/hubs` на бэкенд (`vite.config.ts`, цель — `http://localhost:8080` по умолчанию,
> переопределяется `VITE_API_TARGET`). Детали упаковки — [architecture.md](architecture.md#развёртывание-единый-контейнер-приложения).
## Стек
| Задача | Выбор |
| ----------------- | ------------------------------------------------------------ |
| Сборка/dev | Vite (Rolldown-based) |
| Язык | TypeScript (strict) |
| Данные с сервера | TanStack Query |
| Роутинг | TanStack Router (файловый, `src/routes/`, кодогенерация `routeTree.gen.ts`) |
| UI-компоненты | shadcn-стиль поверх Radix (`@radix-ui/react-*`) + Tailwind CSS v4 |
| Иконки | lucide-react |
| Клиентский стейт | Zustand — только auth-стор (`features/auth/store.ts`); тема — React Context + localStorage, не Zustand |
| Формы | react-hook-form + zod |
| Realtime | @microsoft/signalr |
| QR-коды | qrcode.react (рендерит QR из готовой строки на клиенте) |
| Markdown | react-markdown + remark-gfm (лента новостей; без rehype-raw — сырой HTML не рендерится) |
| Типы API | openapi-typescript (`pnpm gen:api`) — генерирует `schema.gen.ts` для сверки; фичи импортируют руками написанный `shared/api/types.ts` |
| i18n | react-i18next (RU + EN) |
| Линт | oxlint (не ESLint) |
| Пакетный менеджер | pnpm |
Установлены, но не используются: `recharts` (админская статистика — карточки с цифрами, без
графиков), `@tanstack/react-table` (админские таблицы написаны руками, без TanStack Table).
## Структура
Фактическая структура (`frontend/src/`):
```
frontend/
src/
main.tsx # точка входа: QueryClientProvider > ThemeProvider > ToastProvider > RealtimeProvider > RouterProvider
router.tsx # createRouter из routeTree.gen.ts
routeTree.gen.ts # сгенерировано @tanstack/router-plugin, не редактируется руками
index.css # Tailwind v4 (@import), без отдельной styles/-папки
routes/ # файловый роутинг TanStack Router
__root.tsx # шапка (лого, нав, переключатель языка/темы), Outlet
index.tsx, login.tsx, register.tsx, dashboard.tsx, instructions.tsx, news.tsx, settings.tsx
admin.tsx # layout админки (вкладки) + Outlet
admin/
index.tsx, activation.tsx, users.tsx, roles.tsx, nodes.tsx, apps.tsx, news.tsx, audit.tsx
features/
auth/ # api.ts, store.ts (zustand), guards.ts, LoginForm.tsx, RegisterForm.tsx
activation/ # api.ts, ActivationGate.tsx (экран "запросить активацию")
configs/ # api.ts, ConfigCard.tsx, CreateConfigDialog.tsx, SubscriptionCard.tsx
apps/ # api.ts, AppsCatalog.tsx (для /instructions)
news/ # api.ts, NewsFeed.tsx (для /news)
telegram/ # api.ts, TelegramLoginButton.tsx
settings/ # ChangePasswordForm.tsx, TelegramLinkCard.tsx, DeleteAccountSection.tsx
admin/
users/, roles/, activation/, nodes/, inbounds/, apps/, news/, audit/, stats/ # api.ts + диалоги CRUD в каждой
theme/
ThemeProvider.tsx # React Context + localStorage (`pnv-theme`), НЕ zustand
shared/
api/
client.ts # apiRequest(), HttpError, access-token в памяти модуля, 401 → silent refresh → повтор
types.ts # руками написанные типы ответов/запросов (сверены со schema.gen.ts)
schema.gen.ts # генерируется pnpm gen:api, не импортируется напрямую фичами
lib/
i18n.ts, cn.ts, format.ts
realtime/
connection.ts, RealtimeProvider.tsx
ui/
button.tsx, input.tsx, textarea.tsx, label.tsx, card.tsx, dialog.tsx, select.tsx, badge.tsx,
progress.tsx, toast-store.tsx, toaster.tsx
index.html
vite.config.ts
package.json
```
Нет `app/`-папки с провайдерами (они прямо в `main.tsx`), нет `shared/config/` (переменные окружения
читаются точечно через `import.meta.env`), нет `styles/` (один `index.css` с Tailwind).
## Дизайн / UX
- **Тема оформления**: светлая/тёмная/системная (переключатель в шапке). Реализация — класс `dark` на
`<html>` + Tailwind, выбор в `localStorage` (`ThemeProvider`, React Context).
- **Дашборд пользователя** (`/dashboard`): карточки конфигов (протокол, локация, использованный
трафик, статус), кнопки на карточке — показать ссылку/QR (запрашивает `GET .../link` по клику,
не сразу при создании), перевыпустить, отозвать; отдельная карточка «Общая подписка». Для
неактивированного — `ActivationGate` вместо дашборда.
- **Создание конфига**: диалог — выбор инбаунда (по `displayName`) + метка. Лимит одновременных IP
(`limitIp` в 3x-ui) выставляется автоматически по квоте роли пользователя (`AppRole.MaxIpLimit`) —
в форме создания не настраивается. После успеха карточка конфига появляется в списке; ссылку/QR
пользователь открывает отдельно.
- **Страница инструкций** (`/instructions`): статичные шаги + каталог приложений (`GET /api/apps`),
сгруппированный по ОС и показан вкладками (по одной ОС за раз); клик по приложению открывает
ссылку на скачивание.
- **Лента новостей** (`/news`): хронологический список постов админа (заголовок + Markdown-тело,
рендерится через `react-markdown` + `remark-gfm`), пагинация (`GET /api/news`), живое обновление
по SignalR (`newsPublished`, широковещательно всем). Админка (`/admin/news`): CRUD, обычный
`<textarea>` с переключателем предпросмотра Markdown вместо WYSIWYG-редактора.
- **Настройки** (`/settings`): смена пароля, привязка/отвязка Telegram (`TelegramLinkCard`),
удаление аккаунта с подтверждением (`DeleteAccountSection`).
- **Админка** (`/admin/*`): вкладки — обзор (карточки статистики, без графиков), запросы активации,
пользователи, роли, ноды (+ публикация инбаундов), приложения, аудит. Таблицы — обычные `<table>`,
без TanStack Table. Блокировка пользователя — с подтверждением.
- **Состояния**: `isLoading`/`isError`/пусто различаются явно везде (ошибка сети не выглядит как
«пусто» — паттерн закреплён после находки в `ActivationGate`, распространён на все admin-списки).
## Работа с API
- `shared/api/client.ts`: `apiRequest<T>()` оборачивает `fetch`, подставляет access-token в
`Authorization`; на `401` — один прозрачный `POST /api/auth/refresh` и повтор запроса; при неуспехе
рефреша — `setUnauthorizedHandler` колбэк (разлогин).
- Запросы/мутации — через TanStack Query, ключи по фиче, инвалидация после мутаций.
- Типы — из `shared/api/types.ts` (см. таблицу стека выше и [tech-stack.md](tech-stack.md)).
## Авторизация на клиенте
- **Access-token** — в памяти (модуль `client.ts`, не React state и не localStorage) — переживает
ре-рендеры, но не пережить reload (тогда его молча восстанавливает silent refresh по cookie).
- **Refresh-token** — httpOnly Secure cookie (`pnv_refresh_token`, `Path=/api/auth`), JS его не видит.
- Стор авторизации (`features/auth/store.ts`, Zustand) хранит текущего пользователя/`isActivated`.
Guard-хуки `useRequireAuth()`/`useRequireGuest()`/`useRequireAdmin()` (`features/auth/guards.ts`)
редиректят через `useNavigate()` в `useEffect`, если условие не выполнено.
- **Вход — по username** (email не используется). «Забыли пароль?»: при привязанном Telegram —
вход через бота и смена пароля в настройках; иначе — обратиться к админу.
### Вход и привязка через Telegram
- **«Войти через Telegram»** (`TelegramLoginButton`, на `/login`): `POST .../login-request`
показывает `deepLink` как QR (`qrcode.react`), поллит `GET .../login-request/{id}` до
`Approved`/`Rejected`/`Expired`. При `Approved` — сохраняет `accessToken` в память клиента
(refresh уже пришёл в cookie от бэка) и редиректит в панель.
- **«Привязать Telegram»** (`TelegramLinkCard`, в настройках): `POST .../link-token` → QR из
`deepLink`; статус обновляется поллингом. Отвязка — `POST .../unlink`.
## Realtime
- Одно SignalR-подключение к `/hubs/panel` с JWT (`RealtimeProvider`, `shared/realtime/connection.ts`).
- Обработчики `configTrafficUpdated`/`configStatusChanged`/`nodeStatusChanged`/`activationRequested`/
`userActivated`/`newsPublished` точечно инвалидируют/обновляют кэш TanStack Query — UI обновляется
без перезагрузки.
## Скрипты
```bash
pnpm dev # dev-сервер Vite
pnpm build # tsc -b && vite build (прод-сборка)
pnpm preview # предпросмотр prod-сборки
pnpm lint # oxlint
pnpm typecheck # tsc -b
pnpm gen:api # openapi-typescript по /openapi/v1.json живого бэкенда -> schema.gen.ts
```
`pnpm gen:api` требует запущенный бэкенд на `http://localhost:8080` (или `docker compose up`).