Postgres + Auth.js v5: подключаем БД и magic-link

Тема 4/5: День 4. Доходный ИИ-сервис — собираем с нуля Урок 4/7

Без авторизации сервис не работает — некому списывать лимиты и нечего продавать. На этом уроке подключаем Postgres (для теста — Neon, бесплатно), описываем схему через Prisma и добавляем Auth.js v5 с magic-link на email. Без паролей и без OAuth — это самый простой и безопасный путь.

Из чего состоит авторизация

Авторизация складывается из двух связанных частей: базы данных, где хранятся пользователи, сессии и токены, и библиотеки, которая с этой базой работает — шлёт письма и проверяет сессии. В нашем стеке роли распределены так. Базой служит Postgres: на стадии теста мы берём Neon — бесплатный облачный Postgres с регистрацией через GitHub, а для продакшена в РФ позже переедем на Timeweb Managed Postgres. Роль ORM выполняет Prisma: схему базы мы описываем в одном файле, а Prisma генерирует типизированный клиент, так что в коде получается prisma.user.findMany() с автодополнением. За саму авторизацию отвечает Auth.js v5 — npm-пакет, который раньше назывался NextAuth; все данные остаются в нашей базе, без внешних сервисов.

Важно про 152-ФЗ. Neon — это западный сервис с серверами в США/EU. По 152-ФЗ первичная обработка персональных данных граждан РФ должна быть на серверах в России. На этапе тестового запуска Neon допустим (быстро, бесплатно, удобно). Для боевого сервиса с реальными пользователями — переезжаем на российский managed Postgres (Timeweb, VK Cloud, Selectel). Об этом — в платном курсе.

Создание Postgres-проекта на Neon

На время теста заводим базу на Neon — это несколько шагов:

  1. Заходите на neon.tech → Sign up через GitHub
  2. Жмёте «New Project»
  3. Указываете имя (например, «ai-posts»), регион (Frankfurt — ближе к РФ)
  4. Ждёте ~10 секунд пока проект создаётся
  5. На главной странице проекта копируете Connection string вида postgresql://user:pass@host/db?sslmode=require

Эта строка пойдёт в .env.local как DATABASE_URL.

Промпт для подключения Auth.js v5 + Prisma

Подключи Postgres через Prisma и Auth.js v5 к нашему Next.js проекту.

DATABASE_URL: postgresql://... (вставь из Neon)

Сделай:
1. Установи пакеты: prisma, @prisma/client, @auth/core, @auth/prisma-adapter
2. Инициализируй Prisma: npx prisma init
3. В prisma/schema.prisma опиши модели User, Account, Session, VerificationToken по стандарту Auth.js + кастомные поля:
    User: id, email, emailVerified, name, image, plan ("free" default), creditsUsed (int default 0), creditsLimit (int default 5), createdAt
4. Запусти миграцию: npx prisma migrate dev --name init
5. Создай src/lib/db.ts с экспортом Prisma client (singleton)
6. Создай src/auth.ts с Auth.js v5 конфигом: PrismaAdapter, session strategy "database", email-провайдер (magic-link)
7. Создай src/app/api/auth/[...nextauth]/route.ts — handlers для GET и POST

Дальше создай страницы:
- /signup — форма с email и кнопкой «Получить magic-link»
- /login — то же самое
- /dashboard — пока пустая, доступна только авторизованным

Env-переменные: DATABASE_URL, AUTH_SECRET (сгенерируй через openssl rand -hex 32), AUTH_URL (http://localhost:3000), EMAIL_FROM, EMAIL_SERVER (SMTP-строка от Notisend или Resend).

Стиль — как на остальных страницах (тёмная тема, бирюзовый акцент). Тексты на русском.

Как работает magic-link

  1. Пользователь на /signup вводит email
  2. Интерфейс шлёт запрос в Auth.js: «отправь magic-link на этот email»
  3. Auth.js генерирует одноразовый токен, сохраняет в таблицу VerificationToken, отправляет email со ссылкой https://yoursite.com/api/auth/callback/email?token=...
  4. Пользователь кликает ссылку в почте → попадает на callback
  5. Auth.js проверяет токен, создаёт User (если новый) и Session, ставит httpOnly cookie
  6. Пользователь авторизован — сессия живёт 30 дней по умолчанию

У такого подхода есть понятные преимущества перед паролями. Пользователю не нужно ничего запоминать — он всё равно вводит только email. Перебором паролей такую авторизацию не взломать, потому что паролей просто нет. Исчезает и целый сценарий «забыл пароль» с восстановлением. А регистрация и вход при этом превращаются в одну и ту же форму.

Защищённые роуты

Страница /dashboard должна быть доступна только авторизованным. Добавим проверку:

В Next.js 16 защити /dashboard через proxy (в Next.js 16 middleware переименован в proxy).
Создай src/proxy.ts: используй функцию auth() из Auth.js v5. Если сессии нет — редирект на /login?from=<исходный URL>.
Защищай: /dashboard, /api/generate (которая будет позже), /api/account.
Не защищай: /, /pricing, /signup, /login, /api/auth/*.

Модели в БД

Auth.js v5 + PrismaAdapter работает с четырьмя стандартными моделями:

  • User — пользователь (id, email, name, image, emailVerified). Создаётся при первой авторизации. К нему мы добавили бизнес-поля: plan, creditsUsed, creditsLimit.
  • Account — связь User с провайдером авторизации. Один User может иметь несколько Account (например, magic-link + Google в будущем).
  • Session — активная сессия (sessionToken, userId, expires). По дефолту живёт 30 дней.
  • VerificationToken — одноразовые токены для magic-link. Auth.js сам создаёт и удаляет.

На уроке 24 мы будем списывать creditsUsed при каждой генерации, а при оплате — обновлять plan.

Безопасность

Главное правило: никогда не коммитьте .env.local в git. Там лежат DATABASE_URL и AUTH_SECRET. Если случайно закоммитили — сразу пересоздайте: Neon → Settings → Reset password, новый AUTH_SECRET через openssl rand -hex 32.

Каждый запрос к БД фильтруйте по userId. В Auth.js v5 сессия даёт session.user.id — используйте его в каждом where. Если запрос к чужим данным не фильтруется по userId — это дыра: любой авторизованный пользователь сможет прочитать чужие записи.

Если что-то не работает

При первом подключении авторизации чаще всего всплывают четыре типовые проблемы:

  • Magic-link не приходит — проверьте папку «Спам». Если шлёте через SMTP-провайдера (Notisend, Resend) — убедитесь что домен отправителя верифицирован (SPF/DKIM-записи в DNS). Без верификации письма падают в спам или вообще не доставляются.
  • «PrismaClientInitializationError» — проверьте DATABASE_URL в .env.local. Часто забывают ?sslmode=require в конце для Neon.
  • «User not authenticated» — куки не передаются. Проверьте что AUTH_URL совпадает с реальным URL приложения (http://localhost:3000 для dev, продакшен-домен для боевого).
  • «No matching VerificationToken» — токен уже использован или истёк. Токены живут 24 часа, потом нужно запрашивать новый magic-link.

Что в следующем уроке

Авторизация готова: пользователь регистрируется и попадает в /dashboard, но там пока пусто. На уроке 24 добавляем главную функцию сервиса — ИИ-генерацию через OpenRouter.