ساختن بک‌اند تایپ‌اسکریپتی - بخش ۴: هر کاربر، Todoهای متفاوت

NestJS
NestJS

سرفصل‌ها

  1. فریمورک NestJS چیست و چرا؟
  2. شروع پروژه Todo
  3. دیتابیس!
  4. هر کاربر، Todoهای متفاوت

مقدمه

توی بخش قبل Todoها رو توی دیتابیس ذخیره کردیم و تونستیم که اون‌ها رو صورت دائمی اونجا نگهداری کنیم. اما هیچکس نمیخواد که Todoهایی داشته باشه که بقیه بتونن اونارو ببینن و حتی ویرایش و حذف کنن!

توی این بخش سراغ یکی از بخش‌های اصلی هر API میریم. اینکه کاربرها رو اعتباریابی (authenticate) کنیم و اون‌ها رو توی سیستم خودمون بشناسیم و بتونیم داده‌های شخصی‌سازی شده برای اونها داشته باشیم.

مقدمه‌ی بیشتر

برای اینکه توی اپلیکیشن font-end کاربر رو لاگین نگه داریم چندتا استراتژی وجود داره. یکیش اینکه که وقتی کاربر توی صفحه ورود، ایمیل و پسورد رو وارد کرد، اون‌ها رو ذخیره کنیم و برای درخواست‌های بعدی همون رو بفرستیم برای backend. اونجا هم توی هر ریکوئست ایمیل و پسورد رو بررسی کنیم و کاربر رو اعتبار یابی کنیم. ولی ذخیره پسورد کاربر روش خوبی نیست. اصلا نیست!

روش دیگه اینه که وقتی کاربر ایمیل و پسورد رو وارد کرد و درست بود، ما بهش یک Token بدیم که این Token مثل بلیت کاربر برای استفاده از سیستم میمونه. ولی به مدت محدود.

از: https://blog.larapulse.com/web/jwt
از: https://blog.larapulse.com/web/jwt

روش مرسوم در APIها استفاده از JWT (Json Web Token)‎ هست. این نوع توکن از سه بخش تشکیل شده. بخش اول که بهش header میگن، یک استرینگ json هست که اطلاعاتی مثل نوع توکن و الگوریتم استفاده شده برای امضا رو توی خودش نگه میداره (سمت چپ). در آخر این استرینگ json با base64 کد شده (سمت راست). بخش دوم (payload) اطلاعاتی که ما نیاز داریم برای این توکن رو نگهداری میکنه. مثلا id کاربری که این توکن مال اون هست و یک عدد رندوم برای اینکه توکن‌ها مثل هم نباشن و حتی تاریخ منقضی شدن توکن. این بخش هم استرینگ json هست که با base64 کد شده. بخش سوم JWT، امضا (signature) از بخش اول و دوم هست که با یک کلید محرمانه که سمت سرور داریم درست میشه. با این امضا میشه مطمئن بود که توکن‌هایی که میسازیم توسط هیچ‌کسی امکان جعل شدن نداره. هر سه بخش با یک نقطه به هم وصل میشن و توکن ما رو تشکیل میدن.

نتیجه‌گیریِ مقدمه

خب توی اپ nestjs خودمون، ما نیاز به یک endpoint برای لاگین داریم که یوزرنیم و پسورد رو بگیره و اون‌ها رو چک کنه، اگر درست بود JWT برای کاربر برگردونه. از اون به بعد کافیه کاربر توی هر ریکوئست JWT رو بفرسته و ما سمت backend هر جایی که نیاز داشتیم از یک گارد (جلوتر باهاش آشنا میشیم) که JWT رو چک می‌کنه برای محافظت از endpointهایی که نیاز به اعتباریابی کاربر دارن استفاده می‌کنیم.


پیاده‌سازی...

نصب مواد مورد نیاز

برای پیاده‌سازی این ویژگی، نیاز به hash.js برای هش کردن پسورد (پسورد خام رو کسی توی دیتابیس نگهداری نمی‌کنه)، passport (کتابخونه‌ای برای اعتباریابی کاربر با استفاده از روش‌های مختلف) و ماژول ‎@nestjs/passport (برای استفاده از passport در nestjs) و ‎passport-jwt (روش JWT در passport) و همچنین ‎@nestjs/jwt (ماژولی برای راحت کردن کار با JWT) داریم. که خب نصب می‌کنیم.

npm install hash.js passport passport-jwt @nestjs/passport @nestjs/jwt
npm install -D @types/passport-jwt

ایجاد ماژول و entity

اول از همه یه ماژول جدید به نام auth میسازیم. این بار از nest cli استفاده می‌کنیم. کامند زیر رو اجرا می‌کنیم.

npx nest generate module auth

این کامند یک پوشه برای ماژول و کلاس ماژول رو میسازه. توی BootstrapModule هم ماژول جدید رو اضافه می‌کنه.

بعد از اون باید کلاس کاربر رو توی سیستم تعریف کنیم. توی پوشه auth یک فایل به نام user.entity.ts با محتوای زیر ایجاد می‌کنیم.

user.entity.ts
user.entity.ts

