يجب أن يتمكن المستخدمون من تسجيل الدخول إلى تطبيقنا، وعندما يسجّل المستخدم الدخول، يجب أن تُرفق معلوماته تلقائياً بأي ملاحظات جديدة ينشئها.

سننفّذ الآن دعماً لـالمصادقة القائمة على الرموز في الواجهة الخلفية.

تُوضَّح مبادئ المصادقة القائمة على الرموز في مخطط التتابع التالي:

مخطط تتابع للمصادقة القائمة على الرموز

  • يبدأ المستخدم بتسجيل الدخول باستخدام نموذج تسجيل دخول منفَّذ بـ React
    • سنضيف نموذج تسجيل الدخول إلى الواجهة الأمامية في الجزء 5
  • يؤدي هذا إلى أن ترسل شيفرة React اسم المستخدم وكلمة المرور إلى عنوان الخادم /api/login في طلب HTTP POST.
  • إذا كان اسم المستخدم وكلمة المرور صحيحين، يولّد الخادم رمزاً (token) يعرّف بطريقة ما المستخدم المسجَّل الدخول.
    • يُوقَّع الرمز رقمياً، ما يجعل تزويره مستحيلاً (بالوسائل التشفيرية)
  • تستجيب الواجهة الخلفية برمز حالة يشير إلى نجاح العملية، وتعيد الرمز مع الاستجابة.
  • يحفظ المتصفح الرمز، مثلاً في حالة تطبيق React.
  • عندما ينشئ المستخدم ملاحظة جديدة (أو يجري عملية أخرى تتطلب التحقق من الهوية)، ترسل شيفرة React الرمز إلى الخادم مع الطلب.
  • يستخدم الخادم الرمز للتحقق من هوية المستخدم

لننفّذ أولاً وظيفة تسجيل الدخول. ثبّت مكتبة jsonwebtoken التي تتيح لنا توليد رموز JSON للويب.

npm install jsonwebtoken

تذهب شيفرة وظيفة تسجيل الدخول إلى الملف controllers/login.js.

const jwt = require('jsonwebtoken')
const bcrypt = require('bcrypt')
const loginRouter = require('express').Router()
const User = require('../models/user')

loginRouter.post('/', async (request, response) => {
  const { username, password } = request.body

  const user = await User.findOne({ username })
  const passwordCorrect = user === null
    ? false
    : await bcrypt.compare(password, user.passwordHash)

  if (!(user && passwordCorrect)) {
    return response.status(401).json({
      error: 'invalid username or password'
    })
  }

  const userForToken = {
    username: user.username,
    id: user._id,
  }

  const token = jwt.sign(userForToken, process.env.SECRET)

  response
    .status(200)
    .send({ token, username: user.username, name: user.name })
})

module.exports = loginRouter

تبدأ الشيفرة بالبحث عن المستخدم في قاعدة البيانات بواسطة username المرفق بالطلب.

const user = await User.findOne({ username })

ثم تتحقق من password المرفقة هي أيضاً بالطلب.

const passwordCorrect = user === null
  ? false
  : await bcrypt.compare(password, user.passwordHash)

ولأن كلمات المرور نفسها لا تُحفظ في قاعدة البيانات، بل تجزئات (hashes) محسوبة منها، تُستخدم الدالة bcrypt.compare للتحقق من صحة كلمة المرور:

await bcrypt.compare(password, user.passwordHash)

إذا لم يُعثر على المستخدم، أو كانت كلمة المرور غير صحيحة، تُستجاب الطلب برمز الحالة 401 unauthorized. ويُشرح سبب الفشل في جسم الاستجابة.

if (!(user && passwordCorrect)) {
  return response.status(401).json({
    error: 'invalid username or password'
  })
}

إذا كانت كلمة المرور صحيحة، يُنشأ رمز بالدالة jwt.sign. يحتوي الرمز على اسم المستخدم ومعرّف المستخدم بصيغة موقَّعة رقمياً.

const userForToken = {
  username: user.username,
  id: user._id,
}

const token = jwt.sign(userForToken, process.env.SECRET)

وُقِّع الرمز رقمياً باستخدام نص من متغير البيئة SECRET بوصفه السر. يضمن التوقيع الرقمي أن الأطراف التي تعرف السر فقط هي القادرة على توليد رمز صالح. ويجب ضبط قيمة متغير البيئة في ملف .env.

