Menu
CoddyTech

تسجيل الخروج

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

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

تطلب 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

التحدي

سهل
  • يحذف POST /logout جلسة ملف تعريف الارتباط sid، ويمسح ملف تعريف الارتباط، ويُرجع 204.
  • حتى بدون جلسة، يُرجع 204.

سجّل الدخول، وأرسل POST /logout، ثم GET /me.

جرّب بنفسك

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

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