
لاگین پنل ادمین همین سایت خودش روی 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 کردن یه ردیف با یه فرمت هش حدسی.
مطالب بیشتر

Recharts در Next.js و Tailwind: تمدهی و استایلدهی سفارشی
طبیعیکردن Recharts با سیستم طراحی Tailwind: مرز درست Client Component، وصلکردن رنگها به CSS variable برای dark mode خودکار، tooltip و legend سفارشی، و مدیریت لوکیلهای RTL.

راهاندازی Tiptap در Next.js: پنل ادمین قابلویرایش، رندر عمومی فقطخواندنی
راهاندازی دقیق Tiptap پشت هر پست این سایت: یه لیست extension مشترک برای ادیتور و generateHTML()، ذخیرهی JSON توی Postgres، بلاکهای کد با syntax highlighting، و عکسهای آپلودشده (نه base64).