تُستجاب الطلب الناجح برمز الحالة 200 OK. ويُعاد الرمز المولَّد واسم مستخدم المستخدم في جسم الاستجابة.

response
  .status(200)
  .send({ token, username: user.username, name: user.name })

يبقى الآن أن تُضاف شيفرة تسجيل الدخول إلى التطبيق بإدراج الموجّه الجديد في app.js.

const loginRouter = require('./controllers/login')

//...

app.use('/api/login', loginRouter)

لنجرّب تسجيل الدخول باستخدام عميل REST في VS Code:

طلب REST من VS Code مع اسم المستخدم وكلمة المرور

لا يعمل. تُطبع الرسالة التالية في الطرفية:

(node:32911) UnhandledPromiseRejectionWarning: Error: secretOrPrivateKey must have a value
    at Object.module.exports [as sign] (/Users/mluukkai/opetus/_2019fullstack-koodit/osa3/notes-backend/node_modules/jsonwebtoken/sign.js:101:20)
    at loginRouter.post (/Users/mluukkai/opetus/_2019fullstack-koodit/osa3/notes-backend/controllers/login.js:26:21)
(node:32911) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 2)

يفشل الأمر jwt.sign(userForToken, process.env.SECRET). نسينا ضبط قيمة لمتغير البيئة SECRET. ويمكن أن يكون أي نص. وعندما نضبط القيمة في ملف .env (ونعيد تشغيل الخادم)، يعمل تسجيل الدخول.

يعيد تسجيل الدخول الناجح تفاصيل المستخدم والرمز:

استجابة REST من VS Code تعرض التفاصيل والرمز

يعيد اسم المستخدم أو كلمة المرور الخاطئة رسالة خطأ ورمز الحالة المناسب:

استجابة REST من VS Code لبيانات تسجيل دخول غير صحيحة

قصر إنشاء الملاحظات الجديدة على المستخدمين المسجَّلين الدخول

لنغيّر إنشاء الملاحظات الجديدة بحيث لا يصبح ممكناً إلا إذا كان طلب POST مرفقاً برمز صالح. وعندها تُحفظ الملاحظة في قائمة ملاحظات المستخدم الذي يعرّفه الرمز.

توجد طرق عديدة لإرسال الرمز من المتصفح إلى الخادم. سنستخدم ترويسة Authorization. وتخبر الترويسة أيضاً عن مخطط المصادقة المستخدم. وقد يكون هذا ضرورياً إذا قدّم الخادم طرقاً متعددة للمصادقة. تحديد المخطط يخبر الخادم بكيفية تفسير بيانات الاعتماد المرفقة.

مخطط Bearer مناسب لاحتياجاتنا.

عملياً، هذا يعني أنه إذا كان الرمز، على سبيل المثال، النص eyJhbGciOiJIUzI1NiIsInR5c2VybmFtZSI6Im1sdXVra2FpIiwiaW، فستكون قيمة ترويسة Authorization:

Bearer eyJhbGciOiJIUzI1NiIsInR5c2VybmFtZSI6Im1sdXVra2FpIiwiaW

سيتغيّر إنشاء الملاحظات الجديدة على النحو التالي (controllers/notes.js):

const jwt = require('jsonwebtoken')

// ...
  //highlight-start
const getTokenFrom = request => {
  const authorization = request.get('authorization')
  if (authorization && authorization.startsWith('Bearer ')) {
    return authorization.replace('Bearer ', '')
  }
  return null
}
  //highlight-end

notesRouter.post('/', async (request, response) => {
  const body = request.body
//highlight-start
  const decodedToken = jwt.verify(getTokenFrom(request), process.env.SECRET)
  if (!decodedToken.id) {
    return response.status(401).json({ error: 'token invalid' })
  }

  const user = await User.findById(decodedToken.id)
//highlight-end

  if (!user) {
    return response.status(400).json({ error: 'UserId missing or not valid' })
  }

  const note = new Note({
    content: body.content,
    important: body.important || false,
    user: user._id
  })

  const savedNote = await note.save()
  user.notes = user.notes.concat(savedNote._id)
  await user.save()

  response.status(201).json(savedNote)
})

تعزل الدالة المساعدة getTokenFrom الرمز من ترويسة authorization. ويُتحقق من صلاحية الرمز بالدالة jwt.verify. كما تفك هذه الدالة ترميز الرمز، أي تعيد الكائن الذي بُني عليه الرمز.

