Личный кабинет

Кабинет живёт под /dashboard (src/app/[locale]/dashboard/) и показывает профиль пользователя, статус его лицензии и пример CRUD. Это готовый каркас — наполняйте его функциями своего продукта.

Защита маршрута

Доступ к /dashboard/** закрыт на двух уровнях:

  1. Edge-middleware (src/middleware.ts): Auth.js (edge-safe конфиг) проверяет сессию ещё до рендера и редиректит анонимов на /login — с учётом префикса локали (/en/login для англ.).
  2. Серверная проверка (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() уже расставлены в некритичных местах (регистрация, вход, CRUD Item) — посмотри их и добавь свои. Свой провайдер подключаешь так:

    import { setAnalyticsAdapter } from '@/lib/analytics';
    
    setAnalyticsAdapter({
      track: (event) => myProvider.capture(event.name, event.properties),
    });