ارسال خودکار هدرهای ویژهی Authorization، به سمت سرور
در برنامهی backend این سری (که از انتهای مطلب قابل دریافت است)، به Controllers\MoviesController.cs مراجعه کرده و متدهای Get/Delete/Create آنرا با فیلتر [Authorize] مزین میکنیم تا دسترسی به آنها، تنها به کاربران لاگین شدهی در سیستم، محدود شود. در این حالت اگر به برنامهی React مراجعه کرده و برای مثال سعی در ویرایش رکوردی کنیم، اتفاقی رخ نخواهد داد:
علت را نیز در برگهی network کنسول توسعه دهندگان مرورگر، میتوان مشاهده کرد. این درخواست از سمت سرور با Status Code: 401، برگشت خوردهاست. برای رفع این مشکل باید JSON web token ای را که در حین لاگین، از سمت سرور دریافت کرده بودیم، به همراه درخواست خود، مجددا به سمت سرور ارسال کنیم. این ارسال نیز باید به صورت یک هدر مخصوص با کلید Authorization و مقدار "Bearer jwt" باشد.
به همین جهت ابتدا به src\services\authService.js مراجعه کرده و متدی را برای بازگشت JWT ذخیره شدهی در local storage به آن اضافه میکنیم:
سپس به src\services\httpService.js مراجعه کرده و از آن استفاده میکنیم:
کار این یک سطر که در ابتدای ماژول httpService قرار میگیرد، تنظیم هدرهای پیشفرض تمام انواع درخواستهای ارسالی توسط Axios است. البته میتوان از حالتهای اختصاصیتری نیز مانند فقط post، بجای common استفاده کرد. برای نمونه در تنظیم فوق، تمام درخواستهای HTTP Get/Post/Delete/Put ارسالی توسط Axios، دارای هدر Authorization که مقدار آن به ثابتی شروع شدهی با Bearer و سپس مقدار JWT دریافتی از سرور تنظیم میشود، خواهند بود.
مشکل! اگر برنامه را در این حالت اجرا کنید، یک چنین خطایی را مشاهده خواهید کرد:
علت اینجا است که سرویس httpService، دارای ارجاعی به سرویس authService شدهاست و برعکس (در httpService، یک import از authService را داریم و در authService، یک import از httpService را)! یعنی یک وابستگی حلقوی و دو طرفه رخدادهاست.
برای رفع این خطا باید ابتدا مشخص کنیم که کدامیک از این ماژولها، اصلی است و کدامیک باید وابستهی به دیگری باشد. در این حالت httpService، ماژول اصلی است و بدون آن و با نبود امکان اتصال به backend، دیگر authService قابل استفاده نخواهد بود.
به همین جهت به httpService مراجعه کرده و import مربوط به authService را از آن حذف میکنیم. سپس در همینجا متدی را برای تنظیم هدر Authorizationاضافه کرده و آنرا به لیست default exports این ماژول نیز اضافه میکنیم:
در آخر، در authService که ارجاعی را به httpService دارد، فراخوانی متد setJwt را در ابتدای ماژول، انجام خواهیم داد:
به این ترتیب، وابستگی حلقوی بین این دو ماژول برطرف میشود و اکنون این authService است که به httpService وابستهاست و نه برعکس.
تا اینجا اگر تغییرات را ذخیره کرده و سعی در ویرایش یکی از رکوردهای فیلمهای نمایش داده شده کنیم، اینکار با موفقیت انجام میشود؛ چون اینبار درخواست ارسالی، دارای هدر ویژهی authorization است:
روش بررسی انقضای توکنها در سمت کلاینت
اگر JWT قدیمی و منقضی شدهی از روز گذشته را آزمایش کنید، باز هم از سمت سرور، Status Code: 401 دریافت خواهد شد. اما اینبار در لاگهای برنامهی سمت سرور، OnChallenge error مشخص است. در این حالت باید یکبار logout کرد تا JWT قدیمی حذف شود. سپس نیاز به لاگین مجدد است تا یک JWT جدید دریافت گردد. میتوان اینکار را پیش از ارسال اطلاعات به سمت سرور، در سمت کلاینت نیز بررسی کرد:
در اینجا user، همان شیء حاصل از const user = jwtDecode(jwt) است که در قسمت قبل به آن پرداختیم. سپس خاصیت exp آن با زمان جاری مقایسه شده و در صورت وجود مشکلی، استثنایی را صادر میکند. میتوان این متد را پس از فراخوانی jwtDecode، قرار داد.
محدود کردن حذف رکوردهای فیلمها به نقش Admin در Backend
تا اینجا تمام کاربران وارد شدهی به سیستم، میتوانند علاوه بر ویرایش فیلمها، آنها را نیز حذف کنند. به همین جهت میخواهیم دسترسی حذف را از کاربرانی که ادمین نیستند، بگیریم. برای این منظور، در سمت سرور کافی است در کنترلر MoviesController، ویژگی [Authorize(Policy = CustomRoles.Admin)] را به اکشن متد Delete، اضافه کنیم. به این ترتیب اگر کاربری در سیستم ادمین نبود و درخواست حذف رکوردی را صادر کرد، خطای 403 را از سمت سرور دریافت میکند:
در برنامهی مثال backend این سری، در فایل Services\UsersDataSource.cs، یک کاربر ادمین پیشفرض ثبت شدهاست. مابقی کاربرانی که به صورت معمولی در سایت ثبت نام میکنند، ادمین نیستند.
در این حالت اگر کاربری ادمین بود، چون در توکن او که در فایل Services\TokenFactoryService.cs صادر میشود، یک User Claim ویژهی از نوع Role و با مقدار Admin وجود دارد:
این مقدار، در payload توکن نهایی او نیز ظاهر خواهد شد:
بنابراین هربار که برنامهی React ما، هدر Bearer jwt را به سمت سرور ارسال میکند، فیلتر Authorize محدود شدهی به نقش ادمین، این نقش را در صورت وجود در توکن او، پردازش کرده و دسترسیهای لازم را به صورت خودکار صادر میکند و همانطور که پیشتر نیز عنوان شد، اگر کاربری این نقش را به صورت دستی به توکن ارسالی به سمت سرور اضافه کند، به دلیل دسترسی نداشتن به کلیدهای خصوصی تولید مجدد امضای دیجیتال توکن، درخواست او در سمت سرور تعیین اعتبار نشده و برگشت خواهد خورد.
نکته 1: اگر در اینجا چندین بار یک User Claim را با مقادیر متفاوتی، به لیست claims اضافه کنیم، مقادیر آن در خروجی نهایی، به شکل یک آرایه ظاهر میگردند.
نکته 2: پیاده سازی سمت سرور backend این سری، یک باگ امنیتی مهم را دارد! در حین ثبت نام، کاربران میتوانند مقدار خاصیت isAdmin شیء User را:
خودشان دستی تنظیم کرده و ارسال کنند تا به صورت ادمین ثبت شوند! به این مشکل مهم، اصطلاحا mass assignment گفته میشود.
راه حل اصولی مقابلهی با آن، داشتن یک DTO و یا ViewModel خاص قسمت ثبت نام و جدا کردن مدل متناظر با موجودیت User، از شیءای است که اطلاعات نهایی را از کاربر، دریافت میکند. شیءای که اطلاعات را از کاربر دریافت میکند، نباید دارای خاصیت isAdmin قابل تنظیم در حین ثبت نام معمولی کاربران سایت باشد. یک روش دیگر حل این مشکل، استفاده از ویژگی Bind و ذکر صریح نام خواصی است که قرار است bind شوند و نه هیچ خاصیت دیگری از شیء User:
و یا حتی میتوان ویژگی [BindNever] را بر روی خاصیت IsAdmin، در مدل User قرار داد.
نکته 3: اگر میخواهید در برنامهی React، با مواجه شدن با خطای 403 از سمت سرور، کاربر را به یک صفحهی عمومی «دسترسی ندارید» هدایت کنید، میتوانید از interceptor سراسری که در قسمت 24 تعریف کردیم، استفاده کنید. در اینجا status code = 403 را جهت history.push به یک آدرس access-denied سفارشی و جدید، پردازش کنید.
نمایش یا مخفی کردن المانها بر اساس سطوح دسترسی کاربر وارد شدهی به سیستم
میخواهیم در صفحهی نمایش لیست فیلمها، دکمهی new movie را که بالای صفحه قرار دارد، به کاربرانی که لاگین نکردهاند، نمایش ندهیم. همچنین نمیخواهیم اینگونه کاربران، بتوانند فیلمی را ویرایش و یا حذف کنند؛ یعنی لینک به صفحهی جزئیات ویرایشی فیلمها و ستونی که دکمههای حذف هر ردیف را نمایش میدهد، به کاربران وارد نشدهی به سیستم نمایش داده نشوند.
در قسمت قبل، در فایل app.js، شیء currentUser را به state اضافه کردیم و با استفاده از ارسال آن به کامپوننت NavBar:
نام کاربر وارد شدهی به سیستم را نمایش دادیم. با استفاده از همین روش میتوان شیء currentUser را به کامپوننت Movies ارسال کرد و سپس بر اساس محتوای آن، قسمتهای مختلف صفحه را مخفی کرد و یا نمایش داد. البته در اینجا (در فایل app.js) خود کامپوننت Movies درج نشدهاست؛ بلکه مسیریابی آنرا تعریف کردهایم که با روش ارسال پارامتر به یک مسیریابی، در قسمت 15، قابل تغییر و پیاده سازی است:
در اینجا برای ارسال props به یک کامپوننت، نیاز است از ویژگی render استفاده شود. سپس پارامتر arrow function را به همان props تنظیم میکنیم. همچنین با استفاده از spread operator، این props را در المان JSX تعریف شده، گسترده و تزریق میکنیم؛ تا از سایر خواص پیشینی که تزریق شده بودند مانند history، location و match، محروم نشویم و آنها را از دست ندهیم. در نهایت المان کامپوننت مدنظر را همانند روش متداولی که برای تعریف تمام کامپوننتهای React و تنظیم ویژگیهای آنها استفاده میشود، بازگشت میدهیم.
پس از این تغییر به فایل src\components\movies.jsx مراجعه کرده و شیء user را در متد رندر، دریافت میکنیم:
اکنون که در کامپوننت Movies به این شیء user دسترسی پیدا کردیم، توسط آن میتوان قسمتهای مختلف صفحه را مخفی کرد و یا نمایش داد:
با این تغییر، اگر شیء user مقدار دهی شده باشد، عبارت پس از &&، در صفحه درج خواهد شد و برعکس:
در این تصویر همانطور که مشخص است، کاربر هنوز به سیستم وارد نشدهاست؛ بنابراین به علت null بودن شیء user، دکمهی New Movie را مشاهده نمیکند.
روش دریافت نقشهای کاربر وارد شدهی به سیستم در سمت کلاینت
همانطور که پیشتر در مطلب جاری عنوان شد، نقشهای دریافتی از سرور، یک چنین شکلی را در jwtDecode نهایی (یا user در اینجا) دارند:
که البته اگر چندین Role تعریف شده باشند، مقادیر آنها در خروجی نهایی، به شکل یک آرایه ظاهر میگردد. بنابراین برای بررسی آنها میتوان نوشت:
کار این متد، دریافت نقش و یا نقشهای ممکن از jwtDecode، و بازگشت آنها (افزودن آنها به صورت یک خاصیت جدید، به نام roles، به شیء user دریافتی) به صورت یک آرایهی با عناصری LowerCase است. سپس اگر نیاز به بررسی نقش، یا نقشهای کاربری خاص بود، میتوان از یکی از متدهای زیر استفاده کرد:
متد isAuthUserInRoles، آرایهای از نقشها را دریافت میکند و سپس بررسی میکند که آیا کاربر انتخابی، دارای این نقشها هست یا خیر و متد isAuthUserInRole، تنها یک نقش را بررسی میکند.
در این کدها، adminRoleName به صورت زیر تامین شدهاست:
یعنی محتویات فایل config.json تعریف شده را به صورت زیر با افزودن نام نقش ادمین، تغییر دادهایم:
عدم نمایش ستون Delete ردیفهای لیست فیلمها، به کاربرانی که Admin نیستند
اکنون که امکان بررسی نقشهای کاربر لاگین شدهی به سیستم را داریم، میخواهیم ستون Delete ردیفهای لیست فیلمها را فقط به کاربری که دارای نقش Admin است، نمایش دهیم. برای اینکار نیاز به دریافت شیء user، در src\components\moviesTable.jsx وجود دارد. یک روش دریافت کاربر جاری وارد شدهی به سیستم، همانی است که تا به اینجا بررسی کردیم: شیء currentUser را به صورت props، از بالاترین کامپوننت، به پایینتر کامپوننت موجود در component tree ارسال میکنیم. روش دیگر اینکار، دریافت مستقیم کاربر جاری از خود src\services\authService.js است و ... اینکار سادهتر است! به علاوه اینکه همیشه بررسی تاریخ انقضای توکن را نیز به صورت خودکار انجام میدهد و در صورت انقضای توکن، کاربر را در قسمت catch متد getCurrentUser، از سیستم خارج خواهد کرد.
بنابراین در src\components\moviesTable.jsx، ابتدا authService را import میکنیم:
در ادامه ابتدا تعریف ستون حذف را از آرایهی columns خارج کرده و تبدیل به یک خاصیت میکنیم. یعنی در ابتدای کار، چنین ستونی تعریف نشدهاست:
در آخر در متد سازندهی این کامپوننت، کاربر جاری را از authService دریافت کرده و اگر این کاربر دارای نقش Admin بود، ستون deleteColumn را به لیست ستونهای موجود، اضافه میکنیم تا نمایش داده شود:
اکنون برای آزمایش برنامه، یکبار از آن خارج شوید؛ دیگر نباید ستون Delete نمایش داده شود. همچنین یکبار هم تحت عنوان یک کاربر معمولی در سایت ثبت نام کنید. این کاربر نیز چنین ستونی را مشاهده نمیکند.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-28-backend.zip و sample-28-frontend.zip
در برنامهی backend این سری (که از انتهای مطلب قابل دریافت است)، به Controllers\MoviesController.cs مراجعه کرده و متدهای Get/Delete/Create آنرا با فیلتر [Authorize] مزین میکنیم تا دسترسی به آنها، تنها به کاربران لاگین شدهی در سیستم، محدود شود. در این حالت اگر به برنامهی React مراجعه کرده و برای مثال سعی در ویرایش رکوردی کنیم، اتفاقی رخ نخواهد داد:
علت را نیز در برگهی network کنسول توسعه دهندگان مرورگر، میتوان مشاهده کرد. این درخواست از سمت سرور با Status Code: 401، برگشت خوردهاست. برای رفع این مشکل باید JSON web token ای را که در حین لاگین، از سمت سرور دریافت کرده بودیم، به همراه درخواست خود، مجددا به سمت سرور ارسال کنیم. این ارسال نیز باید به صورت یک هدر مخصوص با کلید Authorization و مقدار "Bearer jwt" باشد.
به همین جهت ابتدا به src\services\authService.js مراجعه کرده و متدی را برای بازگشت JWT ذخیره شدهی در local storage به آن اضافه میکنیم:
export function getLocalJwt(){ return localStorage.getItem(tokenKey); }
import * as auth from "./authService"; axios.defaults.headers.common["Authorization"] = "Bearer " + auth.getLocalJwt();
مشکل! اگر برنامه را در این حالت اجرا کنید، یک چنین خطایی را مشاهده خواهید کرد:
Uncaught ReferenceError: Cannot access 'tokenKey' before initialization
برای رفع این خطا باید ابتدا مشخص کنیم که کدامیک از این ماژولها، اصلی است و کدامیک باید وابستهی به دیگری باشد. در این حالت httpService، ماژول اصلی است و بدون آن و با نبود امکان اتصال به backend، دیگر authService قابل استفاده نخواهد بود.
به همین جهت به httpService مراجعه کرده و import مربوط به authService را از آن حذف میکنیم. سپس در همینجا متدی را برای تنظیم هدر Authorizationاضافه کرده و آنرا به لیست default exports این ماژول نیز اضافه میکنیم:
function setJwt(jwt) { axios.defaults.headers.common["Authorization"] = "Bearer " + jwt; } //... export default { // ... setJwt };
http.setJwt(getLocalJwt());
تا اینجا اگر تغییرات را ذخیره کرده و سعی در ویرایش یکی از رکوردهای فیلمهای نمایش داده شده کنیم، اینکار با موفقیت انجام میشود؛ چون اینبار درخواست ارسالی، دارای هدر ویژهی authorization است:
روش بررسی انقضای توکنها در سمت کلاینت
اگر JWT قدیمی و منقضی شدهی از روز گذشته را آزمایش کنید، باز هم از سمت سرور، Status Code: 401 دریافت خواهد شد. اما اینبار در لاگهای برنامهی سمت سرور، OnChallenge error مشخص است. در این حالت باید یکبار logout کرد تا JWT قدیمی حذف شود. سپس نیاز به لاگین مجدد است تا یک JWT جدید دریافت گردد. میتوان اینکار را پیش از ارسال اطلاعات به سمت سرور، در سمت کلاینت نیز بررسی کرد:
function checkExpirationDate(user) { if (!user || !user.exp) { throw new Error("This access token doesn't have an expiration date!"); } user.expirationDateUtc = new Date(0); // The 0 sets the date to the epoch user.expirationDateUtc.setUTCSeconds(user.exp); const isAccessTokenTokenExpired = user.expirationDateUtc.valueOf() < new Date().valueOf(); if (isAccessTokenTokenExpired) { throw new Error("This access token is expired!"); } }
محدود کردن حذف رکوردهای فیلمها به نقش Admin در Backend
تا اینجا تمام کاربران وارد شدهی به سیستم، میتوانند علاوه بر ویرایش فیلمها، آنها را نیز حذف کنند. به همین جهت میخواهیم دسترسی حذف را از کاربرانی که ادمین نیستند، بگیریم. برای این منظور، در سمت سرور کافی است در کنترلر MoviesController، ویژگی [Authorize(Policy = CustomRoles.Admin)] را به اکشن متد Delete، اضافه کنیم. به این ترتیب اگر کاربری در سیستم ادمین نبود و درخواست حذف رکوردی را صادر کرد، خطای 403 را از سمت سرور دریافت میکند:
در برنامهی مثال backend این سری، در فایل Services\UsersDataSource.cs، یک کاربر ادمین پیشفرض ثبت شدهاست. مابقی کاربرانی که به صورت معمولی در سایت ثبت نام میکنند، ادمین نیستند.
در این حالت اگر کاربری ادمین بود، چون در توکن او که در فایل Services\TokenFactoryService.cs صادر میشود، یک User Claim ویژهی از نوع Role و با مقدار Admin وجود دارد:
if (user.IsAdmin) { claims.Add(new Claim(ClaimTypes.Role, CustomRoles.Admin, ClaimValueTypes.String, _configuration.Value.Issuer)); }
{ // ... "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin", // ... }
نکته 1: اگر در اینجا چندین بار یک User Claim را با مقادیر متفاوتی، به لیست claims اضافه کنیم، مقادیر آن در خروجی نهایی، به شکل یک آرایه ظاهر میگردند.
نکته 2: پیاده سازی سمت سرور backend این سری، یک باگ امنیتی مهم را دارد! در حین ثبت نام، کاربران میتوانند مقدار خاصیت isAdmin شیء User را:
public class User : BaseModel { [Required, MinLength(2), MaxLength(50)] public string Name { set; get; } [Required, MinLength(5), MaxLength(255)] public string Email { set; get; } [Required, MinLength(5), MaxLength(1024)] public string Password { set; get; } public bool IsAdmin { set; get; } }
راه حل اصولی مقابلهی با آن، داشتن یک DTO و یا ViewModel خاص قسمت ثبت نام و جدا کردن مدل متناظر با موجودیت User، از شیءای است که اطلاعات نهایی را از کاربر، دریافت میکند. شیءای که اطلاعات را از کاربر دریافت میکند، نباید دارای خاصیت isAdmin قابل تنظیم در حین ثبت نام معمولی کاربران سایت باشد. یک روش دیگر حل این مشکل، استفاده از ویژگی Bind و ذکر صریح نام خواصی است که قرار است bind شوند و نه هیچ خاصیت دیگری از شیء User:
[HttpPost] public ActionResult<User> Create( [FromBody] [Bind(nameof(Models.User.Name), nameof(Models.User.Email), nameof(Models.User.Password))] User data) {
نکته 3: اگر میخواهید در برنامهی React، با مواجه شدن با خطای 403 از سمت سرور، کاربر را به یک صفحهی عمومی «دسترسی ندارید» هدایت کنید، میتوانید از interceptor سراسری که در قسمت 24 تعریف کردیم، استفاده کنید. در اینجا status code = 403 را جهت history.push به یک آدرس access-denied سفارشی و جدید، پردازش کنید.
نمایش یا مخفی کردن المانها بر اساس سطوح دسترسی کاربر وارد شدهی به سیستم
میخواهیم در صفحهی نمایش لیست فیلمها، دکمهی new movie را که بالای صفحه قرار دارد، به کاربرانی که لاگین نکردهاند، نمایش ندهیم. همچنین نمیخواهیم اینگونه کاربران، بتوانند فیلمی را ویرایش و یا حذف کنند؛ یعنی لینک به صفحهی جزئیات ویرایشی فیلمها و ستونی که دکمههای حذف هر ردیف را نمایش میدهد، به کاربران وارد نشدهی به سیستم نمایش داده نشوند.
در قسمت قبل، در فایل app.js، شیء currentUser را به state اضافه کردیم و با استفاده از ارسال آن به کامپوننت NavBar:
<NavBar user={this.state.currentUser} />
<Route path="/movies" render={props => <Movies {...props} user={this.state.currentUser} />} />
پس از این تغییر به فایل src\components\movies.jsx مراجعه کرده و شیء user را در متد رندر، دریافت میکنیم:
class Movies extends Component { // ... render() { const { user } = this.props; // ...
{user && ( <Link to="/movies/new" className="btn btn-primary" style={{ marginBottom: 20 }} > New Movie </Link> )}
در این تصویر همانطور که مشخص است، کاربر هنوز به سیستم وارد نشدهاست؛ بنابراین به علت null بودن شیء user، دکمهی New Movie را مشاهده نمیکند.
روش دریافت نقشهای کاربر وارد شدهی به سیستم در سمت کلاینت
همانطور که پیشتر در مطلب جاری عنوان شد، نقشهای دریافتی از سرور، یک چنین شکلی را در jwtDecode نهایی (یا user در اینجا) دارند:
{ // ... "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin", // ... }
function addRoles(user) { const roles = user["http://schemas.microsoft.com/ws/2008/06/identity/claims/role"]; if (roles) { if (Array.isArray(roles)) { user.roles = roles.map(role => role.toLowerCase()); } else { user.roles = [roles.toLowerCase()]; } } }
export function isAuthUserInRoles(user, requiredRoles) { if (!user || !user.roles) { return false; } if (user.roles.indexOf(adminRoleName.toLowerCase()) >= 0) { return true; // The `Admin` role has full access to every pages. } return requiredRoles.some(requiredRole => { if (user.roles) { return user.roles.indexOf(requiredRole.toLowerCase()) >= 0; } else { return false; } }); } export function isAuthUserInRole(user, requiredRole) { return isAuthUserInRoles(user, [requiredRole]); }
در این کدها، adminRoleName به صورت زیر تامین شدهاست:
import { adminRoleName, apiUrl } from "../config.json";
{ "apiUrl": "https://localhost:5001/api", "adminRoleName": "Admin" }
اکنون که امکان بررسی نقشهای کاربر لاگین شدهی به سیستم را داریم، میخواهیم ستون Delete ردیفهای لیست فیلمها را فقط به کاربری که دارای نقش Admin است، نمایش دهیم. برای اینکار نیاز به دریافت شیء user، در src\components\moviesTable.jsx وجود دارد. یک روش دریافت کاربر جاری وارد شدهی به سیستم، همانی است که تا به اینجا بررسی کردیم: شیء currentUser را به صورت props، از بالاترین کامپوننت، به پایینتر کامپوننت موجود در component tree ارسال میکنیم. روش دیگر اینکار، دریافت مستقیم کاربر جاری از خود src\services\authService.js است و ... اینکار سادهتر است! به علاوه اینکه همیشه بررسی تاریخ انقضای توکن را نیز به صورت خودکار انجام میدهد و در صورت انقضای توکن، کاربر را در قسمت catch متد getCurrentUser، از سیستم خارج خواهد کرد.
بنابراین در src\components\moviesTable.jsx، ابتدا authService را import میکنیم:
import * as auth from "../services/authService";
class MoviesTable extends Component { columns = [ ... ]; // ... deleteColumn = { key: "delete", content: movie => ( <button onClick={() => this.props.onDelete(movie)} className="btn btn-danger btn-sm" > Delete </button> ) };
constructor() { super(); const user = auth.getCurrentUser(); if (user && auth.isAuthUserInRole(user, "Admin")) { this.columns.push(this.deleteColumn); } }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-28-backend.zip و sample-28-frontend.zip