به این شکل entityمون رو توی ماژول رجیستر می‌کنیم.

auth.module.ts
auth.module.ts

سرویس JWT

اول از همه فایل auth.constants.ts رو برای نگهداری ثابت‌های ماژول ایجاد می‌کنیم.

auth.constants.ts
auth.constants.ts

قبلا ماژول ‎@nestjs/jwt رو نصب کردیم. این ماژول سرویسی به نام JwtService رو پیاده‌سازی کرده که میتونیم با ایمپورت کردن ماژول توی ماژول خودمون، به این سرویس دسترسی داشته باشیم.

auth.module.ts
auth.module.ts

ایجاد و پیاده‌سازی سرویس Auth

برای نوشتن منطق لاگین کاربر نیاز به یک سرویس داریم. اون هم اینطوری ایجاد می‌کنیم:

npx nest generate service auth

این دستور یک سرویس به نام auth توی پوشه auth (پوشه ماژولمون) ایجاد می‌کنه و سرویس رو توی ماژول رجیستر می‌کنه. یک فایل تست (spec) هم ایجاد می‌کنه که پاکش می‌کنیم. (سعی کنید تست رو توی روند کار خودتون داشته باشید، به علت ضیغ وقت توی این مقاله تست نداریم)

توی سرویس قرار هست که یوزرنیم و پسورد کاربر رو بگیریم و اگر درست بود JWT برگردونیم برای کاربر. اگر هم درست نبود خطایی رو نشون کاربر میدیم.

auth.service.ts
auth.service.ts

همونطور که میبینید، توی constructor، آبجکت JwtService و User Repository رو دریافت کردیم. توی متد لاگین که نوشتیم، email و password رو دریافت کردیم، دنبال یوزری با ایمیل داده شده می‌گردیم. اگر همچین کاربری پیدا نشه، متد findOne مقدار undefined برمیگردونه و اگر پیدا بشه، آبجکت user رو. یوزر پیدا نشده باشه خطایی رو پرتاب می‌کنیم که Nest خطایی برای کاربر میفرسته.

اگر کاربر پیدا شده باشه، هش پسورد ارسال شده توسط کاربر رو حساب می‌کنیم و با هش پسورد ذخیره شده توی دیتابیس مقایسه می‌کنیم. اگر هر دو هش یکی باشه، یعنی پسورد اولیه که به تابع هش دادیم هم یکی بوده و پسورد درست هست. بنابراین توکن Jwt رو با استفاده از JwtService ایجاد و امضا می‌کنیم. توی payload فقط id کاربر رو میذاریم. پارامتر بعدی هم زمان منقضی شدن توکن هست که ما 24 ساعت در نظر گرفتیم (بر حسب ثانیه هست). در آخر هم توکن رو به عنوان خروجی متد میفرستیم.

ایجاد کنترلر

برای ایجاد controller از دستور زیر استفاده می‌کنیم.

npx nest generate controller auth

اینم نگم چیکار می‌کنه دیگه.. خودتون میدونید ;)

نکته: دستورات حالت خلاصه هم دارن، میتونید با npx nest راهنمای دستورات و خلاصه‌ها رو ببینید.

بعد از generate کردن، به این شکل پیاده‌سازی می‌کنیم.

auth.module.ts
auth.module.ts

توی constructor فقط AuthService رو دریافت می‌کنیم. بعد از اون یک متد با دکوراتور Post داریم که فقط برای متد HTTP POST کار می‌کنه. توی اون با استفاده از Body، از اطلاعات پست شده توسط کاربر، email و password رو میگیریم و خیلی ساده متد login از AuthService رو صدا می‌زنیم و محتوای اون رو برمیگردونیم تا برای کاربر ارسال بشه.

تست، تست، تست

بهتره تا اینجای کار رو یک دور تست کنیم، بعد ادامه کار رو داشته باشیم. از اونجایی که هنوز کاربری توی دیتابیس نداریم و قابلیت ثبت‌نام کاربر هم نداریم، دستی دیتابیس رو باز می‌کنیم و کاربر رو اضافه می‌کنیم.

برای این کار یک بار npm run start:dev رو بزنید که دیتابیس با entityها سینک بشه.

بعدش SQLite Browser رو نصب کنید. توی این برنامه، Open Database رو بزنید، فایل app.db پروژه رو باز کنید. سپس روی جدول user راست کلیک کنید و Browse Table رو بزنید. دکمه New Record رو بزنید و توی سطر ایجاد شده، ایمیل و هش پسورد زیر رو وارد کنید. بعد هم Write Changes رو وارد کنید.

email: sample@sample.com
password: 3627909a29c31381a071ec27f7c9ca97726182aed29a7ddd2e54353322cfb30abb9e3a6df2ac2c20fe23436311d678564d0c8d305930575f60e2d3d048184d79

این متن پسورد، هش شده‌ی 12345 هست. حالا میتونیم با این کاربر، لاگین رو تست کنیم.

این ریکوئست رو توی Postman وارد می‌کنیم. و بعد از پست کردن اون.. توکن رو دریافت می‌کنیم. پس همه مراحل خوب انجام شده و لاگینمون کار می‌کنه.

