المسارات المحمية بتسجيل الدخول
جزء من قسم المصادقة والأمان في رحلة Node.js على Coddy. الدرس 6 من 17.
يحتاج كل مسار خاص إلى البحث عن الجلسة الموجود في GET /me. إذا نُسخ هذا البحث إلى كل مسار، فسيُنسى في أحدها، وسيصبح ذلك المسار متاحًا للجميع. اكتب هذا البحث مرة واحدة كبرمجية وسيطة بدلًا من ذلك.
تعمل البرمجية الوسيطة قبل المسار. فإما أن ترسل إجابة وتتوقف، أو تضع ما عثرت عليه في 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 مهم: فبدونه سيُنفَّذ المسار أيضًا.
التحدي
سهل- اكتب
requireUser: استجابة 401{ "error": "Log in first" }من دون جلسة صالحة، وإلا فليكنreq.user = { id, username }ثمnext(). - تستخدمه
GET /meوGET /notesوPOST /notes. تخزّن الملاحظاتowner: req.user.id، ولا يعرضGET /notesإلا ملاحظاتك.
أرسل GET /notes أثناء تسجيل الخروج، ثم سجّل الدخول وأضف ملاحظة.
جرّب بنفسك
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الجلسات وملفات تعريف الارتباط
التذكّر باستخدام ملفات تعريف الارتباطجلسات تسجيل الدخول