Menu
CoddyTech

ملاحظاتك وحدك

جزء من قسم المصادقة والأمان في رحلة Node.js على Coddy. الدرس 18 من 24.

يثبت تسجيل الدخول هويتك، لكنه لا يثبت أن ملاحظةً ما تخصك. يبحث GET /notes/:id عن ملاحظة باستخدام معرّفها وحده، لذا يستطيع أي شخص سجّل الدخول ولديه معرّف، من رابط تمت مشاركته أو سجلّ، قراءة تلك الملاحظة. يُسمّى التحقق من المالك التفويض.

ضع المالك في الاستعلام نفسه، كي لا تُرجع قاعدة البيانات مطلقًا مستندًا يخص شخصًا آخر. أجب برمز 404، لا 403، عندما لا يُعثر على المستند: فالردّ برمز 403 سيخبر الزائر بأن الملاحظة موجودة.

const order = await orders.findOne({
    _id: new ObjectId(req.params.id),
    customer: req.user.id,
});
if (!order) {
    return res.status(404).json({ error: 'Order not found' });
}

يحتاج كل استعلام يتعامل مع بيانات مستخدم إلى هذا المرشح: استعلامات القراءة والتحديث والحذف.

challenge icon

التحدي

سهل

يعود هذا الفصل إلى تسجيل الدخول باستخدام الجلسات من الفصل 2، وقد تطوّر الخادم. أصبح لديه الآن صفحة على GET /، ومسارات لملاحظة واحدة على GET /notes/:id وDELETE /notes/:id، وحساب مسؤول. في مساري /notes/:id ثغرة: فهما يعثران على الملاحظة باستخدام معرّفها وحده. يمكن لأي مستخدم سجّل الدخول ويعرف معرّفًا أن يقرأ ملاحظة شخص آخر أو يحذفها. عالج هذه الثغرة.

  1. في server.js، ضمن GET /notes/:id، ابحث عن الملاحظة باستخدام _id وowner معًا. يجب أن يكون المالك هو المستخدم الذي سجّل الدخول، req.user.id.
  2. افعل الشيء نفسه في DELETE /notes/:id، حتى لا يتمكن أحد من حذف ملاحظة لا يملكها.
  3. عندما لا تطابق أي ملاحظة، يستجيب كلا المسارين بالفعل بالحالة 404 و{"error": "Note not found"}. أبقِ على هذه الاستجابة لملاحظة شخص آخر أيضًا. فالاستجابة 403 ستخبر الزائر بأن الملاحظة موجودة.

جرّب ذلك: اضغط «تشغيل».

  • أرسل POST /signup، ثم POST /login، وكلاهما مع {"username": "ada", "password": "correct horse"}.
  • أرسل POST /notes مع {"text": "Safe code 4417"}، وانسخ _id من الاستجابة. يعيد GET /notes/ متبوعًا بذلك المعرّف الملاحظة.
  • أنشئ حسابًا وسجّل الدخول باستخدام {"username": "bob", "password": "correct horse"}. أرسل GET /notes/ متبوعًا بمعرّف ada: الحالة 404 و{"error": "Note not found"}. أرسل DELETE /notes/ متبوعًا بالمعرّف نفسه: وستكون النتيجة 404 أيضًا.
  • سجّل الدخول بصفتك ada مجددًا، باستخدام البيانات التي أرسلتها في الخطوة الأولى. ما زالت ملاحظتها موجودة. احذفها الآن باستخدام DELETE: الحالة 204.

جرّب بنفسك

import express from 'express';
import { ObjectId } from 'mongodb';
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());

// حساب المشرف الوحيد. تأتي كلمة مروره من البيئة.
await users.insertOne({ username: 'admin', role: 'admin', passwordHash: await hashPassword(process.env.ADMIN_PASSWORD) });

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,
        sameSite: 'lax',
        secure: true,
        maxAge: 24 * 60 * 60 * 1000,
    });
    res.json({ username: user.username });
});

// يعثر على المستخدم المرتبط بملف تعريف ارتباط الجلسة، أو يردّ برمز 401.
async function requireUser(req, res, next) {
    const { sid } = parseCookies(req.get('cookie'));
    const session = sid && (await sessions.findOne({ _id: sid }));
    const user = session && (await users.findOne({ _id: session.userId }));
    if (!user) {
        return res.status(401).json({ error: 'Log in first' });
    }
    req.user = { id: user._id.toHexString(), username: user.username, role: user.role };
    next();
}

app.get('/me', requireUser, (req, res) => {
    res.json({ username: req.user.username });
});

app.post('/logout', async (req, res) => {
    const { sid } = parseCookies(req.get('cookie'));
    if (sid) {
        await sessions.deleteOne({ _id: sid });
    }
    res.clearCookie('sid');
    res.status(204).end();
});

// ملاحظاتك معروضة في صفحة.
app.get('/', requireUser, async (req, res) => {
    const list = await notes.find({ owner: req.user.id }).toArray();
    const items = list.map((note) => `<li>${note.text}</li>`).join('');
    res.send(`<!doctype html><title>Vault</title><h1>${req.user.username}'s notes</h1><ul>${items}</ul>`);
});

// الملاحظات: يقرأ كل مستخدم ملاحظاته الخاصة ويكتبها فقط.
app.get('/notes', requireUser, async (req, res) => {
    res.json(await notes.find({ owner: req.user.id }).toArray());
});

app.post('/notes', requireUser, async (req, res) => {
    const note = { owner: req.user.id, text: String(req.body.text ?? '') };
    await notes.insertOne(note);
    res.status(201).json(note);
});

app.get('/notes/:id', requireUser, async (req, res) => {
    if (!ObjectId.isValid(req.params.id)) {
        return res.status(404).json({ error: 'Note not found' });
    }
    const note = await notes.findOne({ _id: new ObjectId(req.params.id) });
    if (!note) {
        return res.status(404).json({ error: 'Note not found' });
    }
    res.json(note);
});

app.delete('/notes/:id', requireUser, async (req, res) => {
    if (!ObjectId.isValid(req.params.id)) {
        return res.status(404).json({ error: 'Note not found' });
    }
    const { deletedCount } = await notes.deleteOne({ _id: new ObjectId(req.params.id) });
    if (!deletedCount) {
        return res.status(404).json({ error: 'Note not found' });
    }
    res.status(204).end();
});

// للمشرفين: عدد الحسابات الموجودة.
app.get('/admin', requireUser, async (req, res) => {
    res.json({ users: await users.countDocuments() });
});

app.listen(3000, () => {
    console.log('Vault is running on port 3000');
});
quiz iconاختبر نفسك

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

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

تدرّب بنفسك: Playground لـ Express