Menu
CoddyTech

المسارات المحمية بتسجيل الدخول

جزء من قسم المصادقة والأمان في رحلة 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 مهم: فبدونه سيُنفَّذ المسار أيضًا.

challenge icon

التحدي

سهل
  • اكتب 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');
});
quiz iconاختبر نفسك

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

جميع دروس المصادقة والأمان