Фронтенд
packages/frontend — односторінковий застосунок (SPA) на Vue 3 + Vite, який у production віддає сам бекенд. Ця сторінка — огляд. Деталі окремих підсистем описані на сторінках бекенду, і тут на них лише посилання.
Стек
Залежності взято з packages/frontend/package.json:
| Призначення | Пакет |
|---|---|
| UI-фреймворк | vue 3.5, @vueuse/core |
| Збірка / dev | vite 7, @vitejs/plugin-vue, vite-plugin-vue-devtools |
| Стан | pinia |
| Роутинг | vue-router 5 |
| i18n | vue-i18n 11 |
| Моніторинг помилок | @sentry/vue |
| PWA install prompt | @khmyznikov/pwa-install |
| Інше | mitt (event bus), dompurify, vue-advanced-cropper (аватар) |
| Спільні контракти | shared (workspace:*) |
| Тести | vitest, @vue/test-utils, jsdom, @playwright/test |
| Типи | typescript 6.0.3, vue-tsc |
Dev-сервер і проксі
vite.config.ts: порт 5173, проксі /api → http://127.0.0.1:3000 і /ws → ws://127.0.0.1:3000 (ws: true). Аліас @ → src/. Тег pwa-install оголошено як custom element. Через define у бандл потрапляє BUILD_TIMESTAMP, за яким порівнюються версії застосунку і Service Worker.
Як запускати разом із бекендом: /guide/getting-started, команди: /reference/scripts.
Змінні оточення
Тип ImportMetaEnv в src/env.d.ts описує всі VITE_*, які читає застосунок (типи — рядки збірки). Від звертання до неоголошеної змінної це не захищає: vite/client оголошує в ImportMetaEnv індексну сигнатуру. Змінні:
| Змінна | Де використовується |
|---|---|
VITE_BASE_URL | префікс для всіх HTTP-запитів у utils/base-api.ts |
VITE_API_PATH_PREFIX | префікс назв WS-подій (<prefix>/<route>) у utils/ws-event.ts; має збігатися з бекендовим API_PATH_PREFIX, за замовчуванням api |
VITE_AUTH_PHONE_ENABLED | 'true' вмикає вхід і реєстрацію за телефоном у AuthModals.vue |
VITE_SENTRY_ENABLED, VITE_SENTRY_DSN, VITE_SENTRY_ENVIRONMENT, VITE_SENTRY_RELEASE, VITE_SENTRY_*_SAMPLE_RATE | конфігурація Sentry, див. /backend/observability |
Без VITE_BASE_URL (і з порожнім значенням, як у .env.example) base-api.ts робить запити відносно поточного origin: у dev — через Vite-проксі, у production — до бекенду, що віддає SPA. Задавайте змінну, лише коли API живе на іншому origin (його має дозволяти CORS бекенду).
Структура src/
| Каталог / файл | Вміст |
|---|---|
main.ts | bootstrap: локаль → createApp → Pinia, router, i18n → Sentry → mount; запуск SW через startServiceWorker |
App.vue | initializeApp(): виклик GET /api/main/init, збереження користувача, відкриття WebSocket |
views/ | сторінки: Login, UserAccount, SupportView, VerifyEmailView, ResetPasswordView, юридичні сторінки, admin/ |
components/ | AppHeader, AuthModals (рамка й вибір кроку; кроки — у auth/: LoginForm, ForgotPasswordFlow, RegisterEmailForm, RegisterPhoneFlow, AuthModalShell; стилі форм — assets/auth-forms.css), MenuButton (меню; пункт «Install App» — PwaInstallButton), ConnectionStatus, UpdateNotification, ToastContainer, AvatarEditorModal, admin/ (AdminUserDetailModal, AdminFilterBar — карточка фільтрів, спільна для списків користувачів і історії онлайну) |
composables/ | useWebSocketConnection, useBroadcastHandler, useSessionLifecycle, useLocale, useToast, useAdminUserDetailModal, useAuthRequest, useCompleteSignIn |
stores/ | Pinia: user, state (тема, шрифт, режим PWA, розміри вікна, конфіг сховища), push (дозвіл і підписка на push), toast, support (і демо counter) |
router/index.ts | маршрути та guard |
plugins/i18n.ts | інстанс vue-i18n |
locales/ | файли перекладів |
utils/ | HTTP-клієнт (base-api.ts, api.ts), csrf-token.ts, WebSocket (websocket-types.ts, ws-event.ts, websocket-close.ts, websocket-heartbeat.ts), Sentry, pwa-install.ts, event-bus.ts |
types/ | типи, зокрема i18n.d.ts |
service-worker.ts | Service Worker, збирається окремо |
Загальна структура монорепозиторію: /guide/project-structure.
Роутер і guard
У маршрутах є meta (requiresAuth, requiresAdmin, hideHeader, authModal тощо, типи розширено в env.d.ts). router.beforeEach для захищених маршрутів чекає waitForApp() зі стору user, тобто завершення initializeApp. Публічні сторінки (вхід, верифікація email, скидання пароля) не чекають ініціалізації. Без користувача guard перенаправляє на /login?redirect=<шлях>. Після входу redirect береться лише якщо це шлях усередині застосунку (/…, без //, зворотних слешів і керувальних символів, safe-redirect.ts); інакше користувач потрапляє на /account. Маршрут requiresAdmin для не-адміна перенаправляє на /account.
i18n
Інтерфейс перекладено трьома мовами: en, uk, pl (список — SHIPPED_LOCALE_CODES пакета shared, по файлу в src/locales/; тест locales.spec.ts стежить, щоб файлів було рівно стільки і щоб uk та pl мали всі ключі en з тими самими плейсхолдерами). APP_LOCALE_CODES (18 кодів) ширший: ним користується розпізнавання мови й Support AI, який відповідає і мовами без перекладу інтерфейсу. В основний бандл входить тільки en. uk і pl loadLocale() підвантажує динамічним import() і зливає з en, тож відсутні ключі показуються англійською; якщо файл не завантажився, loadLocale() повертає en і пише попередження, тож вибір мови не залишає інтерфейс без тексту. Початкову мову initLocaleStandalone() у composables/useLocale.ts визначає за navigator.language; якщо мови немає серед трьох, бере en (браузер ru-RU чи de-DE отримує англійський інтерфейс). Support AI фронтенд просить відповідати мовою браузера (supportLanguage()), а не мовою інтерфейсу. Нову мову додають файлом у src/locales/ і кодом у SHIPPED_LOCALE_CODES.
HTTP-клієнт, сесія і CSRF
utils/base-api.ts обгортає fetch, а utils/api.ts містить типізовані методи (/api/auth/*, /api/main/*, /api/push-subscriptions*).
- Усі запити йдуть з
credentials: 'include', тож браузер сам надсилає сесійну cookie. JS її не читає. - CSRF-токен не вбудовується в HTML. Він приходить у полі
csrfTokenвідповіді (зокремаinit) і зберігається в пам'яті модуляutils/csrf-token.ts. Токен перевіряється regex^[A-Za-z0-9_-]{32,128}$і оновлюється з кожної відповіді, у якій є нове значення. - Для
POST/PUT/PATCH/DELETEклієнт додає заголовокX-CSRF-Token. - Коли сервер повертає
csrf_invalid, клієнт один раз емітитьinit_app, щоб повторно ініціалізуватися й отримати новий токен. Неавторизовану відповідь клієнт перетворює на подіюunauthorizedв event bus. - Помилки не кидаються: метод повертає
{ data, error }, деerrorмає нормалізованіcodeіtransportCode. Нормалізація живе вutils/response-normalizer.ts.
Серверна сторона цього механізму описана в Безпека → CSRF.
WebSocket і push
- Після успішного
initApp.vueвідкриває WebSocket зdata.wsUrlіdata.wsToken. Клієнт (useWebSocketConnection,ws-event.ts, heartbeat, broadcast-обробник) описано в /backend/websocket. Префікс задаєVITE_API_PATH_PREFIX(за замовчуваннямapi), він має збігатися з бекендовимAPI_PATH_PREFIX. З нього будуються і WS-події, і всі HTTP-шляхи (utils/api-path.ts:apiPath('auth/login')→/<prefix>/auth/login), OAuth-посилання, URL скриншотів і сховища, фільтр API-запитів у service worker і ключ Vite proxy (/<prefix>зws: trueпокриває і WS-ендпоінт/<prefix>/websocket). - Підписка на Web Push працює через
/api/push-subscriptions*(VAPID-ключ, CRUD). Як SW обробляє push, описано в Web Push сповіщення.
PWA
- Service Worker — окрема збірка
vite.sw.config.ts: lib-режим, форматiife, результатdist/service-worker.js,emptyOutDir: false, щоб не стерти бандл застосунку.utils/sw-manager.ts(startServiceWorker, його викликаєmain.ts) реєструє його тільки в production (scope: '/',updateViaCache: 'none') після подіїload. У dev уже встановлені SW знімаються з реєстрації. - Оновлення.
sw-manager.tsпорівнюєBUILD_TIMESTAMPзастосунку і SW (черезMessageChannel,GET_BUILD_TIMESTAMP). Коли різниця більша за 10 хвилин і це не перше встановлення, він викликаєannounceUpdate(main.tsемітитьupdate_available) — не більше одного разу, хоч би скільки сигналів прийшло, іUpdateNotificationпоказує повідомлення. Помилки SW (SW_ERROR) пересилаються в Sentry. - Встановлення. Веб-компонент
@khmyznikov/pwa-installвикористовується вMenuButton.vue(логіка — composableusePwaInstallButton, сам пункт —PwaInstallButton.vue), а станbeforeinstallpromptзберігаєutils/pwa-install.ts(initializePwaInstallTracking()викликається на старті). - Офлайн і кешування в SW: /backend/caching-etag.
Збірка і тести
pnpm --filter frontend build запускає паралельно vue-tsc --build і vite build, а потім build-sw. Результат лежить у packages/frontend/dist. У Docker-образі Dockerfile копіює dist у packages/backend/public, звідки його віддає бекенд. Подробиці: /backend/deployment.
У production-збірці (застосунок і Service Worker) виклики console.log, console.info, console.debug і console.trace вирізаються мінифікатором: vite.console.ts передає їх у esbuild.pure. Так у консоль браузера не потрапляють токени й дані користувача, зокрема зі сторонніх залежностей. console.warn і console.error лишаються. У dev-режимі (pnpm dev) код не мінифікується, тож усі логи на місці. Не покладайтеся на це вирізання як на захист: секрети не варто логувати й у dev.
Unit-тести (Vitest, src/**/__tests__) і e2e (Playwright, e2e/) описані в /backend/testing. Відома проблема №38: Playwright webServer запускається через npm.