const decodedToken = jwt.verify(token, process.env.SECRET)

إذا كان الرمز مفقوداً أو غير صالح، يُطلق الاستثناء JsonWebTokenError. نحتاج إلى توسيع وسيط معالجة الأخطاء ليتعامل مع هذه الحالة تحديداً:

const errorHandler = (error, request, response, next) => {
  if (error.name === 'CastError') {
    return response.status(400).send({ error: 'malformatted id' })
  } else if (error.name === 'ValidationError') {
    return response.status(400).json({ error: error.message })
  } else if (error.name === 'MongoServerError' && error.message.includes('E11000 duplicate key error')) {
    return response.status(400).json({ error: 'expected `username` to be unique' })
  } else if (error.name ===  'JsonWebTokenError') {
    return response.status(401).json({ error: 'token invalid' })
  }

  next(error)
}

يحتوي الكائن المفكوك من الرمز على الحقلين username وid، وهما يخبران الخادم بمن أجرى الطلب.

إذا لم يحتوي الكائن المفكوك من الرمز على هوية المستخدم (decodedToken.id غير معرَّف)، يُعاد رمز حالة الخطأ 401 unauthorized ويُشرح سبب الفشل في جسم الاستجابة.

if (!decodedToken.id) {
  return response.status(401).json({
    error: 'token invalid'
  })
}

وعندما تُحدَّد هوية مُجري الطلب، يستمر التنفيذ كما كان سابقاً.

يمكن الآن إنشاء ملاحظة جديدة باستخدام Postman إذا أُعطيت ترويسة authorization القيمة الصحيحة، أي النص Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ، حيث القيمة الثانية هي الرمز الذي أعادته عملية login.

يبدو هذا باستخدام Postman كما يلي:

إضافة رمز bearer في Postman

ومع عميل REST في Visual Studio Code

مثال إضافة رمز bearer في VS Code

يمكن العثور على شيفرة التطبيق الحالية في GitHub، في الفرع part4-9.

إذا كان للتطبيق واجهات متعددة تتطلب التحقق من الهوية، فينبغي فصل التحقق من JWT في وسيط خاص به. ويمكن أيضاً استخدام مكتبة جاهزة مثل express-jwt.

مشكلات المصادقة القائمة على الرموز

المصادقة بالرموز سهلة التنفيذ نسبياً، لكنها تنطوي على مشكلة واحدة. فبمجرد أن يحصل مستخدم الـ API، مثل تطبيق React، على رمز، تثق الـ API ثقة عمياء بحامل الرمز. فماذا لو لزم سحب صلاحيات الوصول من حامل الرمز؟

يوجد حلان للمشكلة. الأسهل هو تحديد مدة صلاحية للرمز:

loginRouter.post('/', async (request, response) => {
  const { username, password } = request.body

  const user = await User.findOne({ username })
  const passwordCorrect = user === null
    ? false
    : await bcrypt.compare(password, user.passwordHash)

  if (!(user && passwordCorrect)) {
    return response.status(401).json({
      error: 'invalid username or password'
    })
  }

  const userForToken = {
    username: user.username,
    id: user._id,
  }

  // تنتهي صلاحية الرمز بعد 60*60 ثانية، أي بعد ساعة واحدة
  // highlight-start
  const token = jwt.sign(
    userForToken, 
    process.env.SECRET,
    { expiresIn: 60*60 }
  )
  // highlight-end

  response
    .status(200)
    .send({ token, username: user.username, name: user.name })
})

بمجرد انتهاء صلاحية الرمز، يحتاج تطبيق العميل إلى الحصول على رمز جديد. ويحدث هذا عادةً بإجبار المستخدم على إعادة تسجيل الدخول إلى التطبيق.

ينبغي توسيع وسيط معالجة الأخطاء ليعطي خطأ مناسباً في حالة انتهاء صلاحية الرمز:

const errorHandler = (error, request, response, next) => {
  logger.error(error.message)

  if (error.name === 'CastError') {
    return response.status(400).send({ error: 'malformatted id' })
  } else if (error.name === 'ValidationError') {
    return response.status(400).json({ error: error.message })
  } else if (error.name === 'MongoServerError' && error.message.includes('E11000 duplicate key error')) {
    return response.status(400).json({
      error: 'expected `username` to be unique'
    })
  } else if (error.name === 'JsonWebTokenError') {
    return response.status(401).json({
      error: 'invalid token'
    })
  // highlight-start  
  } else if (error.name === 'TokenExpiredError') {
    return response.status(401).json({
      error: 'token expired'
    })
  }
  // highlight-end

  next(error)
}

