Ідеальна авторизація в Next.js 16: Чому BetterAuth знищив Auth.js v5, GDPR та кінець епохи middleware
У 2026 році фраза "ми збираємо дані користувачів" змушує будь-якого європейського клієнта напружуватися. Штрафи за порушення GDPR досягають 20 мільйонів євро, і бізнес більше не готовий довіряти авторизацію "чорним ящикам" або погано спроектованим архітектурам. Коли ви будуєте SaaS для ринку ЄС, безпека даних - це не просто рядок у технічному завданні, це ваш головний козир, який формує абсолютну довіру.
Довгий час стандартом де-факто для Next.js був NextAuth (який згодом перетворився на Auth.js). Ми всі терпляче прописували pnpm add next-auth@beta, сподіваючись, що 5-та версія нарешті вийде з нескінченної бети, вирішить проблеми з типізацією та перестане тягнути за собою легасі. Але реальність сувора: для серйозних, строго типізованих проектів на TypeScript нам потрібен інструмент нового покоління.
І цим інструментом став BetterAuth.
У цій статті я, як Fullstack-архітектор, покажу, як ми будуємо залізобетонну систему авторизації в Next.js 16, використовуючи BetterAuth, Prisma 7 та новий підхід з proxy.ts, залишаючи застарілий middleware.ts у минулому.
Чому BetterAuth - це новий стандарт у 2026 році?
Перехід на BetterAuth - це не просто данина моді, це стратегічне інженерне рішення. Ось чому ми обрали його основою для наших платформ:
1. Абсолютний контроль над даними (GDPR Compliance)
Для європейських замовників критично важливо знати, де і як зберігаються дані їхніх клієнтів. BetterAuth створений з думкою про прозорість. Він не ховає від вас логіку роботи з базою даних. У зв'язці з Prisma 7 ми маємо чітко визначену схему користувачів, сесій та токенів. Ми деплоїмо наші проекти в ізольованих Docker-контейнерах на власних європейських VPS (Ubuntu), і завдяки BetterAuth ми на 100% контролюємо життєвий цикл кожного байта інформації, не передаючи її стороннім хмарним провайдерам. Це те, що європейські CEO хочуть чути на першому ж мітінгу.
2. Типізація, яка не змушує страждати
Якщо ви працювали з Auth.js, ви знаєте цей біль: спроба додати кастомне поле (наприклад, role або companyId) до сесії перетворювалася на пекло з перевизначенням глобальних інтерфейсів. BetterAuth написаний з підходом TypeScript-First. Він автоматично виводить типи з вашої конфігурації. Ваш код стає строго типізованим "з коробки", що виключає цілий клас помилок ще на етапі написання коду.
3. Модульність замість Моноліту
BetterAuth працює за принципом плагінів. Вам потрібна двофакторна автентифікація (2FA)? Підключаєте плагін. Потрібен магічний лінк? Підключаєте плагін. Ви не тягнете у свій бандл мегабайти зайвого коду, який ніколи не використаєте.
Кінець епохи Middleware: Зустрічаємо proxy.ts
Найбільшим архітектурним зсувом у Next.js 16 стала відмова від звичного middleware.ts на користь proxy.ts.
Раніше ми змушували middleware робити речі, для яких він не був створений: перевіряти JWT-токени на кожному запиті, робити складні редіректи та намагатися "подружити" Edge-середовище з нашою базою даних. Це часто призводило до проблем із продуктивністю, складнощів у налаштуванні Docker-образів та конфліктів модулів.
proxy.ts змінює правила гри:
Швидкість: Він працює на рівні маршрутизації до того, як запит взагалі торкнеться логіки вашого React-додатку.
Безпека: Ідеальне місце для інтеграції з BetterAuth. Ми можемо блискавично перевіряти кукі сесії та блокувати неавторизовані запити до адмін-панелі чи API, не навантажуючи основний потік сервера.
Чистота коду: Логіка захисту маршрутів тепер відокремлена від логіки рендерингу.
У наступних розділах я покажу практичний код: як налаштувати prisma.schema для BetterAuth, як ініціалізувати клієнт та як написати непробивний proxy.ts для захисту ваших сторінок.
Крок 1: Готуємо фундамент з Prisma 7 - База даних як Фортеця
Коли ми говоримо про безпеку та GDPR, база даних - це наше головне сховище, наша фортеця. Ми більше не можемо дозволити собі "магічні" таблиці, які створюються десь під капотом і які страшно чіпати. Оскільки ми розгортаємо наші платформи в ізольованих Docker-контейнерах на власних Ubuntu VPS, нам потрібна абсолютна передбачуваність інфраструктури.
BetterAuth вимагає чотири базові моделі: User, Session, Account та Verification. Але краса цього фреймворку в тому, що він ідеально розширюється. Нам не потрібно створювати додаткові таблиці типу UserProfile або писати складні JOIN-запити. Ми просто беремо базу і "прокачуємо" її під наші бізнес-задачі.
Ось як виглядає наш schema.prisma. Зверніть увагу на те, як елегантно ми вплітаємо специфічні для нашої SaaS-платформи поля безпосередньо в базову модель авторизації:
Фрагмент коду
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ==============================
// BETTER AUTH: ADMIN & USER
// ==============================
model User {
id String @id
name String?
email String @unique
emailVerified Boolean @default(false)
image String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 🟢 КАСТОМНІ ПОЛЯ (Розширення базової моделі)
username String? @unique
bio String? @db.Text
website String?
linkedin String?
role String @default("USER")
isActive Boolean @default(true)
// 🟢 ЗВ'ЯЗКИ З ІНШИМИ СУТНОСТЯМИ ПРОЕКТУ
accounts Account[]
sessions Session[]
softwareComments SoftwareComment[]
blogComments BlogComment[]
techStack UserTechStack[]
unitEconomicsSnapshots UnitEconomicsSnapshot[]
compareHistory CompareHistory[]
ownedWorkspaces Workspace[] @relation("WorkspaceOwner")
workspaceMemberships WorkspaceMember[]
addedWorkspaceSoftware WorkspaceTechStack[]
workspaceVotes WorkspaceVote[]
@@map("users")
}
model Session {
id String @id
expiresAt DateTime
token String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
ipAddress String?
userAgent String?
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("sessions")
}
model Account {
id String @id
accountId String
providerId String
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
accessToken String? @db.Text
refreshToken String? @db.Text
idToken String? @db.Text
accessTokenExpiresAt DateTime?
refreshTokenExpiresAt DateTime?
scope String?
password String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("accounts")
}
model Verification {
id String @id
identifier String
value String @unique
expiresAt DateTime
createdAt DateTime? @default(now())
updatedAt DateTime? @updatedAt
@@map("verifications")
}
Що робить цю схему Enterprise-рівня?
Строгі ідентифікатори (String @id): На відміну від старих підходів з
@default(autoincrement()), тут ми використовуємо рядкові ID. BetterAuth сам генерує криптографічно стійкі ідентифікатори (nano-id або uuid), що ідеально підходить для масштабування PostgreSQL та міграції даних між середовищами.Розширення користувача (Custom Fields): Ми додали поля
role(для розмежування прав доступу),isActive(для м'якого блокування користувачів без видалення їхніх даних, що є важливою вимогою GDPR), а також лінки на соціальні мережі. Пізніше, під час налаштування BetterAuth, ми змусимо TypeScript розпізнавати ці поля прямо в об'єктіsession.user.Глибокі реляційні зв'язки: Наш
User- це не просто ізольований запис для входу на сайт. Це повноцінне ядро платформи. Він відразу пов'язаний із коментарями (BlogComment), робочими просторами (Workspace) та фінансовими розрахунками (UnitEconomicsSnapshot).Cascadeвидалення в сесіях та акаунтах гарантує, що при видаленні користувача база даних не засмітиться "мертвими" токенами.
Після оновлення схеми не забуваємо застосувати міграції та згенерувати клієнт:
Bash
pnpm prisma migrate dev --name init_better_auth
pnpm prisma generate
Тепер, коли наша база даних готова та строго типізована, ми можемо переходити до серця системи - налаштування самого auth.ts.
Крок 2: auth.ts - Серце нашої безпеки та кінець "костилів" Auth.js
Якщо ви коли-небудь намагалися в Auth.js (ex-NextAuth) реалізувати одночасний логін за email або username, та ще й з перевіркою статусу блокування користувача, ви пам'ятаєте цей біль. Доводилося писати власні провайдери Credentials, вручну обробляти сесії в колбеках jwt та session, а TypeScript постійно сварився на відсутність поля role.
BetterAuth вирішує це на рівні архітектури за допомогою плагінів та хуків. Ми отримуємо повний контроль над життєвим циклом запиту. Більше того, оскільки ми деплоїмо проект на власному Ubuntu VPS (через Docker), нам критично важливо мати "запасний вхід" для Master-адміна через змінні оточення на випадок, якщо база даних щойно розгорнута і ще порожня.
Ось наш готовий конфіг src/auth.ts. Зверніть увагу на те, як елегантно ми вирішуємо одразу 4 задачі:
Строга типізація кастомних полів (ролі, статус).
Логін як за Email, так і за Username.
Блокування користувачів (
isActive).Автоматичні сповіщення в Telegram при нових реєстраціях.
TypeScript
// src/auth.ts
import bcrypt from 'bcryptjs'
import { betterAuth } from 'better-auth'
import { prismaAdapter } from 'better-auth/adapters/prisma'
import { prisma } from '@/lib/prisma'
import { sendNewUserNotification } from '@/lib/telegram/telegramBot'
// 🟢 Типізація для body у хуку
interface SignInBody {
email?: string
password?: string
}
export const auth = betterAuth({
database: prismaAdapter(prisma, {
provider: 'postgresql',
}),
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
password: {
hash: async (password) => await bcrypt.hash(password, 10),
verify: async ({ hash, password }) => await bcrypt.compare(password, hash),
},
},
// 🟢 Прибрали блок emailVerification. Ми будемо відправляти лист вручну,
// щоб Next.js не обривав процес відправки (timeout issues)!
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID ?? '',
clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? '',
},
linkedin: {
clientId: process.env.LINKEDIN_CLIENT_ID ?? '',
clientSecret: process.env.LINKEDIN_CLIENT_SECRET ?? '',
},
},
user: {
// 🟢 Магія BetterAuth: ці поля автоматично потраплять у TypeScript-інтерфейс Session
additionalFields: {
role: { type: 'string', defaultValue: 'USER' },
username: { type: 'string', required: false },
isActive: { type: 'boolean', defaultValue: true },
},
},
plugins: [
{
id: 'custom-credentials-enhancements',
hooks: {
before: [
{
// Перехоплюємо запит ДО того, як BetterAuth почне перевіряти хеші
matcher: (ctx) => ctx.path?.startsWith('/sign-in/email') || false,
handler: async (ctx) => {
const bodyData = ctx.body as SignInBody | undefined
if (!bodyData?.email) return
const inputLogin = String(bodyData.email)
const inputPassword = String(bodyData.password || '')
const adminUserEnv = process.env.ADMIN_USER ?? ''
const adminPassEnv = process.env.ADMIN_PASSWORD ?? ''
// 🟢 MASTER ADMIN БЕКДОР:
// Ідеально для першого деплою на VPS. Якщо логін і пароль збігаються з .env,
// ми створюємо або оновлюємо адміна "на льоту".
if (
adminUserEnv &&
adminPassEnv &&
inputLogin === adminUserEnv &&
inputPassword === adminPassEnv
) {
let adminUser = await prisma.user.findUnique({
where: { email: 'admin@upart.club' },
})
if (!adminUser) {
adminUser = await prisma.user.create({
data: {
id: 'master-admin',
name: 'Master Admin',
email: 'admin@upart.club',
role: 'SUPERADMIN',
username: 'admin',
emailVerified: true,
},
})
}
const passwordHash = await bcrypt.hash(adminPassEnv, 10)
const account = await prisma.account.findFirst({
where: { userId: adminUser.id, providerId: 'credential' },
})
if (!account) {
await prisma.account.create({
data: {
id: 'master-admin-account',
accountId: 'admin@upart.club',
providerId: 'credential',
userId: adminUser.id,
password: passwordHash,
},
})
} else {
const isMatch = await bcrypt.compare(adminPassEnv, account.password || '')
if (!isMatch) {
await prisma.account.update({
where: { id: account.id },
data: { password: passwordHash },
})
}
}
// Підміняємо body, щоб BetterAuth продовжив логін вже з правильною поштою
bodyData.email = 'admin@upart.club'
return { context: ctx }
}
// 🟢 ЛОГІН ЗА USERNAME АБО EMAIL
const user = await prisma.user.findFirst({
where: { OR: [{ email: inputLogin }, { username: inputLogin }] },
})
if (user) {
// 🟢 GDPR М'ЯКЕ ВИДАЛЕННЯ: Блокуємо доступ, якщо акаунт деактивовано
if (!user.isActive) {
throw new Error('Account is blocked.')
}
bodyData.email = user.email
return { context: ctx }
}
},
},
],
},
},
],
databaseHooks: {
user: {
create: {
after: async (user) => {
// 🟢 АВТОГЕНЕРАЦІЯ USERNAME
const usernameStr = typeof user.username === 'string' ? user.username : null
let finalUsername: string | null = usernameStr
if (user.email && !finalUsername) {
const baseUsername = user.email
.split('@')[0]
.toLowerCase()
.replace(/[^a-z0-9]/g, '')
let usernameExists = await prisma.user.findUnique({
where: { username: baseUsername },
})
let newUsername = baseUsername
while (usernameExists) {
newUsername = `${baseUsername}${Math.floor(Math.random() * 1000)}`
usernameExists = await prisma.user.findUnique({
where: { username: newUsername },
})
}
await prisma.user.update({
where: { id: user.id },
data: { username: newUsername },
})
finalUsername = newUsername
}
// 🟢 ТЕЛЕГРАМ-СПОВІЩЕННЯ ПРО РЕЄСТРАЦІЮ
// Відправляємо сповіщення власнику платформи тільки для верифікованих акаунтів (Social)
if (user.email && user.emailVerified) {
await sendNewUserNotification({
name: user.name || null,
username: finalUsername,
email: user.email,
provider: 'Social Auth (Google / LinkedIn)',
})
}
},
},
},
},
})
Розбір польотів: Що робить цей код унікальним?
Типізація "З Коробки" (
additionalFields) У Auth.js нам довелося б створювати файлnext-auth.d.ts, екстендити інтерфейсUserіSession. У BetterAuth ви просто декларуєтеadditionalFields. Бібліотека сама згенерує типи, і в будь-якому місці вашого фронтенду викликsession.user.roleбуде строго типізованим.Перехоплення запиту (
hooks.before) Це мій улюблений патерн. Користувач вводить у поле "Email/Username" свій нікнейм. Ми перехоплюємо цей запит до того, як BetterAuth спробує знайти його за поштою. Ми робимо свій запит до Prisma (OR: [{ email }, { username }]), знаходимо реальний email юзера, підміняємо його в об'єкті запиту і пропускаємо далі. Для ядра авторизації це виглядає як звичайний логін за поштою. Геніально і просто.Database Hooks та Інфраструктурні зв'язки Після успішного створення користувача (через Google чи LinkedIn), спрацьовує
databaseHooks. Тут ми не лише гарантуємо, що у кожного юзера буде унікальнийusername(через циклwhile), але й одразу інтегруємося з нашими бізнес-процесами. ВикликsendNewUserNotificationвідправляє повідомлення в Telegram-бота. Ви, як фаундер, миттєво дізнаєтесь про нових клієнтів на вашій SaaS-платформі.
Тепер, коли ядро авторизації налаштоване, нам залишається найголовніше - закрити доступ до приватних сторінок та адмінки. І для цього ми використаємо новітній proxy.ts, відправивши старий middleware.ts на пенсію.
Крок 3: proxy.ts (Middleware) - Ультимативний Edge-захист для Next.js
У Next.js 16 патерн middleware остаточно подорослішав. Ми більше не змушуємо наш React-додаток обробляти базові перевірки безпеки, лімітування запитів (rate limiting) чи SEO-редіректи. Натомість ми перехоплюємо запити на рівні Edge.
Коли європейський B2B-клієнт запитує: "Як ви захищаєте нашу адмін-панель від брутфорс-атак і обробляєте старі SEO-посилання?" - ви не пропонуєте їм купу сторонніх плагінів. Ви показуєте їм цю архітектуру.
Ось наш готовий до продакшену proxy.ts. Він вирішує три масштабні Enterprise-задачі в одному файлі:
API Rate Limiting: Захист від DDoS та спаму.
Контроль доступу до адмінки: Строгий Role-Based Access Control (RBAC), спеціально оптимізований для середовищ Docker та VPS.
Динамічні SEO-редіректи: Блискавичні 308-редіректи через Redis + Prisma.
Зверніть особливу увагу на хак із Docker SSL NAT Loopback. Лише одне це збереже вам дні дебагінгу під час деплою на Ubuntu VPS під Nginx reverse proxy.
TypeScript
// src/proxy.ts
import { type NextRequest, NextResponse } from 'next/server'
import { redis } from '@/lib/redis'
/* -------------------------------------------------------------------------- */
/* Типи */
/* -------------------------------------------------------------------------- */
interface RateLimitInfo {
count: number
resetTime: number
}
declare global {
var rateLimitCleanerStarted: boolean | undefined
}
/* -------------------------------------------------------------------------- */
/* Rate Limiter Storage */
/* -------------------------------------------------------------------------- */
const rateLimitMap = new Map<string, RateLimitInfo>()
// Раз на годину очищаємо пам'ять від старих IP-адрес, щоб запобігти витоку пам'яті
if (!globalThis.rateLimitCleanerStarted) {
setInterval(
() => {
const now = Date.now()
for (const [ip, data] of rateLimitMap.entries()) {
if (now > data.resetTime) {
rateLimitMap.delete(ip)
}
}
},
60 * 60 * 1000,
)
globalThis.rateLimitCleanerStarted = true
}
/* -------------------------------------------------------------------------- */
/* Middleware */
/* -------------------------------------------------------------------------- */
export default async function middleware(req: NextRequest) {
const { nextUrl } = req
const pathname = nextUrl.pathname
// --- 1. RATE LIMITING ДЛЯ API (ОКРІМ АВТОРИЗАЦІЇ) ---
// Виключаємо /api/auth, щоб запити BetterAuth проходили без затримок
if (pathname.startsWith('/api') && !pathname.startsWith('/api/auth')) {
const ip =
req.headers.get('x-forwarded-for')?.split(',')[0] ||
req.headers.get('x-real-ip') ||
'127.0.0.1'
const now = Date.now()
const limit = 50
const windowMs = 60 * 1000 // 1 хвилина
const userRate = rateLimitMap.get(ip)
if (!userRate || now > userRate.resetTime) {
rateLimitMap.set(ip, { count: 1, resetTime: now + windowMs })
} else {
userRate.count++
if (userRate.count > limit) {
return new NextResponse(
JSON.stringify({
error: 'Too many requests. Please try again later.',
}),
{ status: 429, headers: { 'Content-Type': 'application/json' } },
)
}
}
}
// --- 2. ЛОГІКА КОНТРОЛЮ ДОСТУПУ В АДМІНКУ ---
const isAdminLoginPage = pathname === '/admin/login'
const isAdminArea = pathname.startsWith('/admin') && !isAdminLoginPage
if (isAdminLoginPage || isAdminArea) {
// 🟢 Хак для Docker: Жорстко прописуємо локальну HTTP-адресу контейнера!
// Це оминає зовнішній Nginx та HTTPS, уникаючи жахливої помилки SSL NAT Loopback.
const localBaseUrl = 'http://127.0.0.1:3005'
// Запитуємо сесію безпосередньо у локального екземпляра сервера
const sessionRes = await fetch(`${localBaseUrl}/api/auth/get-session`, {
headers: { cookie: req.headers.get('cookie') || '' },
})
const sessionData = await sessionRes.json()
const isLoggedIn = !!sessionData?.session
const userRole = sessionData?.user?.role || 'GUEST'
const allowedAdminRoles = ['SUPERADMIN', 'ADMIN', 'EDITOR', 'MODERATOR']
const hasAdminAccess = allowedAdminRoles.includes(userRole)
// Якщо авторизований адмін намагається зайти на сторінку логіну адмінки
if (isAdminLoginPage) {
if (isLoggedIn && hasAdminAccess) {
return NextResponse.redirect(new URL('/admin', nextUrl))
}
return NextResponse.next()
}
// Якщо намагаються зайти в закриту адмін-панель
if (isAdminArea) {
if (!isLoggedIn) {
const url = new URL('/admin/login', nextUrl)
url.searchParams.set('callbackUrl', pathname)
return NextResponse.redirect(url)
}
if (!hasAdminAccess) {
// Звичайних користувачів без прав адміна викидаємо на головну
return NextResponse.redirect(new URL('/', nextUrl))
}
}
}
// --- 3. SEO 308 РЕДІРЕКТИ (КЕШ REDIS + ФОЛБЕК НА PRISMA) ---
if (
!pathname.startsWith('/_next') &&
!pathname.startsWith('/api') &&
!pathname.startsWith('/admin')
) {
try {
const cacheKey = `redirect:${pathname}`
// Шукаємо правило редіректу в кеші Redis
let targetUrl = await redis.get(cacheKey)
// Якщо в кеші порожньо - звертаємось до БД через лінивий (lazy) імпорт Prisma
if (!targetUrl) {
const { prisma } = await import('@/lib/prisma')
const redirectRule = await prisma.redirect.findUnique({
where: { sourceUrl: pathname },
})
if (redirectRule) {
targetUrl = redirectRule.targetUrl
// Зберігаємо в кеш назавжди
await redis.set(cacheKey, targetUrl)
}
}
// Якщо правило редіректу спрацювало, повертаємо чистий постійний SEO-редірект 308
if (targetUrl) {
return NextResponse.redirect(new URL(targetUrl, nextUrl), 308)
}
} catch (e) {
console.error('Redirect Logic Error:', e)
}
}
return NextResponse.next()
}
/* -------------------------------------------------------------------------- */
/* Config */
/* -------------------------------------------------------------------------- */
export const config = {
// Matcher виключає системні файли, шрифти та статику для економії ресурсів сервера
matcher: [
'/((?!_next/static|_next/image|favicon.ico|assets-admin|assets-main|uploads|.*\\..*).*)',
],
}
Архітектурний розбір:
Коли ви презентуєте цю інфраструктуру клієнту, ви акцентуєте увагу на трьох конкретних архітектурних перемогах:
1. Рішення проблеми Docker SSL NAT Loopback
У production-середовищі ваш Next.js додаток "сидить" всередині Docker-контейнера за Nginx reverse proxy (керованим панелями на кшталт HestiaCP), який обробляє SSL-сертифікати. Якщо ваш middleware намагається зробити запит на https://yourdomain.com/api/auth/get-session зсередини контейнера, запит часто падає через обмеження NAT loopback - контейнер не знає, як безпечно вийти в інтернет і повернутися назад до самого себе.
Жорстко прописавши внутрішню loopback-адресу (http://127.0.0.1:3005), ми повністю оминаємо Nginx. Middleware звертається безпосередньо до Node.js сервера. Це блискавично швидко, на 100% безпечно (оскільки запит ніколи не покидає межі сервера) і абсолютно стійко до помилок DNS або SSL.
2. Нативний Rate Limiting без залежностей від Edge
Багато туторіалів кажуть використовувати Vercel KV або Upstash для rate limiting. Але що робити, якщо політика GDPR клієнта вимагає, щоб усі дані залишалися на їхньому фізичному сервері в Німеччині? Ми реалізували високоефективну Map у пам'яті, яка сама очищається. Вона відстежує IP-адреси та блокує агресивних ботів, які намагаються "покласти" ваш API, зберігаючи при цьому строгий суверенітет даних.
3. Enterprise SEO з Redis та лінивими імпортами (Lazy Imports)
На великих платформах URL-адреси постійно змінюються. Маркетинговим командам потрібні 301/308 редіректи. Звертатися до PostgreSQL при кожному завантаженні сторінки для перевірки редіректів - це вбивство продуктивності. Тут ми використовуємо гібридний підхід: спершу перевіряємо блискавичний кеш Redis. Якщо там нічого немає (miss), ми динамічно імпортуємо import('@/lib/prisma'), щоб не завантажувати масивну ORM у пам'ять під час кожного запиту. Як тільки редірект знайдено, він кешується в Redis. Google миттєво отримує свій 308 Permanent Redirect, зберігаючи ваші SEO-позиції.
Крок 7: Серверні дії та API - як ми контролюємо кожен крок авторизації
Ми відмовилися від "автоматичної магії" там, де вона шкодить стабільності. Наприклад, BetterAuth вміє сам відправляти листи, але в умовах Next.js (через Serverless-ліміти або довгі таймаути) краще тримати цей процес під власним контролем.
Ми створили окремий шар Server Actions, які не просто реєструють юзера, а й захищають нас від спаму за допомогою Google reCAPTCHA v3.
API Handler (Gateway)
Для початку, відкриваємо шлюз для BetterAuth. Тут усе максимально лаконічно: один обробник для всіх методів (GET/POST), який BetterAuth сам розрулює.
TypeScript
// src/app/api/auth/[...all]/route.ts
import { toNextJsHandler } from 'better-auth/next-js'
import { auth } from '@/auth'
// toNextJsHandler автоматично розкидає методи GET, POST і т.д.
export const { GET, POST } = toNextJsHandler(auth)
Реєстрація: Ручний контроль для максимальної надійності
У нашому register.ts ми не просто викликаємо signUp. Ми спочатку перевіряємо юзера на "людяність" через капчу, потім реєструємо його, і лише після успіху самі генеруємо токен верифікації та відправляємо лист. Це гарантує: якщо лист не дійшов - ми про це дізнаємося, бо явно чекаємо на результат.
TypeScript
// src/actions/register.ts
'use server'
import { headers } from 'next/headers'
import { auth } from '@/auth'
import { generateVerificationToken } from '@/lib/auth/tokens'
import { sendVerificationEmail } from '@/lib/mail'
import { prisma } from '@/lib/prisma'
export async function registerUser(formData: FormData) {
// 1. Захист від ботів (Honeypot + reCAPTCHA)
// ... (логіка перевірки капчі)
try {
// 2. Реєстрація юзера через API Better Auth
await auth.api.signUpEmail({
body: { email, password, name, username, role: 'USER', isActive: true },
headers: await headers(),
})
// 3. Генеруємо токен вручну
const verificationToken = await generateVerificationToken(email)
// 4. Відправляємо листа вручну з очікуванням (await)
await sendVerificationEmail(email, verificationToken.value, name || '')
return { success: 'Confirmation email sent!', needsVerification: true }
} catch (error) {
return { error: 'Registration failed.' }
}
}
Безпека профілю та зміна паролю
Найбільша помилка новачків - оновлювати хеш пароля в таблиці User. Ми робимо правильно: хеш пароля - це частина сутності Account. Коли юзер змінює пароль, ми перевіряємо його поточний через bcrypt.compare і лише тоді пишемо новий хеш у відповідний account.id.
TypeScript
// src/actions/user-settings.ts
export async function updatePassword(formData: PasswordData): Promise<ActionResponse> {
// 1. Отримуємо сесію через auth.api
const session = await auth.api.getSession({ headers: await headers() })
if (!session?.user?.id) return { error: 'Unauthorized' }
// 2. І шукаємо саме обліковий запис email (credentials)
const account = await prisma.account.findFirst({
where: { userId: session.user.id, providerId: 'email' },
})
if (!account?.password) return { error: 'Account does not use password auth.' }
// 3. Порівнюємо старий пароль і хешуємо новий
const passwordsMatch = await bcrypt.compare(currentPassword, account.password)
if (!passwordsMatch) return { error: 'Current password incorrect.' }
const hashedPassword = await bcrypt.hash(newPassword, 10)
// 4. Оновлюємо таблицю Account
await prisma.account.update({
where: { id: account.id },
data: { password: hashedPassword },
})
return { success: 'Password updated successfully!' }
}
Клієнтська частина: Auth Client
На фронтенді ми не використовуємо "сирі" фетч-запити. Ми використовуємо authClient, створений через createAuthClient. Це дає нам хуки useSession, signIn, signOut, які працюють як стандартні React-хуки, але під капотом мають повну підтримку Next.js App Router.
TypeScript
// src/lib/auth-client.ts
import { createAuthClient } from 'better-auth/react'
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000',
})
// Експортуємо хуки для використання у компонентах
export const { useSession, signIn, signOut } = authClient
Чому ця архітектура виграє?
Token Strategy (
tokens.ts): Ми використовуємоuuidv4для генерації токенів і зберігаємо їх у таблиціVerificationз префіксами (наприклад,reset_абоverify_). Це робить нашу систему токенів гнучкою: ми можемо мати різні типи запитів (відновлення паролю, підтвердження пошти, зміна email) і ніколи не заплутаємося в них.Безпека: Використання
bcryptдля хешування паролів вручну в Actions дає нам 100% гарантію того, що навіть якщо хтось отримає доступ до бази, він не прочитає паролі.Експірієнс: Використання
auth.apiдозволяє працювати з авторизацією навіть у Server Actions, що раніше було величезним головним болем.
Висновок: Чому цей підхід - це інвестиція в майбутнє
Написання власної системи авторизації - це часто те, чого розробники намагаються уникнути. Але коли ви будуєте продукт для B2B-сектору, особливо на ринку ЄС, ви не можете дозволити собі грати в рулетку з middleware.ts або сподіватися, що чергова версія "legacy-бібліотеки" не зламає вам продакшен.
Вибір на користь BetterAuth, Prisma 7 та proxy.ts - це наш свідомий крок у бік передбачуваності та безпеки.
Що ми отримали в результаті:
Повний контроль над даними (GDPR Ready): Ми не просто використовуємо бібліотеку, ми інтегруємо авторизацію в наш бізнес-потік. Ми самі вирішуємо, як і коли відправляти листи, як блокувати юзерів і як зберігати їхні сесії.
Архітектурна чистота: Перехід на
proxy.ts- це не просто зміна назви файлу. Це перехід на рівень інфраструктури, де ви керуєте запитами до того, як вони навантажать ваш Node.js сервер. Це швидше, надійніше і простіше в дебагінгу.Розробницький "дзен": Завдяки Prisma 7 та підходу BetterAuth до типізації, ми більше не витрачаємо години на те, щоб вибити з TypeScript знання про те, що поле
roleіснує. Воно там є, і воно працює.Надійність для VPS/Docker: Ми вирішили одну з найболючіших проблем деплою - SSL NAT Loopback - і зробили це елегантно, не ламаючи конфігурацію Nginx.
Фінальна думка
Код - це інструмент. А архітектура - це те, як ці інструменти працюють разом, щоб ваш бізнес не зупинився о третій годині ночі через падіння стороннього сервісу авторизації. Ми перестали покладатися на "магію" і почали будувати фундамент, який витримує навантаження, забезпечує відповідність стандартам безпеки та дозволяє масштабуватися без болю.
Налаштуйте це один раз, протестуйте, і ваш наступний B2B-клієнт з Європи навіть не запитає про безпеку - він побачить її в архітектурі вашого продукту.
Успіхів у розробці, і нехай ваші сервіси працюють як годинник!
Якщо у вас виникли запитання по реалізації конкретних плагінів BetterAuth або ви хочете обговорити архітектуру вашого проекту - пишіть у коментарях або залітайте в Telegram. Буду радий поділитися досвідом.

Коментарі (0)
Залишити коментар