بازگشت به وبلاگ

احرازهویت با better-auth در Next.js: یه راه‌اندازی دست‌اول

۳ مرداد ۱۴۰۵4 دقیقه مطالعه
احرازهویت با better-auth در Next.js: یه راه‌اندازی دست‌اول

لاگین پنل ادمین همین سایت خودش روی better-auth کار می‌کنه، پس این یه روایت دست‌اوله، نه یه آموزش که فقط از روی مستندات نوشته شده باشه. این‌جا شکل دقیق یه راه‌اندازی تک‌ادمین با ایمیل/پسورد رو می‌بینیم: کانفیگ سرور، route handler خودِ Next.js، کلاینت، و محافظت دولایه‌ای از روت که واقعاً /admin رو قفل نگه می‌داره.

۱. instance سمت سرور

// src/lib/auth.ts
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { prisma } from "@/lib/prisma";

export const auth = betterAuth({
  database: prismaAdapter(prisma, { provider: "postgresql" }),

  emailAndPassword: { enabled: true, minPasswordLength: 8 },

  // یه فیلد سفارشی روی مدل user، که تعیین می‌کنه کی ادمین حساب می‌شه.
  user: {
    additionalFields: {
      role: { type: "string", required: false, defaultValue: "admin", input: false },
    },
  },

  // session رو توی یه کوکی امضاشده کش کن تا getSession() سر هر ناوبری
  // سراغ دیتابیس نره — فقط هر ۵ دقیقه دوباره باهاش validate می‌شه.
  session: {
    cookieCache: { enabled: true, maxAge: 5 * 60 },
  },

  trustedOrigins: [process.env.NEXT_PUBLIC_AUTH_URL as string],
  secret: process.env.BETTER_AUTH_SECRET,
  baseURL: process.env.BETTER_AUTH_URL,
});

export type Session = typeof auth.$Infer.Session;

۲. Mount کردنش — یه route handler catch-all

better-auth هر endpoint (ورود، خروج، session، تغییر پسورد، و هرچی که یه پلاگین اضافه کنه) رو از طریق یه handler که روی یه route catch-all واحد mount شده expose می‌کنه.

// src/app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";

export const { GET, POST } = toNextJsHandler(auth);

۳. کلاینت

// src/lib/auth-client.ts
import { createAuthClient } from "better-auth/react";

export const authClient = createAuthClient({
  baseURL: process.env.NEXT_PUBLIC_APP_URL, // فقط origin اپ — بدون هیچ path اضافه
});

export const { signIn, signOut, useSession } = authClient;
"use client";
import { useState } from "react";
import { signIn } from "@/lib/auth-client";

export default function AdminLoginPage() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState<string | null>(null);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    const { error: signInError } = await signIn.email({ email, password });
    if (signInError) setError(signInError.message ?? "ورود ناموفق بود");
    else window.location.href = "/admin";
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={email} onChange={(e) => setEmail(e.target.value)} type="email" />
      <input value={password} onChange={(e) => setPassword(e.target.value)} type="password" />
      {error && <p className="text-destructive text-sm">{error}</p>}
      <button type="submit">ورود</button>
    </form>
  );
}

۴. محافظت از /admin — دولایه، عمداً

یه چک تنها یا خیلی کنده (یه رفت‌وبرگشت دیتابیس سر هر ناوبری) یا خیلی ضعیفه (یه چک صرفِ وجود کوکی نمی‌تونه یه session معتبر رو از یه کوکی جعلی/منقضی تشخیص بده). این سایت هردوتاش رو داره، هرکدوم کاری که واقعاً توش خوبه رو انجام می‌ده.

لایه

چی رو چک می‌کنه

هزینه

هدف

Middleware

فقط وجود کوکی session

مجانی — بدون فراخوانی دیتابیس

ری‌دایرکت سریع به لاگین برای حالت واضح (اصلاً کوکی نیست)

Admin layout (auth.api.getSession)

کوکی واقعاً معتبره + role === "admin"

حداکثر یه فراخوانی دیتابیس هر ۵ دقیقه (کش کوکی)

دروازه‌ی واقعی و معتبر

// src/middleware.ts — چک ارزون و optimistic
import { getSessionCookie } from "better-auth/cookies";

const ADMIN_PATH_RE = /^\/(en|fa)\/admin(?!\/login)(\/.*)?$/;

export default function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  if (ADMIN_PATH_RE.test(pathname)) {
    const sessionCookie = getSessionCookie(request);
    if (!sessionCookie) {
      const locale = pathname.split("/")[1];
      return NextResponse.redirect(new URL(`/${locale}/admin/login`, request.url));
    }
  }
  // ...روتینگ next-intl پایین‌تر ادامه پیدا می‌کنه
}
// src/app/[locale]/admin/layout.tsx — دروازه‌ی واقعی
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { auth } from "@/lib/auth";

export default async function AdminLayout({ children }: { children: React.ReactNode }) {
  const session = await auth.api.getSession({ headers: await headers() });

  if (!session || session.user.role !== "admin") {
    redirect("/admin/login");
  }

  return <>{children}</>;
}

Middleware نمی‌تونه امن سراغ auth.api.getSession() سر هر درخواست بره — روی Edge runtime اجرا می‌شه و یعنی یه رفت‌وبرگشت دیتابیس قبل از رندر شدن هر ناوبری ادمین. چک‌کردن وجود کوکی اونجا، و انجام چک معتبر فقط یه‌بار توی layout (پشتیبانی‌شده با کش کوکی ۵ دقیقه‌ای)، چیزیه که پنل ادمین رو هم سریع هم واقعاً امن نگه می‌داره.

۵. Seed کردن تنها حساب مورد نظر

چون هیچ صفحه‌ی sign-up عمومی‌ای وجود نداره، تنها حساب ادمین با رفتن از طریق API خودِ signUpEmail بتر-اوث توی یه اسکریپت seed ساخته می‌شه — این تضمین می‌کنه فرمت هش پسورد همیشه با چیزی که verification ورود انتظار داره یکی باشه، حتی بین نسخه‌های مختلف better-auth، به‌جای دست‌ساز کردن یه هش.

// scripts/seed.ts
await auth.api.signUpEmail({
  body: {
    name: process.env.ADMIN_NAME!,
    email: process.env.ADMIN_EMAIL!,
    password: process.env.ADMIN_PASSWORD!,
  },
});

چک‌لیست

  • baseURL توی کلاینت فقط origin اپه — هیچ‌وقت /api یا هر path دیگه‌ای بهش اضافه نکن.

  • trustedOrigins سمت سرور باید دقیقاً با origin دیپلوی‌شده یکی باشه (پروتکل، هاست، و www در برابر apex همه اهمیت دارن) وگرنه هر درخواست cross-origin با CORS رد می‌شه.

  • از cookieCache استفاده کن تا getSession() سر هر درخواستی که نیازش داره سراغ دیتابیس نره.

  • روت‌ها رو دولایه محافظت کن: یه چک ارزون وجود کوکی توی middleware، و چک واقعی auth.api.getSession() توی یه layout یا page.

  • تنها حساب ادمین رو از طریق auth.api.signUpEmail() توی یه اسکریپت seed بساز، نه با دستی insert کردن یه ردیف با یه فرمت هش حدسی.