كلما قصرت مدة الانتهاء، كان الحل أكثر أماناً. فإذا وقع الرمز في أيدٍ غير أمينة أو لزم سحب وصول المستخدم إلى النظام، فلا يكون الرمز قابلاً للاستخدام إلا لمدة محدودة. غير أن مدة الانتهاء القصيرة قد تكون مصدر إزعاج للمستخدم، لأنها تتطلب منه تسجيل الدخول بتكرار أكبر.

الحل الآخر هو حفظ معلومات عن كل رمز في قاعدة بيانات الواجهة الخلفية، والتحقق عند كل طلب API مما إذا كانت صلاحيات الوصول المقابلة للرموز ما تزال صالحة. وبهذا المخطط يمكن سحب صلاحيات الوصول في أي وقت. وغالباً ما يُسمى هذا النوع من الحلول جلسة من جهة الخادم.

الجانب السلبي للجلسات من جهة الخادم هو تعقيد الواجهة الخلفية المتزايد، فضلاً عن تأثيرها على الأداء، إذ يجب التحقق من صلاحية الرمز في قاعدة البيانات عند كل طلب API. والوصول إلى قاعدة البيانات أبطأ بكثير مقارنةً بالتحقق من صلاحية الرمز نفسه. ولهذا من الشائع جداً حفظ الجلسة المقابلة لرمز ما في قاعدة بيانات مفتاح-قيمة مثل Redis، وهي محدودة الوظائف مقارنةً بـ MongoDB مثلاً أو بقاعدة بيانات علائقية، لكنها سريعة للغاية في بعض سيناريوهات الاستخدام.

عند استخدام الجلسات من جهة الخادم، غالباً ما يكون الرمز مجرد نص عشوائي لا يتضمن أي معلومات عن المستخدم، خلافاً لما هو شائع عند استخدام رموز JWT. فعند كل طلب API، يجلب الخادم المعلومات ذات الصلة بهوية المستخدم من قاعدة البيانات. ومن الشائع أيضاً أنه بدلاً من استخدام ترويسة Authorization، تُستخدم ملفات تعريف الارتباط (cookies) كآلية لنقل الرمز بين العميل والخادم.

ملاحظات ختامية

جرت تغييرات كثيرة على الشيفرة تسببت في مشكلة نمطية في مشروع برمجي سريع الوتيرة: تعطّل معظم الاختبارات. ولأن هذا الجزء من الدورة مكدّس بالفعل بالمعلومات الجديدة، سنترك إصلاح الاختبارات لتمرين غير إلزامي.

يجب دائماً استخدام أسماء المستخدمين وكلمات المرور والتطبيقات التي تعتمد المصادقة بالرموز عبر HTTPS. ويمكننا استخدام خادم HTTPS من Node في تطبيقنا بدلاً من خادم HTTP (وهو يتطلب إعدادات أكثر). وفي المقابل، النسخة الإنتاجية من تطبيقنا موجودة على Fly.io، لذا يبقى تطبيقنا آمناً: فـ Fly.io يوجّه كل حركة المرور بين المتصفح وخادم Fly.io عبر HTTPS.

سننفّذ تسجيل الدخول في الواجهة الأمامية في الجزء التالي.

تمارين 4.15.-4.23.

في التمارين التالية، ستُنفَّذ أساسيات إدارة المستخدمين في تطبيق قائمة المدونات. وأسلم طريقة هي اتباع مادة الدورة من فصل إدارة المستخدمين في الجزء 4 إلى فصل المصادقة بالرموز. ويمكنك طبعاً استخدام إبداعك أيضاً.

تحذير آخر: إذا لاحظت أنك تخلط بين async/await واستدعاءات then، فمن المؤكد بنسبة 99% أنك ترتكب خطأً ما. استخدم أحد الأسلوبين فقط، ولا تجمعهما أبداً.

4.15: توسيع قائمة المدونات، الخطوة 3

نفّذ طريقة لإنشاء مستخدمين جدد بإجراء طلب HTTP POST إلى العنوان api/users. وللمستخدمين username و password و name.

لا تحفظ كلمات المرور في قاعدة البيانات كنص صريح، بل استخدم مكتبة bcrypt كما فعلنا في فصل إنشاء المستخدمين من الجزء 4.

