المسارات المحمية بتسجيل الدخول
جزء من قسم المصادقة والأمان في رحلة Node.js على Coddy. الدرس 11 من 24.
يحتاج كل مسار خاص إلى البحث عن الجلسة من GET /me. إذا نُسخ هذا البحث إلى كل مسار، فسيُنسى في أحدها، وسيصبح ذلك المسار متاحًا للجميع. اكتب هذا البحث مرة واحدة كبرمجية وسيطة بدلًا من ذلك.
تعمل البرمجية الوسيطة قبل المسار. عندما تضع شيئًا في req، مثل req.user، يقرأه المسار من هناك. تكون قيمة _id للمستخدم من نوع ObjectId؛ وتعطي user._id.toHexString() قيمتها كنص. إما أن ترسل استجابة وتتوقف، أو تضع ما عثرت عليه في req وتستدعي next(). مرّرها إلى كل مسار يحتاج إليها، قبل المعالج:
function requireAdult(req, res, next) {
if (Number(req.query.age) < 18) {
return res.status(403).json({ error: 'Adults only' });
}
req.adult = true;
next();
}
app.get('/wines', requireAdult, (req, res) => res.json(wines));إن return مهم: فبدونه سيُنفَّذ المسار أيضًا.
التحدي
سهللا تزال الملاحظات متاحة للجميع. اجعلها خاصة: لا يمكن قراءة الملاحظات أو إضافتها إلا للمستخدم الذي سجّل الدخول، ولا يرى كل مستخدم سوى ملاحظاته.
- في
server.js، اكتب دالة وسيطةrequireUser(req, res, next). انقل البحث عن الجلسة منGET /meإليها: اقرأ ملف تعريف الارتباطsid، وابحث عن الجلسة، ثم ابحث عن المستخدم المرتبط بها.- إذا لم يكن هناك مستخدم كهذا، فأرسل استجابة برمز الحالة 401 و
{"error": "Log in first"}، وتوقف عند هذا الحد. - وإلا، فأضف المستخدم إلى الطلب في
req.user، وهو كائن يحتوي على حقلين، ثم استدعِnext(). الحقلان هماid، وهو_idالخاص بالمستخدم كنص، وusername. تعطي الطريقةtoHexString()الخاصة بـ ObjectId القيمة النصية، مثل"66f1c2a9e4b0a1b2c3d4e5f6".
- إذا لم يكن هناك مستخدم كهذا، فأرسل استجابة برمز الحالة 401 و
- مرّر
requireUserإلىGET /meوGET /notesوPOST /notes، قبل دالة كل مسار. عندئذٍ تستجيبGET /meبـ{"username": "ada"}، المقروءة منreq.user. - في
POST /notes، احفظ كل ملاحظة مع حقلownerيحتوي علىreq.user.id. الاستجابة هي الملاحظة الجديدة:{"owner": "66f1c2a9e4b0a1b2c3d4e5f6", "text": "Door code 1234", "_id": "66f1c2b0e4b0a1b2c3d4e5f7"} - في
GET /notes، أعد الملاحظات التي يكونownerفيها هو المستخدم الذي سجّل الدخول فقط.
جرّب ذلك: اضغط على «العب». أرسل GET /notes: رمز الحالة 401 و{"error": "Log in first"}. أرسل POST /signup، ثم POST /login، مع {"username": "ada", "password": "correct horse"} في كليهما. أرسل POST /notes مع {"text": "Door code 1234"}: رمز الحالة 201 والملاحظة مع owner الخاص بها. أرسل GET /notes: قائمة تحتوي على تلك الملاحظة وحدها. والآن، أنشئ حسابًا وسجّل الدخول باستخدام {"username": "bob", "password": "correct horse"}. يعيد GET /notes القيمة []، لأن bob لم يكتب أي شيء.
جرّب بنفسك
import express from 'express';
import { randomBytes } from 'node:crypto';
import { parseCookies } from './cookies.js';
import { notes, sessions, users } from './db.js';
import { hashPassword, verifyPassword } from './passwords.js';
const app = express();
app.use(express.json());
app.post('/signup', async (req, res) => {
const username = String(req.body.username ?? '').trim().toLowerCase();
const password = String(req.body.password ?? '');
if (!username || password.length < 8) {
return res.status(400).json({ error: 'Pick a username and a password of at least 8 characters' });
}
if (await users.findOne({ username })) {
return res.status(409).json({ error: 'That username is taken' });
}
await users.insertOne({ username, passwordHash: await hashPassword(password) });
res.status(201).json({ username });
});
app.post('/login', async (req, res) => {
const username = String(req.body.username ?? '').trim().toLowerCase();
const user = await users.findOne({ username });
if (!user || !(await verifyPassword(String(req.body.password ?? ''), user.passwordHash))) {
return res.status(401).json({ error: 'Wrong username or password' });
}
const sid = randomBytes(24).toString('base64url');
await sessions.insertOne({ _id: sid, userId: user._id, createdAt: new Date() });
res.cookie('sid', sid, { httpOnly: true });
res.json({ username: user.username });
});
app.get('/me', async (req, res) => {
const { sid } = parseCookies(req.get('cookie'));
const session = sid && (await sessions.findOne({ _id: sid }));
if (!session) {
return res.status(401).json({ error: 'Log in first' });
}
const user = await users.findOne({ _id: session.userId });
res.json({ username: user.username });
});
// يحسب عدد المرات التي أرسل فيها هذا المتصفح طلب GET إلى /visit.
app.get('/visit', (req, res) => {
const cookies = parseCookies(req.get('cookie'));
const visits = Number(cookies.visits ?? 0) + 1;
res.cookie('visits', String(visits));
res.json({ visits });
});
// الملاحظات. يمكن لأي شخص قراءتها وكتابتها في الوقت الحالي.
app.get('/notes', async (req, res) => {
res.json(await notes.find().toArray());
});
app.post('/notes', async (req, res) => {
const note = { text: String(req.body.text ?? '') };
await notes.insertOne(note);
res.status(201).json(note);
});
app.listen(3000, () => {
console.log('Vault is running on port 3000');
});
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس المصادقة والأمان
2الجلسات وملفات تعريف الارتباط
التذكّر باستخدام ملفات تعريف الارتباطالبحث عن جلسةجلسات تسجيل الدخولالمسارات المحمية بتسجيل الدخولتدرّب بنفسك: Playground لـ Express