Личный кабинет
Кабинет живёт под /dashboard (src/app/[locale]/dashboard/) и показывает профиль пользователя,
статус его лицензии и пример CRUD. Это готовый каркас — наполняйте его функциями своего продукта.
Защита маршрута
Доступ к /dashboard/** закрыт на двух уровнях:
- Edge-middleware (
src/middleware.ts): Auth.js (edge-safe конфиг) проверяет сессию ещё до рендера и редиректит анонимов на/login— с учётом префикса локали (/en/loginдля англ.). - Серверная проверка (
requireUser()изsrc/lib/auth-helpers.ts): каждая страница кабинета дополнительно вызываетrequireUser()— он возвращает пользователя или делаетredirect('/login'). Это «пояс и подтяжки» поверх middleware: страница получает объект пользователя без null-проверок.
Подробнее про сессии и провайдеры — в «Аутентификация».
Профиль и статус лицензии
Страница «Обзор» (dashboard/page.tsx) показывает две карточки:
- Профиль — имя и email из сессии (
requireUser()). - Подписка — статус лицензии. Сервис
src/services/billing.service.tsберёт последнюю записьLicense(getLatestLicense) и сводит её в ярлык чистой функциейsummarizeBilling:
const STATUS_LABELS: Record<LicenseStatus, string> = {
none: 'Нет лицензии',
pending: 'Ожидает оплаты',
active: 'Активна',
refunded: 'Возвращена',
};
Сама лицензия хранится в модели License — см. «База данных».
Оплата и апгрейд
Страница dashboard/billing/page.tsx выводит тарифы (PRICING_PLANS) и кнопку UpgradeButton.
Кнопка отправляет POST /api/checkout с planId; роут создаёт pending-лицензию, вызывает
провайдера платежей и возвращает confirmationUrl, на который редиректится покупатель:
// src/app/api/checkout/route.ts (сокращённо)
const license = await createPendingLicense(prisma, { userId: user.id, plan: plan.name /* ... */ });
const payment = await createLicenseCheckout({ provider, creds, order, orderId: license.id /* ... */ });
return NextResponse.json({ confirmationUrl: payment.confirmationUrl, licenseId: license.id });
По умолчанию провайдер — mock, поэтому весь путь оплаты работает без реальных ключей. Детали —
в «Платежи».
Пример CRUD (Items)
Секция «Обзор» содержит панель ItemsPanel, работающую с примерной моделью Item через REST:
| Метод | Роут | Действие |
|---|---|---|
GET | /api/items | список элементов текущего пользователя |
POST | /api/items | создать элемент ({ title }) |
DELETE | /api/items/[id] | удалить свой элемент |
Все роуты требуют сессию (иначе 401), а deleteItem проверяет владельца элемента — защита от
IDOR (src/services/item.service.ts). Это учебный пример: замените Item и /api/items на
сущности своего продукта.
Что каркас, а что настройка
-
Лендинг (
/) и пример CRUD — заготовки под замену: блоки на DaisyUI, которые вы переписываете под свой продукт, а не настраиваемая подсистема. -
Аналитика — единый хук
track()(src/lib/analytics.ts), no-op по умолчанию. При старте сервераsrc/instrumentation.tsчитаетANALYTICS_PROVIDERи регистрирует адаптер:yandex-metrica(нужен ещёNEXT_PUBLIC_METRIKA_ID— без него тоже no-op) илиconsoleдля отладки. БезANALYTICS_PROVIDERадаптер не регистрируется — события никуда не отправляются. Примеры вызововtrack()уже расставлены в некритичных местах (регистрация, вход, CRUDItem) — посмотри их и добавь свои. Свой провайдер подключаешь так:import { setAnalyticsAdapter } from '@/lib/analytics'; setAnalyticsAdapter({ track: (event) => myProvider.capture(event.name, event.properties), });