Menu
CoddyTech

נתיבים מאחורי התחברות

חלק מהיחידה אימות ואבטחה במסלול ה-Node.js של Coddy. שיעור 11 מתוך 24.

כל נתיב פרטי זקוק לחיפוש הפגישה מתוך GET /me. אם מעתיקים אותו לכל נתיב, פעם אחת ישכחו להוסיף אותו, ואז הנתיב הזה יהיה פתוח לכולם. במקום זאת, כתבו אותו פעם אחת בתור תוכנת ביניים.

תוכנת הביניים פועלת לפני הנתיב. כשהיא מציבה משהו בתוך req, כמו req.user, הנתיב קורא אותו משם. ה־_id של משתמש הוא ObjectId; ‏user._id.toHexString() מחזירה אותו כטקסט. היא משיבה ועוצרת, או מציבה בתוך 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

אתגר

קל

ההערות עדיין פתוחות לכולם. הפכו אותן לפרטיות: רק משתמש מחובר יכול לקרוא הערות או להוסיף אותן, וכל משתמש רואה רק את ההערות שלו.

  1. ב־server.js, כתבו פונקציית תוכנת ביניים requireUser(req, res, next). העבירו אליה את חיפוש ההפעלה מ־GET /me: קראו את עוגיית sid, מצאו את ההפעלה ואז מצאו את המשתמש שלה.
    • כשאין משתמש כזה, השיבו עם סטטוס 401 ועם {"error": "Log in first"}, ועצרו שם.
    • אחרת, הציבו את המשתמש בבקשה בתור req.user, אובייקט עם שני שדות, ואז קראו ל־next(). שני השדות הם id, ה־_id של המשתמש כטקסט, ו־username. המתודה toHexString() של ObjectId מחזירה את הערך שלו כטקסט, למשל "66f1c2a9e4b0a1b2c3d4e5f6".
  2. העבירו את requireUser אל GET /me, אל GET /notes ואל POST /notes, לפני הפונקציה של כל נתיב. לאחר מכן, GET /me משיב עם {"username": "ada"}, שנקרא מתוך req.user.
  3. ב־POST /notes, שמרו כל הערה עם שדה owner שמכיל את req.user.id. התשובה היא ההערה החדשה: {"owner": "66f1c2a9e4b0a1b2c3d4e5f6", "text": "Door code 1234", "_id": "66f1c2b0e4b0a1b2c3d4e5f7"}
  4. ב־GET /notes, החזירו רק את ההערות שה־owner שלהן הוא המשתמש המחובר.

נסו זאת: לחצו על Run. שלחו GET /notes: הסטטוס הוא 401 ו־{"error": "Log in first"}. שלחו POST /signup, ואז POST /login, ובשניהם השתמשו ב־{"username": "ada", "password": "correct horse"}. שלחו POST /notes עם {"text": "Door code 1234"}: הסטטוס הוא 201 והתגובה כוללת את ההערה עם owner שלה. שלחו GET /notes: רשימה שמכילה את ההערה היחידה הזאת. עכשיו הירשמו והתחברו עם {"username": "bob", "password": "correct horse"}. הבקשה GET /notes מחזירה [], כי bob עדיין לא כתב דבר.

נסו בעצמכם

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בחנו את עצמכם

השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.

כל השיעורים ביחידה אימות ואבטחה

תרגלו בעצמכם: Express אונליין