قسمت هجدهم - طراحی احراز هویت در Next.js برای پنل ادمین
بررسی اینکه چه کسی باید وارد شود ،چه کسی اجازه دسترسی دارد،login flow چگونه باید باشد session کجا قرار میگیرد و نهایتا کدام مسیرها باید محافظت شوند؟
طراحی احراز هویت در Next.js برای پنل ادمین
تا اینجای مسیر، فرمها را ساختیم، اعتبارسنجی را با Zod انجام دادیم، Route Handlers را بهعنوان مسیر اصلی API انتخاب کردیم و برای پروژهها هم CRUD اولیه را طراحی کردیم. اما یک مشکل مهم هنوز باقی مانده است:
الان اگر endpointهای ساخت، ویرایش یا حذف پروژه بدون محافظت باشند، هر کسی میتواند به آنها درخواست بفرستد.
برای همین قبل از اینکه وارد ساخت داشبورد مدیریتی شویم، باید یک لایه مهم را طراحی کنیم:
Authentication
یا همان احراز هویت.
در این قسمت قرار نیست هنوز همه چیز را کامل پیادهسازی کنیم.
هدف این بخش بیشتر طراحی درست مدل auth در پروژه است، تا بدانیم:
- چه کسی باید وارد شود؟
- چه کسی اجازه دسترسی دارد؟
- login flow چگونه باید باشد؟
- session کجا قرار میگیرد؟
- کدام مسیرها باید محافظت شوند؟
اگر این طراحی از ابتدا درست انجام شود، ساخت داشبورد، APIهای ادمین و بخش مدیریت خیلی تمیزتر پیش میرود.
auth در این پروژه برای چه کسی است؟
اولین سؤال مهم این است که اصلاً احراز هویت در این پروژه برای چه نوع کاربری است؟
در بعضی پروژهها، auth برای همه کاربران است.
مثلاً:
- ثبتنام کاربر
- ورود کاربر
- پروفایل کاربر
- نقشهای مختلف
- پنل کاربران
اما در پروژه پرتفولیو یا CMS شخصی، معمولاً مدل سادهتر است.
اینجا auth بیشتر برای این فرد یا این گروه است:
- صاحب سایت
- ادمین
- مدیر محتوا
- کسی که قرار است پروژه، مقاله و محتوای سایت را مدیریت کند
پس ما فعلاً با یک سیستم عمومی user auth برای همه بازدیدکنندهها طرف نیستیم.
ما داریم یک admin auth طراحی میکنیم.
این تفاوت خیلی مهم است، چون باعث میشود معماری پروژه را سادهتر و واقعیتر نگه داریم.
یعنی فعلاً هدف ما این نیست که:
- کاربران عمومی ثبتنام کنند
- همه بتوانند login داشته باشند
- نقشهای پیچیده کاربری تعریف کنیم
هدف این است که فقط ادمین بتواند وارد شود و به بخش مدیریت دسترسی داشته باشد.
مدل کاربر ادمین
برای اینکه ادمین بتواند وارد سیستم شود، باید در دیتابیس یک مدل کاربر داشته باشیم.
یک مدل ساده در Prisma میتواند اینطور باشد:
model AdminUser {
id String @id @default(cuid())
email String @unique
passwordHash String
name String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
در این مدل چند نکته مهم وجود دارد:
emailباید یکتا باشد- رمز عبور را بهصورت مستقیم ذخیره نمیکنیم
- بهجای رمز خام، مقدار
passwordHashرا نگه میداریم
این یعنی اگر کاربر با این اطلاعات وارد شود:
email: admin@example.com
password: 12345678
ما نباید 12345678 را مستقیم داخل دیتابیس ذخیره کنیم.
بلکه باید نسخه هششده آن را ذخیره کنیم.
در قسمت بعد که وارد bcrypt میشویم، دقیقاً همین موضوع را پیادهسازی میکنیم.
اگر بخواهی مدل را کمی توسعهپذیرتر کنی، میتوانی فیلد نقش هم اضافه کنی:
role String @default("admin")
اما برای این پروژه، اگر فقط یک ادمین یا تعداد کمی مدیر محتوا داری، مدل ساده بالا کاملاً کافی است.
بعد از اضافه کردن مدل، migration را اجرا میکنیم:
npx prisma migrate dev --name add_admin_user_model
صفحه ورود
در این پروژه، صفحه ورود نقطه شروع دسترسی به پنل ادمین است.
مثلاً میتوانیم چنین مسیری داشته باشیم:
/login
یا اگر بخواهیم ساختار مشخصتر باشد:
/admin/login
هر دو قابل استفادهاند، اما برای پروژهای که بخش مدیریت جدا دارد، معمولاً این ساختار خواناتر است:
/admin/login
/admin
/admin/projects
/admin/posts
صفحه ورود معمولاً شامل این فیلدهاست:
- ایمیل
- رمز عبور
- دکمه ورود
یک فرم ساده:
<form>
<input type="email" name="email" />
<input type="password" name="password" />
<button type="submit">Login</button>
</form>
اما از نظر طراحی فقط UI مهم نیست. این صفحه باید یک رفتار مشخص داشته باشد:
- داده را از کاربر بگیرد
- به endpoint ورود بفرستد
- خطای ورود نامعتبر را نمایش دهد
- در صورت موفقیت، session ایجاد کند
- کاربر را به پنل ادمین منتقل کند
پس صفحه ورود فقط یک فرم ساده نیست؛ بخشی از جریان کامل auth است.
session چیست؟
بعد از اینکه ادمین با موفقیت وارد شد، سیستم باید somehow به خاطر بسپارد که این کاربر احراز هویت شده است.
اگر این کار را نکنیم، در هر درخواست دوباره باید ایمیل و رمز را بپرسیم، که منطقی نیست.
اینجاست که مفهوم session وارد میشود.
session یعنی یک وضعیت احراز هویت که بعد از login ایجاد میشود و در درخواستهای بعدی قابل بررسی است.
به زبان ساده:
- کاربر login میکند
- سرور موفق بودن login را تأیید میکند
- یک session برای کاربر ایجاد میشود
- در درخواستهای بعدی، سرور session را بررسی میکند
- اگر session معتبر بود، دسترسی داده میشود
در پروژههای Next.js این session معمولاً با cookie نگهداری میشود.
یعنی بعد از ورود موفق، سرور یک cookie تنظیم میکند و مرورگر آن را در درخواستهای بعدی همراه خود میفرستد.
مدل ذهنی خیلی ساده:
Login موفق -> Set Cookie -> ذخیره session -> بررسی session در مسیرهای محافظتشده
در این دوره فعلاً میخواهیم یک مدل session ساده و قابل فهم داشته باشیم، نه یک سیستم بسیار پیچیده.
مسیرهای نیازمند محافظت
قبل از پیادهسازی، باید دقیقاً مشخص کنیم چه مسیرهایی private هستند.
در این پروژه معمولاً این مسیرها باید فقط برای ادمین قابل دسترسی باشند:
/admin/admin/projects/admin/projects/new/admin/projects/[id]/edit/admin/posts/admin/posts/new/admin/posts/[id]/edit
همچنین این endpointها هم باید محافظت شوند:
POST /api/projectsPATCH /api/projects/[id]DELETE /api/projects/[id]POST /api/postsPATCH /api/posts/[id]DELETE /api/posts/[id]
در مقابل، بعضی مسیرها عمومی هستند:
//projects/projects/[slug]/blog/blog/[slug]GET /api/projectsGET /api/projects/[id]یا endpoint عمومی بر اساسslug
پس از همین حالا باید پروژه را به دو ناحیه ذهنی تقسیم کنیم:
Public Area
Admin Area
ناحیه عمومی برای بازدیدکننده است.
ناحیه ادمین فقط برای کاربر احراز هویتشده.
معماری ساده auth در این پروژه
برای اینکه روند کار واضح باشد، میتوانیم auth را اینطور طراحی کنیم:
- مدل
AdminUserدر دیتابیس - ساخت ادمین اولیه
- صفحه
/admin/login - endpoint برای login
- بررسی
emailوpassword - ساخت session بعد از ورود موفق
- ذخیره session در
cookie - بررسی session در مسیرهای محافظتشده
- خروج از حساب با حذف session
این مسیر، ساده و مناسب پروژههای پنل ادمین شخصی است.
آیا نیاز به ثبتنام داریم؟
در این نوع پروژهها معمولاً ثبتنام عمومی لازم نیست.
چون قرار نیست هر بازدیدکنندهای حساب بسازد.
ادمین معمولاً از قبل توسط خود توسعهدهنده یا مالک سایت ساخته میشود.
پس بهجای signup flow عمومی، معمولاً یکی از این دو روش را داریم:
- ساخت دستی ادمین در دیتابیس
- ساخت ادمین از طریق seed script
مثلاً بعداً میتوانیم یک فایل seed داشته باشیم که یک کاربر ادمین بسازد.
جمعبندی
در این قسمت، هنوز auth را کامل پیادهسازی نکردیم، اما طراحی آن را مشخص کردیم.
یاد گرفتیم که در این پروژه:
- auth برای بازدیدکنندههای عمومی نیست
- auth مخصوص ادمین یا مدیر محتوا است
- به مدل
AdminUserدر دیتابیس نیاز داریم - رمز عبور نباید مستقیم ذخیره شود
- بعد از login باید
sessionایجاد شود - مسیرهای ادمین و endpointهای مدیریتی باید محافظت شوند
- در این پروژه معمولاً به signup عمومی نیاز نداریم
پس از اینجا به بعد مسیر ما کاملاً روشن است:
Admin User -> Login -> Session -> Protected Admin Routes
در قسمت بعد، همین طراحی را وارد مرحله عملی میکنیم و با bcrypt یک login امن میسازیم؛ یعنی رمز را هش میکنیم، اطلاعات ورود را بررسی میکنیم و جریان ورود را کاملتر پیادهسازی میکنیم.