Postman login request
Postman login request

پیاده‌سازی Jwt Guard

توی nestjs، گاردها ساختاری هستن برای محافظت کنترلرهامون. توی ماژول ‎@nestjs/passport گارد از قبل تعریف شده‌ای وجود داره که در لایه‌های زیرین از کتابخونه passport برای لاگین کاربر استفاده می‌کنه. کار ما این هست که Jwt Strategy رو برای استفاده درون این ماژول تعریف کنیم.

فایل jwt.strategy.ts رو میسازیم.

jwt.strategy.ts
jwt.strategy.ts

متد getUserById که به AuthService اضافه شده، به صورت زیر هست:

بخشی از auth.service.ts
بخشی از auth.service.ts

توی این استراتژی، از استراتژی موجود در passport-jwt ارث بری می‌کنیم و اون رو با مقادیر خودمون کانفیگ می‌کنیم. توی constructor این کلاس، constructor کلاس ارث‌بری شده رو با super صدا میزنیم و مقادیر لازم رو بهش ارسال می‌کنیم. توی متد validate هم، payload جدا شده از JWT برای ما ارسال میشه و باید بررسی کنیم که آیا درست هست یا نه. چون ما id رو توی payload ذخیره می‌کردیم، کاربری با این id پیدا بشه یعنی JWT درسته، پس کاربر رو برمیگردونیم. اگر نه خطا میدیم.

حالا کافیه توی ماژول auth، کلاس JwtStrategy و ماژول PassportModule رو رجیستر کنیم.

auth.module.ts
auth.module.ts

الان میتونیم از AuthGuard که توسط ماژول ‎@nestjs/passport پیاده‌سازی شده و در لایه زیرین از کتابخونه passport استفاده می‌کنه و به JwtStrategy که پیاده‌سازی کردیم وصله، توی کنترلرمون استفاده کنیم.

بخشی از todo.controller.ts
بخشی از todo.controller.ts

با این کار استفاده از کنترلر رو ملزم به ارسال توکن Jwt میکنیم و passport از JwtStrategy ما استفاده می‌کنه و کاربر رو اعتباریابی می‌کنه.


هر کاربر، Todoهای متفاوت

برای اینکه هر کدوم از کاربرها Todoهای متفاوتی داشته باشن باید ساختار entity برای Todo رو تغییر بدیم. ارتباط Todo به User یک ارتباط چند به یک هست. یعنی چند Todo میتونن به یک کاربر ربط داده بشن.

بخشی از todo.entity.ts
بخشی از todo.entity.ts

اینجا ما یک property جدید توی کلاسمون ایجاد کردیم و با دکوراتور ManyToOne، رابطه اون با User رو مشخص کردیم.

نکته: اگر از قبل Todo توی دیتابیس داشتید، احتمالا با اجرای برنامه به دلیل تغییر ساختار جداول به مشکل میخورید. باید فایل app.db قبلی رو پاک کنید و دوباره برنامه رو اجرا کنید تا فایل با ساختار جدید ایجاد بشه. البته همه Todo ها و کاربری که چند پاراگراف قبل ایجاد کردیم هم پاک میشه. کاربر رو دوباره ایجاد کنید که بهش نیاز داریم.

حالا سرویسمون رو جوری می‌نویسیم که موقع ذخیره Todo و گرفتن از دیتابیس، User رو در نظر بگیره.

todo.service.ts
todo.service.ts

همونطور که میبینید، توی constructor، آبجکت ریکوئست رو دریافت می‌کنیم. این آبجکت اطلاعاتی راجع به ریکوئست کاربر در اختیار داره. مثل header های ریکوئست. هنگامی هم که از ماژول passport استفاده می‌کنید، کاربری که اعتباریابی اون انجام شده به این آبجکت اضافه میشه، پس با ریکوئست به آبجکت کاربر دسترسی داریم.

حالا کافیه هر جایی که نیاز هست، این کاربر رو در ذخیره کردن و گرفتن Todoها از دیتابیس دخیل کنیم. توی شرط‌هایی find و توی آبجکتی که برای save میفرستیم!

تست

برای تست این مطالب تازه اضافه شده، ابتدا باید لاگین کنید و یک توکن جدید بگیرید. لاگین رو که قبلا گفتیم...

با در دست داشتن توکن، توی Headerهامون، یک بخش به صورت:

Authorization: Bearer {token}

اضافه می‌کنیم و درخواست‌هامون رو با این Header ارسال می‌کنیم. بقیه مراحل مثل بخش اول هست. برای ایجاد و آپدیت و دیلیت و گرفتن Todoها میتونید endpointهای مختلفی که داشتیم رو تست کنید.

Postman Authorization header
Postman Authorization header

سخن پایانی

امیدوارم که این نوشته برای شما مفید بوده باشه. بخش آخر یکم عجله‌ای شد. اگر مشکلی در نوشته‌ها هست یا سوالی در این خصوص هست میتونید توی کامنت‌ها بگید.

سورس کامل پروژه هم میتونید توی این مخزن گیت‌هاب پیدا کنید.