ملاحظة واجه بعض مستخدمي Windows مشكلات مع bcrypt. إذا صادفت مشكلات، فأزل المكتبة بالأمر

npm uninstall bcrypt 

وثبّت bcryptjs بدلاً منها.

نفّذ طريقة لعرض تفاصيل جميع المستخدمين بإجراء طلب HTTP مناسب.

يمكن أن تبدو قائمة المستخدمين، على سبيل المثال، كما يلي:

واجهة المتصفح api/users تعرض بيانات JSON لمستخدمين اثنين

4.16*: توسيع قائمة المدونات، الخطوة 4

أضف ميزة تفرض القيود التالية على إنشاء المستخدمين الجدد: يجب إعطاء اسم المستخدم وكلمة المرور معاً، ويجب أن يتكوّن كل منهما من 3 أحرف على الأقل. ويجب أن يكون اسم المستخدم فريداً.

يجب أن تستجيب العملية برمز حالة مناسب ورسالة خطأ من نوع ما إذا أُنشئ مستخدم غير صالح.

ملاحظة لا تختبر قيود كلمة المرور بتحققات Mongoose. فهذه ليست فكرة جيدة لأن كلمة المرور التي تستقبلها الواجهة الخلفية وتجزئة كلمة المرور المحفوظة في قاعدة البيانات ليسا الشيء نفسه. ينبغي التحقق من طول كلمة المرور في المتحكم كما فعلنا في الجزء 3 قبل استخدام تحقق Mongoose.

كما نفّذ اختبارات تضمن عدم إنشاء مستخدمين غير صالحين، وأن عملية إضافة مستخدم غير صالحة تعيد رمز حالة ورسالة خطأ مناسبين.

ملاحظة إذا قررت تعريف الاختبارات في ملفات متعددة، فينبغي أن تعلم أن كل ملف اختبار يُنفَّذ افتراضياً في عملية خاصة به (انظر نموذج تنفيذ الاختبارات في التوثيق). ونتيجة ذلك أن ملفات الاختبار المختلفة تُنفَّذ في الوقت نفسه. ولأن الاختبارات تتشارك قاعدة البيانات نفسها، فقد يسبب التنفيذ المتزامن مشكلات، يمكن تجنّبها بتنفيذ الاختبارات بالخيار --test-concurrency=1، أي تعريفها لتُنفَّذ تسلسلياً.

4.17: توسيع قائمة المدونات، الخطوة 5

وسّع المدونات بحيث تحتوي كل مدونة على معلومات عن منشئ المدونة.

عدّل إضافة المدونات الجديدة بحيث يُعيَّن أي مستخدم من قاعدة البيانات منشئاً للمدونة عند إنشائها (مثلاً أول مستخدم يُعثر عليه). نفّذ هذا وفق فصل populate من الجزء 4. ولا يهم حتى الآن أي مستخدم يُعيَّن منشئاً. وستكتمل الوظيفة في التمرين 4.19.

عدّل عرض جميع المدونات بحيث تُعرض معلومات المستخدم المنشئ مع المدونة:

api/blogs يدمج معلومات المستخدم المنشئ في بيانات JSON

كما يعرض عرض جميع المستخدمين المدونات التي أنشأها كل مستخدم:

api/users يدمج المدونات في بيانات JSON

4.18: توسيع قائمة المدونات، الخطوة 6

نفّذ المصادقة القائمة على الرموز وفق فصل المصادقة بالرموز من الجزء 4.

4.19: توسيع قائمة المدونات، الخطوة 7

عدّل إضافة المدونات الجديدة بحيث لا تصبح ممكنة إلا إذا أُرسل رمز صالح مع طلب HTTP POST. ويُعيَّن المستخدم الذي يعرّفه الرمز منشئاً للمدونة.

4.20*: توسيع قائمة المدونات، الخطوة 8

هذا المثال من الجزء 4 يعرض أخذ الرمز من الترويسة بالدالة المساعدة getTokenFrom في controllers/blogs.js.

إذا استخدمت الحل نفسه، فأعد هيكلة أخذ الرمز إلى وسيط. وينبغي أن يأخذ الوسيط الرمز من ترويسة Authorization ويسنده إلى الحقل token في كائن request.

بعبارة أخرى، إذا سجّلت هذا الوسيط في الملف app.js قبل جميع المسارات

app.use(middleware.tokenExtractor)

