Menu
CoddyTech

تسجيل الخروج

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

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

تطلب res.clearCookie(name) من المتصفح إسقاط ملف تعريف ارتباط. وتعني الحالة 204 «لا يوجد محتوى» أن العملية نجحت وأنه لا يوجد شيء لإرساله: res.status(204).end().

إليك مسارًا ذا صلة يسجّل خروج المستخدم من جميع الأجهزة دفعة واحدة:

app.post('/logout-everywhere', requireUser, async (req, res) => {
    await sessions.deleteMany({ userId: new ObjectId(req.user.id) });
    res.clearCookie('sid');
    res.status(204).end();
});
challenge icon

التحدي

سهل

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

  1. في server.js، أضف POST /logout. لا يحتاج إلى متن للطلب.
  2. اقرأ ملف تعريف الارتباط sid. إذا وُجد، فاحذف الجلسة التي لها _id هذا من sessions.
  3. أخبر المتصفح أن ينسى ملف تعريف الارتباط sid.
  4. أرسل استجابة بالحالة 204 ومن دون متن. تعني الحالة 204 No Content أن العملية نجحت وليس هناك شيء لإرساله. أرسل استجابة بالحالة 204 حتى عندما لا يتضمن الطلب ملف تعريف الارتباط sid.

جرّب ذلك: اضغط «العب». أرسل POST /signup، ثم POST /login، مع {"username": "ada", "password": "correct horse"} في كليهما. يعيد GET /me القيمة {"username": "ada"}. أرسل POST /logout: الحالة 204 ومتن فارغ. أرسل GET /me مرة أخرى: الحالة 401 و{"error": "Log in first"}. افتح البيانات: مجموعة sessions فارغة. أرسل POST /logout مرة أخرى: تظل الحالة 204.

جرّب بنفسك

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 });
});

// يعثر على المستخدم المرتبط بملف تعريف ارتباط الجلسة، أو يردّ برمز 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 };
    next();
}

app.get('/me', requireUser, (req, res) => {
    res.json({ username: req.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', 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.listen(3000, () => {
    console.log('Vault is running on port 3000');
});

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

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