Menu
CoddyTech

תפקידי מנהל מערכת

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

הבקשה GET /admin בודקת רק שמישהו מחובר, ולכן כל חבר רואה אותה. תפקיד שמאוחסן אצל המשתמש, כמו 'member' או 'admin', מציין מה מותר לו לעשות. מפעל middleware, פונקציה שמחזירה middleware, בודק תפקיד אחד לכל נתיב:

function requirePlan(plan) {
    return (req, res, next) => {
        if (req.user.plan !== plan) {
            return res.status(402).json({ error: `Needs the ${plan} plan` });
        }
        next();
    };
}

app.get('/exports', requireUser, requirePlan('pro'), sendExports);

התפקיד תמיד נקבע על ידי השרת. ההרשמה מעתיקה רק את השדות שהיא מצפה להם מתוך req.body, כך שמבקר ששולח "role": "admin" לא מקבל מכך דבר.

challenge icon

אתגר

קל
  • תהליך ההרשמה שומר role: 'member', בלי קשר למה שמצוין בגוף הבקשה.
  • כתבו את requireRole(role): החזירו 403 עם { "error": "Not allowed" } אלא אם req.user.role תואם. GET /admin משתמש ב־requireRole('admin') אחרי requireUser.

הירשמו עם "role": "admin" ושלחו GET /admin. לאחר מכן התחברו בתור admin עם admin-pass-for-tests, הסיסמה של ADMIN_PASSWORD.

נסו בעצמכם

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), owner: req.user.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), owner: req.user.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');
});

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