يمكن للمسارات الوصول إلى الرمز عبر request.token:

blogsRouter.post('/', async (request, response) => {
  // ..
  const decodedToken = jwt.verify(request.token, process.env.SECRET)
  // ..
})

تذكّر أن دالة الوسيط العادية دالة بثلاثة معاملات، تستدعي في النهاية المعامل الأخير next لنقل التحكم إلى الوسيط التالي:

const tokenExtractor = (request, response, next) => {
  // شيفرة تستخرج الرمز

  next()
}

4.21*: توسيع قائمة المدونات، الخطوة 9

غيّر عملية حذف المدونة بحيث لا يمكن حذف المدونة إلا للمستخدم الذي أضافها. لذا لا يصبح حذف المدونة ممكناً إلا إذا كان الرمز المُرسَل مع الطلب هو نفسه رمز منشئ المدونة.

إذا جرت محاولة حذف مدونة دون رمز أو بمستخدم غير صالح، فينبغي أن تعيد العملية رمز حالة مناسباً.

لاحظ أنه إذا جلبت مدونة من قاعدة البيانات،

const blog = await Blog.findById(...)

فإن الحقل blog.user لا يحتوي نصاً، بل كائناً. لذا إذا أردت مقارنة معرّف الكائن المجلوب من قاعدة البيانات بمعرّف نصي، فلن تنجح عملية مقارنة عادية. ويجب أولاً تحويل المعرّف المجلوب من قاعدة البيانات إلى نص.

if ( blog.user.toString() === userid.toString() ) ...

4.22*: توسيع قائمة المدونات، الخطوة 10

يحتاج كل من إنشاء مدونة جديدة وحذف مدونة إلى معرفة هوية المستخدم الذي يجري العملية. والوسيط tokenExtractor الذي أنشأناه في التمرين 4.20 يساعد، لكن ما يزال على كل من معالجي عمليتي post وdelete معرفة من هو المستخدم الحامل لرمز معين.

أنشئ الآن وسيطاً جديداً باسم userExtractor يحدد المستخدم المرتبط بالطلب ويرفقه بكائن الطلب. وبعد تسجيل الوسيط، ينبغي أن يتمكن معالجا post و delete من الوصول إلى المستخدم مباشرةً بالرجوع إلى request.user:

blogsRouter.post('/', userExtractor, async (request, response) => {
  // احصل على المستخدم من كائن الطلب
  const user = request.user
  // ..
})

blogsRouter.delete('/:id', userExtractor, async (request, response) => {
  // احصل على المستخدم من كائن الطلب
  const user = request.user
  // ..
})

لاحظ أن وسيط userExtractor سُجِّل في هذه الحالة مع مسارات فردية، لذا لا يُنفَّذ إلا في حالات معينة. فبدلاً من استخدام userExtractor مع جميع المسارات،

// استخدم الوسيط في جميع المسارات
app.use(middleware.userExtractor)

app.use('/api/blogs', blogsRouter)  
app.use('/api/users', usersRouter)
app.use('/api/login', loginRouter)

يمكننا تسجيله بحيث لا يُنفَّذ إلا مع مسارات /api/blogs:

// استخدم الوسيط فقط في مسارات /api/blogs
app.use('/api/blogs', middleware.userExtractor, blogsRouter)
app.use('/api/users', usersRouter)
app.use('/api/login', loginRouter)

ويُفعل هذا بربط دوال وسيط متعددة كمعاملات للدالة use. وبالطريقة نفسها، يمكن أيضاً تسجيل الوسيط لمسارات فردية فقط:

router.post('/', userExtractor, async (request, response) => {
  // ...
})

تأكد من أن جلب جميع المدونات بطلب GET ما يزال يعمل دون رمز.

4.23*: توسيع قائمة المدونات، الخطوة 11

بعد إضافة المصادقة القائمة على الرموز تعطّلت اختبارات إضافة مدونة جديدة. أصلحها. واكتب أيضاً اختباراً جديداً يضمن فشل إضافة مدونة برمز الحالة المناسب 401 Unauthorized إذا لم يُقدَّم رمز.

من المرجح أن يكون هذا مفيداً عند إجراء الإصلاح.

هذا هو التمرين الأخير في هذا الجزء من الدورة، وقد حان وقت دفع شيفرتك إلى GitHub وتعليم جميع تمارينك المنجزة في نظام إرسال التمارين.